我们讲React为了递归更新DOM树不阻塞主线程,而引入了并发模型,把整个递归任务拆分成若干个小工作单元执行,这节我们就来讲讲React是怎么拆分小任务的。
原理剖析
为了组织任务单元,我们需要学习一个新的数据结构——fiber树。每个组件对应一个fiber,而每个Fiber对应一个任务单元。
为了加深理解,我们来看一个简单的例子:
Didact.render( <div> <h1> <p /> <a /> </h1> <h2 /> </div>, container )
在render函数中,我们将创建根fiber并将其赋值给nexttunitofwork。剩下的任务在performUnitOfWork函数中执行。
实际上,我们将为每个fiber做下面三件事:
将元素添加到DOM中
为元素的子元素创建Fiber
返回下一个工作单元
其实,fiber这种数据结构的目标之一,就是方便查找下一个工作单元。所以每个fiber节点都同时包含它的第一个子节点(child)、下一个兄弟节点(sibling)和它的父节点(parent)的链路。
当一个fiber节点任务单元完成时,如果它有子节点,那子节点就是下一个执行单元。在我们的示例中,当div这个Fiber节点完成时,下一个执行单元就是H1节点。
如果一个fiber节点没有子节点,就会执行下一个兄弟节点。示例中的p节点执行完成,a是下一个执行单元。
同理,如果一个fiber节点没有子节点也没有下一个兄弟节点,则会指向父节点(原文为uncle节点,个人以为不妥),比如说上面示例中的a节点和h2节点分别指向父级h1节点和div节点。
综上,每个fiber节点都可以指向第一个子节点(child)、下一个兄弟节点(sibling)和它的父节点(parent),并且至少有其中一个指向:
代码实现
旧递归逻辑
在前面的章节中,我们讲了普通的递归来实现render方法,具体代码如下:
function render(element, container) { const dom = element.type == "TEXT_ELEMENT" ? document.createTextNode("") : document.createElement(element.type) const isProperty = key => key !== "children" Object.keys(element.props) .filter(isProperty) .forEach(name => { dom[name] = element.props[name] }) element.props.children.forEach(child =>render(child, dom)) container.appendChild(dom) }
fiber改造
接下来我们应用前面原理剖析的理论知识,来把DOM渲染的render方法改造成fiber渲染的方式:
1. 将 render 中创建 DOM 节点的部分抽离为 creactDOM 函数;
/** * createDom 创建 DOM 节点 * @param {fiber} fiber 节点 * @return {dom} dom 节点 */ function createDom (fiber) { // 如果是文本类型,创建空的文本节点,如果不是文本类型,按 type 类型创建节点 const dom = fiber.type === 'TEXT_ELEMENT' ? document.createTextNode("") : document.createElement(fiber.type) // isProperty 表示不是 children 的属性 const isProperty = key => key !== "children" // 遍历 props,为 dom 添加属性 Object.keys(fiber.props) .filter(isProperty) .forEach(name => { dom[name] = fiber.props[name] }) // 返回 dom return dom }
在createDom中,创建DOM节点,该函数接收一个fiber作为参数,处理不同类型的节点,生成新的DOM结构,并返回该DOM节点。
2. 在 render 中设置第一个工作单元为 fiber 根节点;
fiber 根节点仅包含 children 属性,值为长度为1的fiber数组。
// 下一个工作单元 let nextUnitOfWork = null /** * 将 fiber 添加至真实 DOM * @param {element} fiber * @param {container} 真实 DOM */ function render (element, container) { nextUnitOfWork = { dom: container, props: { children: [element] } } }
3. 通过 requestIdleCallback 在浏览器空闲时,渲染 fiber;
/** * workLoop 工作循环函数 * @param {deadline} 截止时间 */ function workLoop(deadline) { // 是否应该停止工作循环函数 let shouldYield = false // 如果存在下一个工作单元,且没有优先级更高的其他工作时,循环执行 while (nextUnitOfWork && !shouldYield) { nextUnitOfWork = performUnitOfWork( nextUnitOfWork ) // 如果截止时间快到了,停止工作循环函数 shouldYield = deadline.timeRemaining() < 1 } // 通知浏览器,空闲时间应该执行 workLoop requestIdleCallback(workLoop) } // 通知浏览器,空闲时间应该执行 workLoop requestIdleCallback(workLoop)
workLoop类似任务栈,不停地处理一个又一个工作单元,并且添加了是否中止的判断,同时还有继续下一个工作单元的逻辑。这样可以有效地避免主线程被阻塞,这也是React采用fiber数据结构的最重要的目的。
4. 渲染 fiber 的函数 performUnitOfWork;
performUnitOfWork主要实现前面提到的三个功能:
添加 dom 节点
如果 fiber 没有 dom 节点,为它创建一个 dom 节点;
如果 fiber 有父节点,将 fiber.dom 添加至父节点;
新建 filber
遍历子节点;
创建 fiber;
将第一个子节点设置为 fiber 的子节点;
第一个之外的子节点设置为该节点的兄弟节点;
返回下一个工作单元(fiber)
如果有子节点,返回子节点;
如果有兄弟节点,返回兄弟节点;
否则继续走 while 循环,直到找到 root;
/** * performUnitOfWork 处理工作单元 * @param {fiber} fiber * @return {nextUnitOfWork} 下一个工作单元 */ function performUnitOfWork(fiber) { //----------------------1. 添加 dom 节点---------------------- // 如果 fiber 没有 dom 节点,为它创建一个 dom 节点 if (!fiber.dom) { fiber.dom = createDom(fiber) } // 如果 fiber 有父节点,将 fiber.dom 添加至父节点 if (fiber.parent) { fiber.parent.dom.appendChild(fiber.dom) } //----------------------2 新建 filber---------------------- // 子节点 const elements = fiber.props.children // 索引 let index = 0 // 上一个兄弟节点 let prevSibling = null // 遍历子节点 while (index < elements.length) { const element = elements[index] // 创建 fiber const newFiber = { type: element.type, props: element.props, parent: fiber, dom: null, } // 将第一个子节点设置为 fiber 的子节点 if (index === 0) { fiber.child = newFiber } else if (element) { // 第一个之外的子节点设置为该节点的兄弟节点 prevSibling.sibling = newFiber } prevSibling = newFiber index++ } //----------------------3 返回下一个工作单元(fiber)---------------------- // 如果有子节点,返回子节点 if (fiber.child) { return fiber.child } let nextFiber = fiber while (nextFiber) { // 如果有兄弟节点,返回兄弟节点 if (nextFiber.sibling) { return nextFiber.sibling } // 否则继续走 while 循环,直到找到 root。 nextFiber = nextFiber.parent } }
在performUnitOfWork函数,我们接受一个fiber作为入参,然后处理当前工作单元fiber,并返回下一个工作单元fiber。
以上我们实现了将 fiber 渲染到页面的功能,并且渲染过程是可中断的。
写在后面
今天就到这里,fiber的内容还是挺多的,由于代码内容比较多,功能集中,所以多以注释的方式,解释了实现细节,可以慢慢消化。
不过重要的是懂得原理,然后再学习下具体的实现细节。
发表评论 取消回复