React16性能改善的原理(二)

前情提要

上一篇咱们提到若是 setState 以后,虚拟 dom diff 比较耗时,那么致使浏览器 FPS 下降,使得用户以为页面卡顿。那么 react 新的调度算法就是把本来一次 diff 的过程切分到各个帧去执行,使得浏览器在 diff 过程当中也能响应用户事件。接下来咱们具体分析下新的调度算法是怎么回事。javascript

原虚拟DOM问题

假设咱们有一个 react 应用以下:java

class App extends React.Component {
  render() {
    return (
      <div>
        <div>{this.props.name}</div>
        <ul>
          <li>{this.props.items[0]}</li>
          <li>{this.props.items[1]}</li>
        </ul>
      </div>
    );
  }
}

整个 app 的虚拟 dom 大体是这样的:react

var rootHost = {
  type: 'div',
  children: [ {
    type: 'div',
    children: [ {type: 'text'} ]
  }. {
    type: 'ul',
    children: [
      { type: 'li', children:[ {type: 'text'} ] },
      { type: 'li', children:[ {type: 'text'} ] }
    ]
  } ]
}

当更新发生 diff 两棵新老虚拟 dom 树的时候是递归的逐层比较(以下图)。这个过程是一次完成的,若是要按上一篇咱们说的把 diff 过程切割成好多时间片来执行,难度是如何记住状态且恢复现场。譬如说你 diff 到一半函数返回了,等下一个时间片继续 diff。若是只记住上次递归到哪一个节点,那么你只能顺着他的 children 继续 diff,而它的兄弟节点就丢失了。若是要完美恢复现场保存的结构估计得挺复杂。因此 react16 改造了虚拟dom的结构,引入了 fiber 的链表结构。算法

clipboard.png

如今解决方案 - fiber

fiber 节点至关于之前的虚拟 dom 节点,结构以下:segmentfault

const Fiber = {
  tag: HOST_COMPONENT,
  type: "div",
  return: parentFiber,
  child: childFiber,
  sibling: null,
  alternate: currentFiber,
  stateNode: document.createElement("div")| instance,
  props: { children: [], className: "foo"},
  partialState: null,
  effectTag: PLACEMENT,
  effects: []
};

先讲重要的几个属性: return 存储的是当前节点的父节点(元素),child 存储的是第一个子节点(元素),sibling 存储的是他右边第一个的兄弟节点(元素)。alternate 保存是当更新发生时候同一个节点带有新的 props 和 state 生成的新 fiber 节点。 那么虚拟 dom 的存储结构用链表的形式描述了整棵树。浏览器

clipboard.png

从顶层开始左序深度优先遍历以下图所示:数据结构

clipboard.png

咱们在遍历 dom 树 diff 的时候,即便中断了,咱们只须要记住中断时候的那么一个节点,就能够在下个时间片恢复继续遍历并 diff。这就是 fiber 数据结构选用链表的一大好处。我先用文字大体描述下 fiber diff 算法的过程再来看代码。从跟节点开始遍历,碰到一个节点和 alternate 比较并记录下须要更新的东西,并把这些更新提交到当前节点的父亲。当遍历完这颗树的时候,再经过 return 回溯到根节点。这个过程当中把全部的更新所有带到根节点,再一次更新到真实的 dom 中去。架构

clipboard.png

从根节点开始:app

  1. div1 经过 child 到 div2。
  2. div2 和本身的 alternate 比较完把更新 commit1 经过 return 提交到 div1。
  3. div2 经过 sibling 到 ul1。
  4. ul1 和本身的 alternate 比较完把更新 commit2 经过 return 提交到 div1。
  5. ul1 经过 child 到 li1。
  6. li1 和本身的 alternate 比较完把更新 commit3 经过 return 提交到 ul1。
  7. li1 经过 sibling 到 li2。
  8. li2 和本身的 alternate 比较完把更新 commit4 经过 return 提交到 ul1。
  9. 遍历完整棵树开始回溯,li2 经过 return 回到 ul1。
  10. 把 commit3 和 commit4 经过 return 提交到 div1。
  11. ul1 经过 return 回到 div1。
  12. 获取到全部更新 commit1-4,一次更新到真是的 dom 中去。

使用fiber算法更新的代码实现

React.Component.prototype.setState = function( partialState, callback ) {
  updateQueue.pus( {
    stateNode: this,
    partialState: partialState
  } );
  requestIdleCallback(performWork); // 这里就开始干活了
}

function performWork(deadline) {
  workLoop(deadline)
  if (nextUnitOfWork || updateQueue.length > 0) {
    requestIdleCallback(performWork) //继续干
  }
}

setState 先把这次更新放到更新队列 updateQueue 里面,而后调用调度器开始作更新任务。performWork 先调用 workLoop 对 fiber 树进行遍历比较,就是咱们上面提到的遍历过程。当这次时间片时间不够遍历完整个 fiber 树,或者遍历并比较完以后 workLoop 函数结束。接下来咱们判断下 fiber 树是否遍历完或者更新队列 updateQueue 是否还有待更新的任务。若是有则调用 requestIdleCallback 在下个时间片继续干活。nextUnitOfWork 是个全局变量,记录 workLoop 遍历 fiber 树中断在哪一个节点。dom

