Virtual DOM(虚拟DOM):
使用js对象表示真实DOM对象,精简真实DOM对象中的原生DOM属性和方法,保留必要属性和方法。
虚拟DOM保存在内存中,方便随时调用。
对虚拟DOM使用diff算法得出真实DOM对象最小必要操作,减少真实DOM对象实际操作,节省性能开销。
// 示例
const virtualDom = {
key:'A1',
tag:'div',
children:[{
key:'B1',
tag:'div',
children:[{
key:'C1',
tag:'div',
children:[]
},{
key:'C2',
tag:'div',
children:[]
}]
},{
key:'B2',
tag:'div',
children:[]
}],
}
React中虚拟DOM可使用JS语法和JSX语法两种方式创建:
JS语法:
React.createElement("div", { id: "A1" },
React.createElement("div", { id: "B1" },
React.createElement("div", { id: "C1" }, "abc"),//C1子元素abc为文本节点
React.createElement("div", { id: "C2" })
),
React.createElement("div", { id: "B2" })
);
JSX语法:
<div id='A1'>
<div id='B1'>
<div id='C1'>abc</div>
<div id='C2'></div>
</div>
<div id='B2'></div>
</div>
JSX语法在浏览器中运行时需先使用Babel转换为JS语法。
React.createElement实现原理:
const TEXT_ELEMENT= Symbol('TEXT_ELEMENT'); // 文本节点类型
function createElement(type, props, ...children ){
// 删除冗余属性
delete props.__self;
delete props.__source;
// 返回虚拟DOM对象
return{
type,
// props 包含所有属性和children
props: {
...props,
children: children.map(child => {
if(typeof child === 'object'){ // 元素节点
return child
}
if(typeof child ==='string'){ // 文本节点
// 封装并返回文本节点类型的虚拟DOM
return {
type: TEXT_ELEMENT,
props:{
text: child,
children:[]
}
}
}
})
}
}
}
const React = { createElement }
ReactDOM.render实现原理:
function render(element, container){
// 创建真实DOM元素对象
const dom =
// 判断是否为文本节点类型虚拟DOM
element.type == TEXT_ELEMENT
? document.createTextNode("")
: document.createElement(element.type)
// 判断属性名称是否为 "children"
const isProperty = key => key !== "children"
Object.keys(element.props) // 遍历虚拟DOM props
.filter(isProperty) // 过滤children属性
.forEach(name => {
dom[name] = element.props[name] // 将所有属性添加到真实DOM元素
})
// 子元素递归执行render方法
element.props.children.forEach(child =>
render(child, dom)
)
// 将真实DOM挂载到页面上
container.appendChild(dom)
}
const ReactDOM = { render }
调用示例:
const app = (<div id='A1'>
<div id='B1'>
<div id='C1'>abc</div>
<div id='C2'></div>
</div>
<div id='B2'></div>
</div>)
ReactDOM.render(app, document.getElementById('app'));
注意:上面代码中递归调用子元素运行render的逻辑在dom树过于庞大时会造成进程阻塞页面卡顿,React在新的版本中通过fiber解决了该问题,实现原理如下:
let nextUnitOfWork = null
function workLoop(deadline){
let shouldYield = false; // 超时标记
// 当前帧有空余时间 且 仍有回调待执行
while(nextNode && !shouldYield){
nextUnitOfWork = performUnitOfWork(nextUnitOfWork) // 运行回调并返回下一节点
shouldYield = deadline.timeRemaining() < 1; // 更新超时标记
}
if(nextNode){ // 回调没有执行完
requestIdleCallback(workLoop,{timeout:167}) // 注册到下一次空闲回调中
}
}
function performUnitOfWork(node){
// 运行回调 ...
// 返回下一节点
return node.next;
}
requestIdleCallback(workLoop, { timeout: 167 });
注意:React已并不使用requestIdleCallback方法实现调度,而是通过MessageChannel实现调度包(scheduler package),概念上是类似的。