likes
comments
collection
share

学习React之Fiber和diff

作者站长头像
站长
· 阅读数 4

为什么会产生Fiber?

  • 在React16之前的版本,diff过程是不可中断的,意味着如果执行diff时间过长,会阻塞了渲染,造成卡顿,为了避免这种情况,让diff过程可以中断,并且不阻塞渲染。
  • 如何将diff可以中断呢?首先将diff过程拆分成一个个小单元,每完成一个后如果有其他事情要做则可以中断,恢复的时候接着执行下一个单元即可。为了组织这些小单元,产生了Fiber Tree。
  • 如何判断需要中断呢?借鉴requestIdleCallback方法,会传递一个timeRemaining方法,该方法可以实时拿到剩余时间。
  • react没有使用requestIdleCallback,原因是效率不够

看看Fiber的结构

  • 每个 Fiber 的数据都来自于元素树中的一个元素,元素与 Fiber 是一一对应的。与元素树不同的是,元素树每次渲染都会被重建,而 Fiber 会被复用,Fiber 的属性会被更新。
type Fiber = {
  // ---- Fiber类型 ----

  /** 工作类型,枚举值包括:函数组件、类组件、HTML元素、Fragment等 */
  tag: WorkTag,
  /** 就是那个子元素列表用的key属性 */
  key: null | string,
  /** 对应React元素ReactElmement.type属性 */
  elementType: any,
  /** 函数组件对应的函数或类组件对应的类 */
  type: any,

  // ---- Fiber Tree树形结构 ----

  /** 指向父FiberNode的指针 */
  return: Fiber | null,
  /** 指向子FiberNode的指针 */
  child: Fiber | null,
  /** 指向平级FiberNode的指针 */
  sibling: Fiber | null,
  
  // ---- Fiber数据 ----

  /** 经本次渲染更新的props值 */  
  pendingProps: any,
  /** 上一次渲染的props值 */
  memoizedProps: any,
  /** 上一次渲染的state值,或是本次更新中的state值 */
  memoizedState: any,
  /** 各种state更新、回调、副作用回调和DOM更新的队列 */
  updateQueue: mixed,
  /** 为类组件保存对实例对象的引用,或为HTML元素保存对真实DOM的引用 */
  stateNode: any,

  // ---- Effect副作用 ----

  /** 副作用种类的位域,可同时标记多种副作用,如Placement、Update、Callback等 */
  flags: Flags,
  /** 指向下一个具有副作用的Fiber的引用,在React 18中貌似已被弃用 */
  nextEffect: Fiber | null,

  // ---- 异步性/并发性 ----
  
  /** 当前Fiber与成对的进行中Fiber的双向引用 */
  alternate: Fiber | null,
  /** 标记Lane车道模型中车道的位域,表示调度的优先级 */
  lanes: Lanes
};
  • Fiber 与 Fiber 之间,并没有按照传统的 parent-children 方式建立树形结构。而是在父节点和它的第一个子节点间,利用child 和 return 属性建立了双向链表。节点与它的平级节点间,利用 sibling 属性建立了单向链表,同时平级节点的 return 属性,也都被设置成和单向链表起点的节点 return 一样的值引用。

学习React之Fiber和diff

创建虚拟DOM

function createElement(type, config, children) {
  let propName
  const props = {}
  let key = null
  let ref = null
  
  if (config) {
    if (config.key) {
      key = '' + config.key;
    }
    if (config.ref) {
      ref = config.ref
    }
    for (propName in config) {
      if (!RESERVED_PROPS.hasOwnProperty(propName)) {
        props[propName] = config[propName]
      }
    }
  }
  
   const childrenLength = arguments.length - 2;
  if (childrenLength === 1) {
    props.children = children;
  } else if (childrenLength > 1) {
    const childArray = Array(childrenLength);
    for (let i = 0; i < childrenLength; i++) {
      childArray[i] = arguments[i + 2];
    }
    props.children = childArray;
  }
  
  const element = ReactElement(
    type,
    key,
    ref,
    undefined,
    undefined,
    ReactSharedInternals.owner,
    props,
  );
  return element
}
  • 虚拟DOM其实是一个JS对象

优缺点

  • 有跨平台的兼容性,都可以使用同一套虚拟DOM来描述视图内容。
  • 减少了操作真实DOM的实际次数。
  • 渲染大量虚拟DOM的时候,由于多了一层虚拟DOM的计算,速度会比正常的要慢。

渲染

let workInProgress = null
let currentRootFiber = null
let nextUnitOfWork = null
let deletions = null // 记录需要删除的fiber