function workLoop(deadline) {
  if (!nextUnitOfWork) {
    //一个周期内只建立一次
    nextUnitOfWork = createWorkInProgress(updateQueue)
  }

  while (nextUnitOfWork && deadline.timeRemaining() > EXPIRATION_TIME) {
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork)
  }

  if (pendingCommit) {
    //当全局 pendingCommit 变量被负值
    commitAllwork(pendingCommit)
  }
}

刚开始遍历的时候判断全局变量 nextUnitOfWork 是否存在?若是存在表示上次任务中断了,咱们继续,若是不存在咱们就从更新队列里面取第一个任务,并生成对应的 fiber 根节点。接下来咱们就是正式的工做了,用循环从某个节点开始遍历 fiber 树。performUnitOfWork 根据咱们上面提到的遍历规则,在对当前节点处理完以后,返回下一个须要遍历的节点。循环除了要判断是否有下一个节点(是否遍历完),还要判断当前给你的时间是否用完,若是用完了则须要返回,让浏览器响应用户的交互事件,而后再在下个时间片继续。workLoop 最后一步判断全局变量 pendingCommit 是否存在,若是存在则把此次遍历 fiber 树产生的全部更新一次更新到真实的 dom 上去。注意 pendingCommit 在完成一次完整的遍历过程以前是不会有值的。

function createWorkInProgress(updateQueue) {
  const updateTask = updateQueue.shift()
  if (!updateTask) return

  if (updateTask.partialState) {
    // 证实这是一个setState操做
    updateTask.stateNode._internalfiber.partialState = updateTask.partialState
  }

  const rootFiber =
    updateTask.fromTag === tag.HostRoot
      ? updateTask.stateNode._rootContainerFiber
      : getRoot(updateTask.stateNode._internalfiber)

  return {
    tag: tag.HostRoot,
    stateNode: updateTask.stateNode,
    props: updateTask.props || rootFiber.props,
    alternate: rootFiber // 用于连接新旧的 VDOM
  }
}

function getRoot(fiber) {
  let _fiber = fiber
  while (_fiber.return) {
    _fiber = _fiber.return
  }
  return _fiber
}

createWorkInProgress 拿出更新队列 updateQueue 第一个任务,而后看触发这个任务的节点是什么类型。若是不是根节点,则经过循环迭代节点的 return 找到最上层的根节点。最后生成一个新的 fiber 节点,这个节点就是当前 fiber 节点的 alternate 指向的,也就是说下面会在当前节点和这个新生成的节点直接进行 diff。

function performUnitOfWork(workInProgress) {
  const nextChild = beginWork(workInProgress)
  if (nextChild) return nextChild

  // 没有 nextChild, 咱们看看这个节点有没有 sibling
  let current = workInProgress
  while (current) {
    //收集当前节点的effect,而后向上传递
    completeWork(current)
    if (current.sibling) return current.sibling
    //没有 sibling,回到这个节点的父亲,看看有没有sibling
    current = current.return
  }
}

performUnitOfWork 作的工做是 diff 当前节点,diff 完看看有没有子节点,若是没有子节点则把更新先提交到父节点。而后再看有没有兄弟节点,若是有则返回出去看成下次遍历的节点。若是仍是没有,说明整个 fiber 树已经遍历完了,则进入到回溯过程,把全部的更新都集中到根节点进行更新真实 dom。

function completeWork(currentFiber) {
  if (currentFiber.tag === tag.classComponent) {
    // 用于回溯最高点的 root
    currentFiber.stateNode._internalfiber = currentFiber
  }

  if (currentFiber.return) {
    const currentEffect = currentFiber.effects || [] //收集当前节点的 effect list
    const currentEffectTag = currentFiber.effectTag ? [currentFiber] : []
    const parentEffects = currentFiber.return.effects || []
    currentFiber.return.effects = parentEffects.concat(currentEffect, currentEffectTag)
  } else {
    // 到达最顶端了
    pendingCommit = currentFiber
  }
}

咱们看到 completeWork 中当判断到当前节点是根节点的时候才赋值 pendingCommit 整个全局变量。

function commitAllwork(topFiber) {
  topFiber.effects.forEach(f => {
    commitWork(f)
  })

  topFiber.stateNode._rootContainerFiber = topFiber
  topFiber.effects = []
  nextUnitOfWork = null
  pendingCommit = null
}

当回溯完,有了 pendingCommit,则 commitAllwork 会被调用。它作的工做就是循环遍历根节点的 effets 数据,里面保存着全部要更新的内容。commitWork 就是执行具体更新的函数,这里就不展开了(由于这篇主要想讲的是 fiber 更新的调度算法)。

因此大家看遍历 dom 数 diff 的过程是能够被打断而且在后续的时间片上接着干,只是最后一步 commitAllwork 是同步的不能打断的。这样 react 使用新的调度算法优化了更新过程当中执行时间过长致使的页面卡顿现象。

参考文献

  1. 为 Luy 实现 React Fiber 架构 - 更详细的代码实现能够看这片文章。
相关文章
相关标签/搜索