上面我们的performUnitOfWork并没有实现,但是从上面的结构可以看出来,他接收的参数是一个小任务,同时通过这个小任务还可以找到他的下一个小任务,Fiber构建的就是这样一个数据结构。Fiber之前的数据结构是一棵树,父节点的children指向了子节点,但是只有这一个指针是不能实现中断继续的。比如我现在有一个父节点A,A有三个子节点B,C,D,当我遍历到C的时候中断了,重新开始的时候,其实我是不知道C下面该执行哪个的,因为只知道C,并没有指针指向他的父节点,也没有指针指向他的兄弟。Fiber就是改造了这样一个结构,加上了指向父节点和兄弟节点的指针:
上面的图片还是来自于官方的演讲,可以看到和之前父节点指向所有子节点不同,这里有三个指针:
child: 父节点指向第一个子元素的指针。
sibling:从第一个子元素往后,指向下一个兄弟元素。
return:所有子元素都有的指向父元素的指针。
有了这几个指针后,我们可以在任意一个元素中断遍历并恢复,比如在上图List处中断了,恢复的时候可以通过child找到他的子元素,也可以通过return找到他的父元素,如果他还有兄弟节点也可以用sibling找到。Fiber这个结构外形看着还是棵树,但是没有了指向所有子元素的指针,父节点只指向第一个子节点,然后子节点有指向其他子节点的指针,这其实是个链表。
实现Fiber现在我们可以自己来实现一下Fiber了,我们需要将之前的vDom结构转换为Fiber的数据结构,同时需要能够通过其中任意一个节点返回下一个节点,其实就是遍历这个链表。遍历的时候从根节点出发,先找子元素,如果子元素存在,直接返回,如果没有子元素了就找兄弟元素,找完所有的兄弟元素后再返回父元素,然后再找这个父元素的兄弟元素。整个遍历过程其实是个深度优先遍历,从上到下,然后最后一行开始从左到右遍历。比如下图从div1开始遍历的话,遍历的顺序就应该是div1 -> div2 -> h1 -> a -> div2 -> p -> div1。可以看到这个序列中,当我们return父节点时,这些父节点会被第二次遍历,所以我们写代码时,return的父节点不会作为下一个任务返回,只有sibling和child才会作为下一个任务返回。
// performUnitOfWork用来执行任务,参数是我们的当前fiber任务,返回值是下一个任务 function performUnitOfWork(fiber) { // 根节点的dom就是container,如果没有这个属性,说明当前fiber不是根节点 if(!fiber.dom) { fiber.dom = createDom(fiber); // 创建一个DOM挂载上去 } // 如果有父节点,将当前节点挂载到父节点上 if(fiber.return) { fiber.return.dom.appendChild(fiber.dom); } // 将我们前面的vDom结构转换为fiber结构 const elements = fiber.children; let prevSibling = null; if(elements && elements.length) { for(let i = 0; i < elements.length; i++) { const element = elements[i]; const newFiber = { type: element.type, props: element.props, return: fiber, dom: null } // 父级的child指向第一个子元素 if(i === 0) { fiber.child = newFiber; } else { // 每个子元素拥有指向下一个子元素的指针 prevSibling.sibling = newFiber; } prevSibling = newFiber; } } // 这个函数的返回值是下一个任务,这其实是一个深度优先遍历 // 先找子元素,没有子元素了就找兄弟元素 // 兄弟元素也没有了就返回父元素 // 然后再找这个父元素的兄弟元素 // 最后到根节点结束 // 这个遍历的顺序其实就是从上到下,从左到右 if(fiber.child) { return fiber.child; } let nextFiber = fiber; while(nextFiber) { if(nextFiber.sibling) { return nextFiber.sibling; } nextFiber = nextFiber.return; } } 统一commit DOM操作上面我们的performUnitOfWork一边构建Fiber结构一边操作DOMappendChild,这样如果某次更新好几个节点,操作了第一个节点之后就中断了,那我们可能只看到第一个节点渲染到了页面,后续几个节点等浏览器空了才陆续渲染。为了避免这种情况,我们应该将DOM操作都搜集起来,最后统一执行,这就是commit。为了能够记录位置,我们还需要一个全局变量workInProgressRoot来记录根节点,然后在workLoop检测如果任务执行完了,就commit:
function workLoop(deadline) { while(nextUnitOfWork && deadline.timeRemaining() > 1) { // 这个while循环会在任务执行完或者时间到了的时候结束 nextUnitOfWork = performUnitOfWork(nextUnitOfWork); } // 任务做完后统一渲染 if(!nextUnitOfWork && workInProgressRoot) { commitRoot(); } // 如果任务还没完,但是时间到了,我们需要继续注册requestIdleCallback requestIdleCallback(workLoop); }