function render(element, container) { // 构建当前渲染的fiber树
  workInProgress = {
    dom: container,
    props: {
      child: [element]
    },
    alternate: currentRootFiber // 后续需要进行新旧对比
  }
  deletions = []
  nextUnitOfWork = workInProgress
}

ReactDOM.render(element, container)

可中断过程

function workLoop(deadline) {
  let shouldYield = false
  while (nextUnitOfWork && !shouldYield) {
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork)
    // timeRemaining方法返回实时剩余时间
    // didTimeout属性用于指示回调是否因为超过了设定的超时时间而被触发
    shouldYield = deadline.timeRemaining() < 1
  }
  requestIdleCallback(workLoop) // 留到下一个空闲时间执行
}
requestIdleCallback(workLoop)

function performUnitOfWork(fiber) {
  // 1. 给新fiber添加dom
  // 2. 创建新的fiber
  const isFunc = fiber.type instanceof Function
  if (isFunc) {
    updateFunctionComponent(fiber)
  } else {
    updateHostComponent(fiber)
  }  
     
  // 3. 返回下一个工作单元
  if (fiber.child) { // 先递归子节点
    return fiber.child
  }
  let nextFiber = fiber
  while (nextFiber) {
    if (nextFiber.sibling) { // 没有则寻找兄弟节点
      return nextFiber.sibling
    }
    nextFiber = nextFiber.return // 否则返回到父节点,父节点的child必然递归过了,就接着寻找其兄弟节点即可
  }
}

function updateFunctionComponent(fiber) {
  const children = [fiber.type(fiber.props)]
  reconcileChildren(fiber, children)
}

function updateHostComponent(fiber) {
  if (!fiber.dom) {
    fiber.dom = createDom(fiber)
  }
  // 此时还不能向fiber.return.dom添加当前dom,由于是可中断过程,如果添加了容易导致ui新旧dom同时存在
  reconcileChildren(fiber, fiber.props.children)
}

协调阶段

function reconcileChildren(fiber, elements) {
  let index = 0
  let oldFiber = fiber.alternate && fiber.alternate.child
  let prevSibling = null
  while (index < elements.length || oldFiber !== null) {
    const element = elements[index]
    let newFiber = null
    // 对比oldFiber和新DOM
    const sameType = oldFiber && element && oldFiber.type === element.type
    if (sameType) {
      // update
      newFiber = {
        type: oldFiber.type,
        props: element.props,
        dom: oldFiber.dom,
        return: fiber,
        alternate: oldFiber,
        flags: "Update"
      }
    }
    if (element && !sameType) {
      // 新增当前元素
      newFiber = {
        type: oldFiber.type,
        props: element.props,
        dom: null,
        return: fiber,
        alternate: null,
        flags: "Placement"
      }
    }
    if (oldFiber && !sameType) {
      // 删除当前元素
      oldFiber.flags = "Deletion"
      deletions.push(oldFiber)
    }
    
    if (index === 0) {
      fiber.child = newFiber
    } else {
      prevSibling.sibling = newFiber
    }
    prevSibling = newFiber
    ++index
  }
}

初始化更新队列

function initializeUpdateQueue(fiber) { // 初始化更新队列
  const updateQueue = {
    shared: {
      pending: null
    }
  }
  
  fiber.updateQueue = updateQueue
}

function createUpdate() {
  return {}
}

function enqueueUpdate(fiber, update) { // 向当前的fiber更新队列添加更新
  const updateQueue = fiber.updateQueue
  const sharedQueue = updateQueue.shared
  const pending = sharedQueue.pending
  
  // pending 永远指向最新的更新
  if (!pending) {
    update.next = update
  } else {
    update.next = pending.next
    pending.next = update
  }
  sharedQueue.pending = update
}

学习React之Fiber和diff

学习React之Fiber和diff

收集副作用链

  • 为了避免fiber树寻找有副作用的fiber节点
  • fiber树的构建是深度有限的,故EffectList中总是子级Fiber在前面
let rootFiber = {}

function collectEffectList(returnFiber, completedWork) {
  if (returnFiber) {
      // 把自己的effectList传递给父fiber
      if (!returnFiber.firstEffect) { // 如果父fiber没有链表,则指向子fiber的firstEffect
        returnFiber.firstEffect = completedWork.firstEffect
      }
      if (completedWork.lastEffect) {
        if (returnFiber.lastEffect) {
          returnFiber.lastEffect.nextEffect = completedWork.firstEffect
        }
        returnFiber.lastEffect = completedWork.lastEffect
      }
  }
  // 将自己传递给父fiber的effectList
  const flags = completedWork.flags
  if (flags) { // 只要flags不为noFlags(0)就说明有副作用
    if (returnFiber.lastEffect) {
      returnFiber.lastEffect.nextEffect = completedWork
    } else {
      returnFiber.firstEffect = completedWork
    }
    returnFiber.lastEffect = completedWork
  }
}

