实现React同步渲染
On this page
大家好,我是万维读客的讲师曹欢欢。上一节我们已经可以解析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树了。