React: JSX生成真实DOM结点

在上一篇文章中,我们介绍了 Babel 是如何将 JSX 代码编译成可执行代码的,随后也实现了一个自己的解析器,模拟了 Babel 编译的过程。

现在我们再来回顾一下,假定有如下业务代码:

const style = {
  color: 'red',
  fontSize: '20px',
};

const greet = function (name) {
  return `hello ${name}`;  
};

const App = (
  <div className="container">
    <p style={style}>saying {greet('scott')} hah</p>
    <div>
      <p>this is jsx-like code</p>
      <i className="icon"/>
      <p>parsing it now</p>
      <img className="icon"/>
    </div>
    <input type="button" value="i am a button"/>
    <em/>
  </div>
);

console.log(App);

ReactDOM.render(App, document.getElementById('root'));

经过编译之后,会生成下面的可执行代码:

var style = {
  color: 'red',
  fontSize: '20px'
};

var greet = function greet(name) {
  return 'hello ' + name;
};

var App = React.createElement(
  'div',
  { className: 'container' },
  React.createElement(
    'p',
    { style: style },
    'saying ',
    greet('scott'),
    ' hah'
  ),
  React.createElement(
    'div',
    null,
    React.createElement(
      'p',
      null,
      'this is jsx-like code'
    ),
    React.createElement('i', { className: 'icon' }),
    React.createElement(
      'p',
      null,
      'parsing it now'
    ),
    React.createElement('img', { className: 'icon' })
  ),
  React.createElement('input', { type: 'button', value: 'i am a button' }),
  React.createElement('em', null)
);

console.log(App);

ReactDOM.render(App, document.getElementById('root'));

引入所需的React库:

<!DOCTYPE html>
<html>
  <body>
    <div id="root"></div>
    <script crossorigin src="https://unpkg.com/react@16/umd/react.production.min.js"></script>
    <script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>
    <script src="index.js"></script>
  </body>
</html>

运行以上代码,我们会发现控制台打印信息如下图所示:

从图中可以看出,type 就是标签名,其他字段比较常用的有 key、ref 以及 props,其中 props 中会包含 className、style 和 children 等字段。这些信息最终会映射成真实的 DOM 结点,所以这就是我们熟知的 Virtual DOM,而 ReactDOM.render() 函数就是将虚拟 DOM 转换成真实 DOM 的工具。

我们现在可以得出一个结论,React.createElement() 负责根据代码生成虚拟 DOM,ReactDOM.render() 负责将虚拟 DOM 映射到真实 DOM 上。

究竟 React.createElement() 和 ReactDOM.render() 是如何将程序转换成真实 DOM 的呢?接下来,我们就来试着实现 React.createElement() 和 ReactDOM.render() 的逻辑,模拟一下这个过程。

先来实现 React.createElement() 方法:

const React = {
  // 创建DOM描述对象 即虚拟DOM
  createElement(tag, attrs, ...children) {
    let vnode = {
      type: tag,
      props: {
        ...attrs,
        children,
      }
    };

    return vnode;
  }
};

以上代码会生成下面的虚拟 DOM 结构:

然后是 ReactDOM.render() 方法:

const ReactDOM = {
  // 渲染真实DOM
  render(vnode, container) {
    let realDOM = this.generateDOM(vnode);
    container.appendChild(realDOM);
  },
  // 获取真实DOM
  generateDOM(vnode) {
    let elem = document.createElement(vnode.type);
    // 特殊key值映射
    let specialKeyMap = {
      className: 'class',
      fontSize: 'font-size',
    };
    let {props} = vnode;

    // 设置DOM属性
    props && Object.keys(props).forEach(key => {
      if (key === 'children') {
        // 处理子结点
        props.children.forEach(child => {
          if (typeof child === 'string') {
            // 纯内容结点
            elem.appendChild(document.createTextNode(child));
          } else {
            // DOM结点
            elem.appendChild(this.generateDOM(child));
          }
        });
      } else if (key === 'style') {
        // 设置样式属性
        let styleObj = props.style;
        let styleItems = [];

        Object.keys(styleObj).forEach(styleKey => {
          styleItems.push(`${specialKeyMap[styleKey] || styleKey}:${styleObj[styleKey]}`);
        });

        elem.setAttribute('style', styleItems.join(';'));
      } else {
        // 设置其他属性
        elem.setAttribute(specialKeyMap[key] || key, props[key]);
      }
    });

    return elem;
  }
};

最后我们把前面引用的React库替换成上面我们自己实现的代码,然后运行,见证奇迹的时刻到了:

只需两段简短的代码,我们就生成了一个迷你版的虚拟 DOM,并最终生成了真实的 DOM 结构,是不是很简单?当然,React 所实现的功能远不止这些,我们后续会继续介绍。

posted @ 2018-07-15 23:16 liuhe688 阅读(...) 评论(...) 编辑 收藏