学习React之Fiber和diff

学习React之Fiber和diff

提交阶段

function commitRoot() {
  deletions.forEach(commitWork) // 确保没有旧节点中需要删除的
  commitWork(workInProgress.child)
  currentRootFiber = workInProgress
  workInProgress = null
}

function commitWork(fiber) {
  if (!fiber) return
  
  let domParentFiber = fiber.parent
  while (!domParentFiber.dom) {
    domParentFiber = domParentFiber.return
  }
  
  const domParent = fiber.parent.dom
  if (fiber.flags === 'Placement' && fiber.dom !== null) {
    domParent.appendChild(fiber.dom)
  } else if (fiber.flags === 'Deletion') {
    commitDeletion(fiber, domParent)
  } else if (fiber.flags === 'Update' && fiber.dom !== null) {
    updateDom(fiber.dom, fiber.alternate.props, fiber.props)
  }
  commitWork(fiber.child)
  commitWork(fiber.sibling)
}

const isProperty = key => key !== "children"
const isNew = (prev, next) => key =>
  prev[key] !== next[key]
const isGone = (prev, next) => key => !(key in next)
function updateDom(dom, prevProps, nextProps) {
  // 移除新属性不存在而旧属性存在的属性
  Object.keys(prevProps)
    .filter(isProperty)
    .filter(isGone(prevProps, nextProps))
    .forEach(name => {
      dom[name] = ""
    })

  // 新增旧属性不存在的属性或修改属性新旧属性不一致的属性
  Object.keys(nextProps)
    .filter(isProperty)
    .filter(isNew(prevProps, nextProps))
    .forEach(name => {
      dom[name] = nextProps[name]
    })
}

function commitDeletion(fiber, domParent) {
  if (fiber.dom) {
    domParent.removeChild(fiber.dom)
  } else {
    commitDeletion(fiber.child, domParent)
  }
}

总结执行过程

  • 当组件内更新 state 或有 context 更新时,React 会进入渲染阶段(Render Phase)。这一阶段是异步的,Fiber 协调引擎会启动workLoop ,从 Fiber 树的根部开始遍历,快速跳过已处理的节点;对有变化的节点,引擎会为 Current(当前)节点克隆一个 WorkInProgress(进行中)节点,将这两个 Fiber 的 alternate 属性分别指向对方,并把更新都记录在WorkInProgress 节点上。
    • 该workLoop可以随时跑、随时停。
  • 当 Fiber 树所有节点都完成工作后,WorkInProgress 节点会被改称为 FinishedWork(已完成)节点,WorkInProgress 树也会被改称为 FinishedWork树。这时 React 会进入提交阶段(Commit Phase),这一阶段主要是同步执行的。Fiber 协调引擎会把FinishedWork 节点上记录的所有修改,按一定顺序提交并体现在页面上。

提交阶段

  • 分为三个同步执行子阶段:
  1. 变更前子阶段:调用类组件的getSnapshotBeforeUpdate方法

  2. 变更子阶段:更新真实DOM

    • 递归提交与删除相关的副作用(移除ref、真实DOM、执行类组件componentWillUnmount
    • 递归提交添加、重新排序真实DOM副作用
    • 依次执行FiberNode中的useLayoutEffect的清除函数
    • 引擎用FinishedWork树替换Current树
  3. 布局子阶段:这个阶段真实DOM树已经完成了变更,会调用useLayoutEffect的副作用回调函数和类组件的componentDidMount方法

  • 在上述提交阶段的三个同步子阶段之后,下一次渲染阶段之前,引擎会异步或同步调用flushPassiveEffects方法,这个函数会先后两轮按深度优先遍历 Fiber 树上每个节点:
  1. 第一轮:如果节点的 updateQueue 链表中有待执行的、由 useEffect 定义的副作用,则顺序执行它们的清除函数;
  2. 第二轮:如果节点的 updateQueue 链表中有待执行的、由 useEffect 定义的副作用,则顺序执行它们的副作用回调函数,并保存清除函数,供下一轮提交阶段执行。

来源

转载自:https://juejin.cn/post/7361261846089302066
评论
请登录