React原理:通俗易懂的 JSX 与 虚拟DOM

lxf2023-05-06 00:41:36

前言

React 的编写语法是 JSX、TSX, 但是他们并不能被浏览器识别,即运行的结果不能呈现在浏览器中。那么 React 是如何解决这个问题的呢?本文通过简易的 demo,理清 vdom 和 jsx 的原理

JSX

JSX 本质上是个语法糖,在运行的时候,需要通过 babel 编译。

比如:

const ele = <div>123</div>

最终会被 babel 编译成下面的代码

const ele = React.createElement(
    'div',
    {},
    "123"
)

而上面 createElement(vdom) 中的 vdom 其实就是虚拟DOM。

VDOM

VDOM 可以理解为 babel 编译后的一种 数据结构

const ele = (
    <div className='container'>
        div文本
        <h1> 我是h1 </h1>
        <div onClick={() => alert(123)}> 
            啊啊啊
        </div>
    </div>
)

在经过 babel 编译后会得到如下的结果:

React原理:通俗易懂的 JSX 与 虚拟DOM

简单点写就是:

const ele = {
    type: "div",
    props: {
        className: "container",
    },
    children: [
        "div文本",
        {
            type: "h1",
            props: {},
            children: ["我是h1"]
        },
        {
            type: "div",
            props: {
                onClick: function() {
                    alert(123)
                }
            },
            children: [
                "啊啊啊"
            ]
        }
    ]
}

这就是 VDOM。

VDOM 渲染

那既然 React 代码被 babel 编译成了 React.createElement 包裹的 VDOM,那么 VDOM 又是如何渲染到页面上的呢?我们可以通过简单的 demo 来理解它的原理。

React.createElement

从上面编译的结果来看,当通过 React.render 时,会将 ele 转成 vdom

React.render(
  ele,
  document.getElementById('root')
)
//等价于
React.render(
  React.createElement(vdom),
  document.getElementById('root')
);

所以 createElement 应该接受如下参数:

  1. type:当前 vdom 的类型,比如 html标签、自定义组件
  2. attrs:当前 vdom 的属性,比如 style、className、绑定的事件
  3. children:子节点

所以 createElement 实现如下:

const React = {
    createElement,
}

function createElement(type, attrs, ...children) {
    //...children 是因为子节点数量不固定,拓展出来
    return {
        type,
        attrs,
        children
    }
}

createElement 返回一个用来记录 VDOM 信息的对象,这样,我们就可以通过 vdom 去生成真实dom。即,我们需要实现 render 方法

React.render

render 其实也不难,总得来说过程如下:

  1. 根据 type ,判断当前节点是什么类型,创建对应类型的真实DOM。
  2. 根据 attrs,为这个 真实DOM 设置属性。
  3. 根据 children递归执行 render,生成子节点的 真实DOM。
  4. 把最终的真实DOM结果返回。

代码如下:

/**
 * 
 * @param {*} vnode 虚拟dom 
 * @param {*} container 根节点容器
 * @returns 
 */
function render(vnode, container) {
    //如果是文本类型
    if (isTextDom(vnode)) {
        const textNode = document.createTextNode(vnode);
        return container.appendChild(textNode);
    } else if (isElementVdom(vnode)) {
        //如果是元素类型
        //根据 type 创建对应的元素
        const dom = document.createElement(vnode.type);
        
        //添加属性
        if(vnode.attrs) {
            Object.keys(vnode.attrs).forEach((key) => {
               const value = vnode.attrs[key];
               setAttribute(dom, key, value);
            });
        }
        
        //递归 children 生成子节点真实dom
        vnode.children.forEach((child) => render(child, dom));
        //返回最终的结果
        return container.appendChild(dom);
    }
}

function isTextDom(vdom) {
    return typeof vdom === 'string' || vdom === 'number'
}

function isElementVdom(vdom) {
    return typeof vdom === 'object'&& typeof vdom.type === 'string'
}

function setAttribute(dom, key, value) {
    //如果是事件属性,通过 addEventListener 绑定
    if (typeof value == "function" && key.startsWith("on")) {
       const eventType = key.slice(2).toLowerCase();
       dom.addEventListener(eventType, value);
    } else if (key == "style" && typeof value == "object") {
       // 如果是 style
       Object.assign(dom.style, value);
    } else if (key === 'className') {
       // 如果是 className, 改成 class
       dom.setAttribute('class', value);
    } else if (typeof value != "object" && typeof value != "function") {
       // 如果是普通属性
       dom.setAttribute(key, value);
    }
}

最后,将 render 挂在到 ReactDOM 上:

const ReactDOM = {
    render: (vnode, container) => {
        container.innerHTML = '';
        return render(vnode, container);
    }
}

以上,我们就实现了 vdom 的渲染。现在我们来测试一下。

测试 demo

首先,创建一个 react 应用。

mkdir react_vdom && cd react_vdom && npm init -y

其次,添加相关依赖,其中,parcel是零配置前端打包工具,推荐各种 demo 里面用。

yarn add -D @babel/preset-react @babel/preset-env parcel-bundler

然后,添加 .babelrc 文件

//.babelrc
{
    "presets": ["@babel/preset-react"]
}

根目录下,创建 src/index.js,将上面的代码拷贝

// src/index.js
const React = {
  createElement,
};

const ReactDOM = {
  render: (vnode, container) => {
    container.innerHTML = "";
    return render(vnode, container);
  },
};

function createElement(tag, attrs, ...children) {
  return {
    tag,
    attrs,
    children,
  };
}

function render(vnode, container) {
  if (typeof vnode === "string") {
    let textNode = document.createTextNode(vnode);
    return container.appendChild(textNode);
  }

  const dom = document.createElement(vnode.tag);

  if (vnode.attrs) {
    Object.keys(vnode.attrs).forEach((key) => {
      const value = vnode.attrs[key];
      setAttribute(dom, key, value);
    });
  }

  vnode.children.forEach((child) => render(child, dom));

  return container.appendChild(dom);
}

function setAttribute(dom, key, value) {
  if (typeof value == "function" && key.startsWith("on")) {
    const eventType = key.slice(2).toLowerCase();
    dom.addEventListener(eventType, value);
  } else if (key == "style" && typeof value == "object") {
    Object.assign(dom.style, value);
  } else if (key === 'className') {
    dom.setAttribute('class', value)
  } else if (typeof value != "object" && typeof value != "function") {
    dom.setAttribute(key, value);
  }
}

const ele = (
  <div className='container'>
    div文本
    <h1> 我是h1 </h1>
    <div onClick={() => alert(2)}>啊啊啊</div>
  </div>
);

ReactDOM.render(ele, document.getElementById("root")); //等价于ReactDOM.render(React.createElement(vdom), document.getElementById('root'))

根目录下,创建 index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>react_vdom_code</title>
</head>
<body>
    <div id="root"></div>
    <script src="src/index.js"></script>
</body>
</html>

package.json 中添加命令

"scripts: {
    "start": "parcel index.html"
},

运行 yarn start,打开页面如下:

React原理:通俗易懂的 JSX 与 虚拟DOM

结语

以上内容如有错误,欢迎留言指出,一起进步