实现React同步渲染

大家好,我是万维读客的讲师曹欢欢。上一节我们已经可以解析JSX了,本节我们基于JSX解析后的数据结构实现虚拟DOM的创建和操作。

React的渲染方式

参考React网站,React渲染元素代码如下:

const root = ReactDOM.createRoot(
  document.getElementById('root')
);
const element = <h1>Hello, world</h1>;
root.render(element);

这里面主要是createRoot方法创建虚拟DOM根节点,然后render方法渲染虚拟DOM。下面我们来实现我们自己的createRoot方法和render方法。

构建虚拟DOM根节点

修改jsx.test.jsx,增加react渲染虚拟DOM能力的测试代码,修改后如下

it('build virtual DOM', ()=>{
      const ele = (
      <div className="container">
          <span>hello</span>
          <a>w3cdoc.com</a>
      </div>);
      const container = document.createElement('div');
      const root = Treact.createRoot(container);
      root.render(ele);
      console.log(root.container.innerHTML);// 查看渲染结果
  })

这里我们分析下createRoot方法和render方法:

  • createRoot方法 入参是一个DOM元素,作为根节点
  • render方法 入参是jsx编译后的数据结构,例如: {type:'', props:{ xx:yy, children:[] }}

修改Treact.jsx代码,增加createRoot函数和render函数如下:

class TreactRoot{
    constructor(container){
        this.container = container;
    }
    render(element){
        const node = document.createElement(element.type);
        this.container.appendChild(node);
    }
}

export function createRoot(container){
    return new TreactRoot(container);
}

查看控制台测试用例结果:

stdout | treact02/jsx.test.jsx > jsx Test > build virtual DOM
<div></div>

 ✓ treact02/jsx.test.jsx (2)
   ✓ jsx Test (2)
     ✓ build jsx
     ✓ build virtual DOM

 Test Files  1 passed (1)
      Tests  2 passed (2)
   Start at  21:19:26
   Duration  18ms

输出了<div></div>, 说明render函数已经正常创建了DOM根节点。接着需要优化render函数,创建出虚拟DOM树。

构建虚拟DOM树

上面我们只渲染了根节点元素,首先我们增加对元素属性的渲染,代码如下:

element.pros.forEach(key => {
    node[key] = element.pros[key];
})

然后我们需要处理children元素的渲染,这里我们需要有个递归的处理,渲染每个子元素节点和渲染根节点是一样的。

修改后代码如下:

class TreactRoot {
    constructor(container) {
        this.container = container;
    }
    render(element) {
        this.renderElement(element, this.container);
    }
    renderElement(ele, parent) {
        const node = document.createElement(ele.type);
        Object.keys(ele.props).filter(key => key != 'children').forEach(key => {
            node[key] = ele.props[key];
        })
        ele.props.children.forEach(child => {
            this.renderElement(child, node);
        })
        parent.appendChild(node);
    }
}

但是我们看控制台会有报错:

stdout | treact02/jsx.test.jsx > jsx Test > build jsx
{"type":"div","props":{"className":"container","children":[{"type":"span","props":{"children":["hello"]}},{"type":"a","props":{"children":["w3cdoc.com"]}}]}}

 ❯ treact02/jsx.test.jsx (2)
   ❯ jsx Test (2)
     ✓ build jsx
     × build virtual DOM

⎯⎯⎯⎯⎯ Failed Tests 1 

 FAIL  treact02/jsx.test.jsx > jsx Test > build virtual DOM
TypeError: Cannot convert undefined or null to object
 ❯ TreactRoot.renderElement treact02/Treact.jsx:20:16
     18|     renderElement(ele, parent) {
     19|         const node = document.createElement(ele.type);
     20|         Object.keys(ele.props).filter(key => key != 'children').forEach(key => {
       |                ^
     21|             node[key] = ele.props[key];
     22|         })
 ❯ treact02/Treact.jsx:24:18

正好对比第一个测试用例的输出结构,我们可以看到最后一级叶子节点数据是"children":["hello"],这里是没有type和props属性的,这里我们需要对这种DOM节点做特殊处理。

为了处理方便,我们可以在jsx的转换函数createElement中统一来处理输出结构,保证虚拟DOM数据结构一致性。

修改后Treact.jsx代码如下:

export function createElement(type, props, ...children) {
    return {
        type,
        props: {
            ...props,
            children: children.map(child=>{
                if(typeof child != 'object'){
                    return {
                        type: 'HostText',
                        props:{
                            nodeValue: child,
                            children:[]
                        }
                    }
                }else{
                    return child;
                }
            })
        }
    }
}

class TreactRoot {
    constructor(container) {
        this.container = container;
    }
    render(element) {
        this.renderElement(element, this.container);
    }
    renderElement(ele, parent) {
        const node = document.createElement(ele.type);
        Object.keys(ele.props).filter(key => key != 'children').forEach(key => {
            node[key] = ele.props[key];
        })
        ele.props.children.forEach(child => {
            this.renderElement(child, node);
        })
        parent.appendChild(node);
    }
}

export function createRoot(container) {
    return new TreactRoot(container);
}

查看测试用例的输出:

stdout | treact02/jsx.test.jsx > jsx Test > build virtual DOM
<div class="container"><span><hosttext></hosttext></span><a><hosttext></hosttext></a></div>

 ✓ treact02/jsx.test.jsx (2)
   ✓ jsx Test (2)
     ✓ build jsx
     ✓ build virtual DOM

 Test Files  1 passed (1)
      Tests  2 passed (2)
   Start at  21:45:49
   Duration  21ms

发现上面的文本节点处理的不对,输出成了hosttext,这里需要再render函数中加下特殊处理,修改代码:

renderElement(ele, parent) {
    const node = ele.type === 'HostText'?document.createTextNode(''):document.createElement(ele.type);
    Object.keys(ele.props).filter(key => key != 'children').forEach(key => {
        node[key] = ele.props[key];
    })
    ele.props.children.forEach(child => {
        this.renderElement(child, node);
    })
    parent.appendChild(node);
}

修改后发现输出结果已经正常的,我们完善下测试用例:

it('build virtual DOM', ()=>{
    const ele = (
    <div className="container">
        <span>hello</span>
        <a>w3cdoc.com</a>
    </div>);
    const container = document.createElement('div');
    const root = Treact.createRoot(container);
    root.render(ele);
    expect(root.container.innerHTML).toBe('<div class="container"><span>hello</span><a>w3cdoc.com</a></div>');
})

查看控制台测试用例全部执行成功,这里我们已经可以构建虚拟DOM树了。

参考学习

  1. React渲染元素: https://zh-hans.legacy.reactjs.org/docs/rendering-elements.html


请遵守《互联网环境法规》文明发言,欢迎讨论问题
扫码反馈

扫一扫,反馈当前页面

咨询反馈
扫码关注
返回顶部