在 fiber 架构之前,react 是使用树形的 vdom 来描述应用的,树形结构可以很简单的描述一个 ui 界面,比如
{
type: 'div',
props: {
class: 'div'
},
children: [
{
type: 'p',
children: ['hello']
},
{
type: 'p',
children: ['world']
}
]
}
我们可以写一个渲染函数将 vdom 渲染出来
function renderVDOM(vdom, $container) {
// 文本节点
if (typeof vdom === 'string') {
const $dom = document.createTextNode(vdom)
$container.appendChild($dom)
return
}
const { type, props = {}, children = [] } = vdom
const $dom = document.createElement(type)
Object.keys(props).forEach(key => $dom.setAttribute(key, props[key]))
children.forEach(childVdom => renderVDOM(childVdom, $dom))
$container.appendChild($dom)
}
树形结构的渲染我们很容易就想到递归(stack reconciler),但是这种递归的渲染方式是不可中断的,一旦执行渲染函数就会占用 js 线程直到渲染结束,当页面的 ui 极其复杂,例如存在大量表单时这样的渲染方式很容易造成页面卡顿,因此 react 希望渲染是可以分片的,每次只执行一小段时间就让出 js 线程留给页面去做交互,然后再去接着执行渲染。那传统的树形结构是不是就不能实现可中断的渲染了呢,我们不妨试一下
function renderVDOM_async(rootVdom, $container) {
const INTERVAL = 5 // 每次只执行5ms
const DOMKEY = Symbol('DOMKEY') // 将真实dom暂存在vdom上面
let vdomStack = [[[rootVdom], 0]] // 执行栈
function pop() {
while (vdomStack.length) {
const [pendVdomList, curIdx] = vdomStack[vdomStack.length - 1]
if (curIdx === pendVdomList.length - 1) {
vdomStack.pop()
} else {
vdomStack[vdomStack.length - 1][1] = vdomStack[vdomStack.length - 1][1] + 1
break
}
}
}
function mount(vdom) {
(vdom.children || []).forEach(childVdom => {
if (typeof childVdom === 'string') {
vdom[DOMKEY].appendChild(document.createTextNode(childVdom))
} else {
vdom[DOMKEY].appendChild(childVdom[DOMKEY])
mount(childVdom)
}
})
}
function render() {
const start = performance.now()
// 只执行5ms
while (performance.now() - start < INTERVAL) {
// 所有的vdom已经遍历完毕生成了真实dom 统一挂载
// 挂载是不可中断的
if (!vdomStack.length) {
mount(rootVdom)
$container.appendChild(rootVdom[DOMKEY])
return
}
// 当前执行的是哪一层vdom 以及此层级的第几个
const [pendVdomList, curIdx] = vdomStack[vdomStack.length - 1]
const vdom = pendVdomList[curIdx]
// 文本节点
let $dom
if (typeof vdom === 'string') {
pop()
} else {
const { type, props = {}, children = [] } = vDom
$dom = document.createElement(type)
Object.keys(props).forEach(key => $dom.setAttribute(key, props[key]))
if (children.length) {
vdomStack.push([children, 0])
} else {
// 说明此层vdom已经遍历完毕 可以出栈了
if (curIdx === pendVdomList.length - 1) {
pop()
// 此层级没有执行完毕 移至下一位
} else {
vdomStack[vdomStack.length - 1][1] = curIdx + 1
}
}
}
vdom[DOMKEY] = $dom
}
// 让出主线程
setTimeout(() => {
render()
}, 0)
}
render()
}
上面尝试实现了一个可中断的树形结构渲染,做法是将递归的写法转为迭代。可以看到,由于树形的 vdom 结构没有子节点到父节点的指向,以及子节点到同级子节点的指向,要想实现中断必须手动的去维护一个执行栈,用进出栈来模拟父子节点指向,同时还需要维护同级节点的渲染位置变量,用来实现同级子节点渲染的移位。
总而言之是比较繁琐,由于数据结构的缺陷需要做许多额外的工作,react 最终选择使用链表是这个原因吗,如果你知道的话欢迎讨论。