React18 源码解析之 reconcileChildren 生成 fiber 的过程
侧边栏壁纸
  • 累计撰写 1,061 篇文章
  • 累计收到 0 条评论

React18 源码解析之 reconcileChildren 生成 fiber 的过程

加速器之家
2024-10-20 / 0 评论 / 3 阅读 / 正在检测是否收录...

我们解析的源码是 React18.1.0 版本,请注意版本号。React 源码学习的 GitHub 仓库地址:https://github.com/wenzi0github/react

React 中维护着两棵 fiber 树,一棵是正在展示的 UI 对应的那棵树,我们称为 current 树;另一棵通过更新,将要构建的树。那么在更新的过程中,是通过怎么样的对比过程,来决定是复用之前的节点,还是创建新的节点。

1. reconcileChildren #

函数 reconcileChildren() 是一个入口函数,这里会根据 current 的 fiber 节点的状态,分化为 mountChildFibers() 和 reconcileChildFibers()。

/**
* 调和,创建或更新fiber树
* 若current的fiber节点为null,调用 mountChildFibers 初始化
* 若current不为空,说明要得到一棵新的fiber树,执行 reconcileChildFibers() 方法
* @param current 当前树中的fiber节点,可能为空
* @param workInProgress 将要构建树的fiber节点
* @param nextChildren 将要构建为fiber节点的element结构
* @param renderLanes 当前的渲染优先级
*/
export function reconcileChildren(current: Fiber | null, workInProgress: Fiber, nextChildren: any, renderLanes: Lanes) { if (current === null) { /**
* mount阶段,这是一个还未渲染的全新组件,我们不用通过对比最小副作用来更新它的子节点。
* 直接转换nextChildren即可,不用标记哪些节点需要删除等等
*/
workInProgress.child = mountChildFibers(workInProgress, null, nextChildren, renderLanes); } else { /**
* 若current不为null,则需要进行的工作:
* 1. 判断之前的fiber节点是否可以复用;
* 2. 若不能复用,则需要标记删除等;
*/
workInProgress.child = reconcileChildFibers( workInProgress, /**
* 因为我们要构建的是workInProgress的子节点,这里也传入current的子节点,
* 方便后续的对比和复用
*/
current.child, nextChildren, renderLanes, ); } }

再看下这两个函数的区别:

export const reconcileChildFibers = ChildReconciler(true); // 需要收集副作用
export const mountChildFibers = ChildReconciler(false); // 不用追踪副作用

这两个函数都是 ChildReconciler() 生成,只是参数不一样。可见这两个函数就区别在是否要追踪 fiber 节点的副作用。

2. ChildReconciler #

ChildReconciler(shouldTrackSideEffects) 只有一个参数,并返回的是一个函数。

/**
* 子元素协调器,即把当前fiber节点中的element结构转为fiber节点
* @param {boolean} shouldTrackSideEffects 是否要追踪副作用,即我们本来打算复用之前的fiber节点,但又复用不了,需要给该fiber节点打上标记,后续操作该节点
* @returns {function(Fiber, (Fiber|null), *, Lanes): *} 返回可以将element转为fiber的函数
*/
function ChildReconciler(shouldTrackSideEffects) { // 暂时省略其他代码 function reconcileChildFibers(returnFiber: Fiber, // 当前 Fiber 节点,即 workInProgress
currentFirstChild: Fiber | null, // current 树上对应的当前 Fiber 节点的第一个子 Fiber 节点,mount 时为 null,主要是为了是否能复用之前的节点
newChild: any, // returnFiber中的element结构,用来构建returnFiber的子节点
lanes: Lanes, // 优先级相关
): Fiber | null { // 省略 return deleteRemainingChildren(returnFiber, currentFirstChild); } return reconcileChildFibers; }

当 current 对应的 fiber 节点为 null 时,那它就没有子节点,也无所谓复用和删除的说法,直接按照 workInProgress 里的 element 构建新的 fiber 节点即可,这时,是不用收集副作用的。

若 current 对应的 fiber 节点不为 null 时,那么就把 current 的子节点拿过来,看看是否有能复用的节点,有能复用的节点就直接复用;不能复用的,比如类型发生了改变的(div 标签变成了 p 标签),新结构里已经没有该 fiber 节点了等等,都是要打上标记,后续在 commit 阶段进行处理。

3. reconcileChildFibers #

函数 reconcileChildFibers() 不做实际的操作,仅是根据 element 的类型,调用不同的方法来处理,相当于一个路由分发。

/**
* 将returnFiber节点(即当前的workInProgress对应的节点)里的element结构转为fiber结构
* @param returnFiber 当前的workInProgress对应的fiber节点
* @param currentFirstChild current 树上对应的当前 Fiber 节点的第一个子 Fiber 节点,可能为null
* @param newChild returnFiber中的element结构,用来构建returnFiber的子节点
* @param lanes
* @returns {Fiber|*}
*/
function reconcileChildFibers(returnFiber: Fiber, // 当前 Fiber 节点,即 workInProgress
currentFirstChild: Fiber | null,
newChild: any,
lanes: Lanes, // 优先级相关
): Fiber | null { // 是否是顶层的没有key的fragment组件 const isUnkeyedTopLevelFragment = typeof newChild === 'object' && newChild !== null && newChild.type === REACT_FRAGMENT_TYPE && newChild.key === null; // 若是顶层的fragment组件,则直接使用其children if (isUnkeyedTopLevelFragment) { newChild = newChild.props.children; } // Handle object types // 判断该节点的类型 if (typeof newChild === 'object' && newChild !== null) { /**
* newChild是Object,再具体判断 newChild 的具体类型。
* 1. 是普通React的函数组件、类组件、html标签等
* 2. portal类型;
* 3. lazy类型;
* 4. newChild 是一个数组,即 workInProgress 节点下有并排多个结构,这时 newChild 就是一个数组
* 5. 其他迭代类型,我暂时也不确定这哪种?
*/
switch (newChild.$$typeof) { case REACT_ELEMENT_TYPE: // 一般的React组件,如

return placeSingleChild( // 调度单体element结构的元素 reconcileSingleElement(returnFiber, currentFirstChild, newChild, lanes), ); case REACT_PORTAL_TYPE: return placeSingleChild(reconcileSinglePortal(returnFiber, currentFirstChild, newChild, lanes)); case REACT_LAZY_TYPE: const payload = newChild._payload; const init = newChild._init; // TODO: This function is supposed to be non-recursive. return reconcileChildFibers(returnFiber, currentFirstChild, init(payload), lanes); } if (isArray(newChild)) { // 若 newChild 是个数组 return reconcileChildrenArray(returnFiber, currentFirstChild, newChild, lanes); } if (getIteratorFn(newChild)) { return reconcileChildrenIterator(returnFiber, currentFirstChild, newChild, lanes); } throwOnInvalidObjectType(returnFiber, newChild); } if ((typeof newChild === 'string' && newChild !== '') || typeof newChild === 'number') { // 文本节点 return placeSingleChild(reconcileSingleTextNode(returnFiber, currentFirstChild, '' + newChild, lanes)); } // Remaining cases are all treated as empty. // 若没有匹配到任何类型,说明当前newChild无法转为fiber节点,如boolean类型,null等是无法转为fiber节点的 // deleteRemainingChildren()的作用是删除 returnFiber 节点下,第2个参数传入的fiber节点,及后续所有的兄弟节点 // 如 a->b->c->d-e,假如我们第2个参数传入的是c,则删除c及后续的d、e等兄弟节点, // 而这里,第2个参数传入的是 currentFirstChild,则意味着删除returnFiber节点下所有的子节点 // 为什么要删除呢?这是因为,为了保证前后两棵树是一致的,若jsx在workInProgress所在树中,无法转为fiber节点, // 说明 returnFiber 下所有的fiber节点均无法复用 return deleteRemainingChildren(returnFiber, currentFirstChild); }

我们先来看下源码 reconcileChildFibers() 中都判断了 newChild 的哪些类型:

  1. 是否是顶层的 fragment 元素,如在执行 render()时,用的是 fragment 标签(<> 或 )包裹,则表示该元素顶级的 fragment 组件,这里直接使用其 children;
  2. 合法的 ReactElement,如通过 createElement、creatPortal 等创建创建的元素,只是 $$typeof 不一样;这里也把 lazy type 归类到了这里;
  3. 普通数组,每一项都是合法的其他元素,如一个 div 标签下有并列的 span 标签、函数组件、纯文本等,这些在 jsx 转换过程中,会形成数组;
  4. Iterator,跟数组类似,只是遍历方式不同;
  5. string 或 number 类型:如(
    abc
    )里的 abc 即为字符串类型的文本(外层的div节点依然是html标签,在React中会把div标签和abc分成两部分进行处理);

函数 reconcileChildFibers() 只处理 workInProgress 节点里的 element 结构,无论 element 是一个节点,还是一组节点,只会把这一层的节点都进行转换。若 element 中对应的只有一个 fiber 节点,那就返回这个节点,若是一组数据,则会形成一个 fiber 单向链表,然后返回这个链表的头节点。

源码的注释里也明确说了,reconcileChildFibers()不是递归函数,他只处理当前层级的数据。如果还有印象的话,我们在之前讲解的函数performUnitOfWork(),他本身就是一个连续递归的操作。整个流程的控制权在那里。

function performUnitOfWork(unitOfWork: Fiber): void {
  const current = unitOfWork.alternate;

  let next;
  // 若 next 是fiber节点,则 workInProgress 指向到该新的fiber节点,继续处理其内部的jsx
  // 若 next 为 null,说明当前所有的结构均已处理完毕,completeUnitOfWork()判断是去处理兄弟节点,还是返回到上级节点
  next = beginWork(current, unitOfWork, subtreeRenderLanes);

  unitOfWork.memoizedProps = unitOfWork.pendingProps;
  if (next === null) {
    // unitOfWork已经是最内层的节点了,没有子节点了
    // If this doesn't spawn new work, complete the current work.
    completeUnitOfWork(unitOfWork);
  } else {
    workInProgress = next;
  }
}

这里我们主要讲解一般的 React 类型 REACT_ELEMENT_TYPE,数组类型和普通文本类型的 element 的构建。

reconcileChildFibers()函数的整体思想就是复用复用复用,能复用之前 fiber 节点的,绝不创建新的 fiber 节点。只不过,之前的 fiber 节点是否可以复用,复用哪个 fiber 节点,情况比较复杂,接下来我们一一讲解下。

4. 单体 element 结构的元素 reconcileSingleElement #

若 element 中只对应一个元素,且是普通 React 的函数组件、类组件、html 标签等类型,那我们调用 reconcileSingleElement() 来处理。

  1. 判断是否可以复用之前的节点,复用节点的标准是 key 一样、类型一样,任意一个不一样,都无法复用;
  2. 新要构建的节点是只有一个节点,但之前不一定只有一个节点,比如之前是多个 li 标签,新 element 中只有一个 li 标签;

若无法复用之前的节点,那之前的节点也没留着的必要了,则将之前的节点删除,创建一个新的节点。

这里我们说的复用节点,指的是复用current.alternate的那个节点,因为在没有任何更新时,两棵 fiber 树是一一对应的。在产生更新后,可能就会存在对应不上的情况,因此才有了下面的各种 diff 对比环节。

4.1 对比判断是否有可复用的节点 #

在对比过程中,采用了循环的方式,我们知道同一 fiber 节点下,所有同一级别的子 fiber 节点是横向单向链表,串联起来的。而且,虽然新节点是单个节点,但却无法保证之前的节点也是单个节点,因此这里从第 1 个子 fiber 节点开始,查找第一个 key 和节点类型都一样的节点。

若找到,进行复用;若找不到,则删除之前所有的子节点,创建新的 fiber 节点。

/**
* 单个普通ReactElement的构建
* @param returnFiber
* @param currentFirstChild
* @param element
* @param lanes
* @returns {Fiber}
*/
function reconcileSingleElement(returnFiber: Fiber,
currentFirstChild: Fiber | null,
element: ReactElement,
lanes: Lanes,
): Fiber { // element是workInProgress中的,表示正在构建中的 const key = element.key; // child: 当前正在对比的child,初始时是第1个子节点 let child = currentFirstChild; // 新节点是单个节点,但无法保证之前的节点也是单个节点, // 这里用循环查找第一个 key和节点类型都一样的节点,进行复用 while (child !== null) { // TODO: If key === null and child.key === null, then this only applies to // the first item in the list. // 比较 key 值是否有变化,这是复用 Fiber 节点的先决条件 // 若找到 key 一样的节点,即使 key 都为 null,那也是节点一样 // 注意 key 为 null 我们也认为是相等,因为单个节点没有 key 也是正常的 if (child.key === key) { const elementType = element.type; if (elementType === REACT_FRAGMENT_TYPE) { // 复用之前的fiber节点,整体在下面 } // Didn't match. // 若key一样,但节点类型没有匹配上,无法直接复用,则直接删除该节点和其兄弟节点,停止循环, // 开始走while后面的创建新fiber节点的逻辑 deleteRemainingChildren(returnFiber, child); break; } else { // 若key不一样,不能复用,标记删除当前单个child节点 deleteChild(returnFiber, child); } child = child.sibling; // 指针指向下一个sibling节点,检测是否可以复用 } // 上面的一通循环没找到可以复用的节点,则接下来直接创建一个新的fiber节点 if (element.type === REACT_FRAGMENT_TYPE) { // 若新节点的类型是 REACT_FRAGMENT_TYPE,则调用 createFiberFromFragment() 方法创建fiber节点 // createFiberFromFragment() 也是调用的createFiber(),第1个参数指定fragment类型 // 然后再调用 new FiberNode() 创建一个fiber节点实例 const created = createFiberFromFragment(element.props.children, returnFiber.mode, lanes, element.key); created.return = returnFiber; // 新节点的return指向到父级节点 // 额外的,fragment元素没有ref return created; } else { // 若新节点是其他类型,如普通的html元素、函数组件、类组件等,则会调用 createFiberFromElement() // 这里面再接着调用 createFiberFromTypeAndProps(),然后判断element的type是哪种类型 // 然后再调用对应的create方法创建fiber节点 // 有心的同学可能已经发现,这里用了一个else,但实际上if中已经有return了,这里就用不到else了,可以去提pr了! const created = createFiberFromElement(element, returnFiber.mode, lanes); created.ref = coerceRef(returnFiber, currentFirstChild, element); // 处理ref created.return = returnFiber; return created; } }

如何复用之前的 fiber 节点?我们知道 fragment 标签 没有什么意义,仅仅是为了聚合内容,而且 fragment 标签也是可以设置 key 的。fragment 标签与其他标签是不一样的,因此这里单独进行了处理:

// 将要构建的是 fragment 类型,然后在之前的节点里找到一个 fragment 类型的
if (child.tag === Fragment) {
  /**
* deleteRemainingChildren(returnFiber, fiber); // 删除当前fiber及后续所有的兄弟节点
*/
deleteRemainingChildren(returnFiber, child.sibling); // 已找到可复用的fiber节点,从下一个节点开始全部删除 /**
* useFiber是将当前可以复用的节点和属性传入,然后复制合并到workInProgress上
* @type {Fiber}
*/
const existing = useFiber(child, element.props.children); // 该节点是fragment类型,则复用其children existing.return = returnFiber; // 重置新Fiber节点的return指针,指向当前Fiber节点 // 多说一句,fragment类型的fiber没有ref属性,这里不用处理 return existing; } else { // 其他类型,如REACT_ELEMENT_TYPE, REACT_LAZY_TYPE等 if ( child.elementType === elementType || // Keep this check inline so it only runs on the false path: (__DEV__ ? isCompatibleFamilyForHotReloading(child, element) : false) || // Lazy types should reconcile their resolved type. // We need to do this after the Hot Reloading check above, // because hot reloading has different semantics than prod because // it doesn't resuspend. So we can't let the call below suspend. (typeof elementType === 'object' && elementType !== null && elementType.$$typeof === REACT_LAZY_TYPE && resolveLazy(elementType) === child.type) ) { /**
* deleteRemainingChildren(returnFiber, fiber); // 删除当前fiber及后续所有的兄弟节点
*/
deleteRemainingChildren(returnFiber, child.sibling); // 已找到可复用的fiber节点,从下一个节点开始全部删除 const existing = useFiber(child, element.props); // 复用child节点和element.props属性 existing.ref = coerceRef(returnFiber, child, element); // 处理ref existing.return = returnFiber; // 重置新Fiber节点的return指针,指向当前Fiber节点 return existing; } }

这里可能会有人有疑问,deleteRemainingChildren() 只删除后续的节点,那前面的节点怎么办呢?前面的节点已经在 while 循环中的 else 逻辑里,把匹配不上的节点标记为删除了。

从这里也能看到,我们在 React 组件的状态变更时,尽量不要修改元素的标签类型,否则当前元素对应的 fiber 节点及所有的子节点都会被丢弃,然后重新创建。如

// 原来的
function App() {
  return (
    <div>
<Count />
<p>p>

div> ); } // 经 useState() 修改后的 function App() { return ( <secion>
<Count />
<p>p>

secion> ); }

虽然只是外层的 div 标签变成了 section 标签,内部的都没有变化,但 React 在进行对比时,还是认为没有匹配上,然后把 div 对应的 fiber 节点及所有的子节点都删除了,重新从 section 标签开始构建新的 fiber 节点。

4.2 复用之前的节点 #

若在循环的过程中,找到了可复用的 fiber 节点。

deleteRemainingChildren(returnFiber, child.sibling); // 已找到可复用的child节点,从下一个节点开始全部删除
const existing = useFiber(child, element.props); // 复用匹配到的child节点,并使用element中新的props属性
existing.ref = coerceRef(returnFiber, child, element); // 处理ref
existing.return = returnFiber; // 复用的Fiber节点的return指针,指向当前Fiber节点

在已经找到可以复用的 child 节点后,child 节点后续的节点就都可以删除了。

我们再看下 useFiber() 中是如何复用 child 这个节点的:

function useFiber(fiber: Fiber, pendingProps: mixed): Fiber {
  // We currently set sibling to null and index to 0 here because it is easy
  // to forget to do before returning it. E.g. for the single child case.
  // 将新的 fiber 节点的 index 设置为0,sibling 设置为null,
  // 因为目前我们还不知道这个节点用来干什么,比如他可能用于单节点的 case 中
  const clone = createWorkInProgress(fiber, pendingProps);
  clone.index = 0;
  clone.sibling = null;
  return clone;
}

/**
* 说是复用current节点,其实是复用 current.alternate 的那个节点,
* 因为 current 和 workInProgress 两个节点是通过 alternate 属性互相指向的
* @param current
* @param pendingProps
* @returns {Fiber}
*/
export function createWorkInProgress(current: Fiber, pendingProps: any): Fiber { let workInProgress = current.alternate; if (workInProgress === null) { /**
* 翻译:我们使用双缓冲池技术,因为我们知道我们最多只需要两个版本的树。
* 我们可以汇集其他未使用的节点,进行自由的重用。
* 这是惰性创建的,以避免为从不更新的对象分配额外的对象。
* 它还允许我们在需要时回收额外的内存
*/
// 若 workInProgress 为 null,则直接创建一个新的fiber节点 workInProgress = createFiber( current.tag, pendingProps, // 传入最新的props current.key, current.mode, ); workInProgress.elementType = current.elementType; workInProgress.type = current.type; workInProgress.stateNode = current.stateNode; // workInProgress 和 current通过 alternate 属性互相进行指向 workInProgress.alternate = current; current.alternate = workInProgress; } else { workInProgress.pendingProps = pendingProps; // 设置新的props // Needed because Blocks store data on type. workInProgress.type = current.type; // We already have an alternate. // Reset the effect tag. workInProgress.flags = NoFlags; // The effects are no longer valid. workInProgress.subtreeFlags = NoFlags; workInProgress.deletions = null; } // Reset all effects except static ones. // Static effects are not specific to a render. workInProgress.flags = current.flags & StaticMask; workInProgress.childLanes = current.childLanes; workInProgress.lanes = current.lanes; workInProgress.child = current.child; workInProgress.memoizedProps = current.memoizedProps; workInProgress.memoizedState = current.memoizedState; workInProgress.updateQueue = current.updateQueue; // Clone the dependencies object. This is mutated during the render phase, so // it cannot be shared with the current fiber. const currentDependencies = current.dependencies; workInProgress.dependencies = currentDependencies === null ? null : { lanes: currentDependencies.lanes, firstContext: currentDependencies.firstContext, }; // These will be overridden during the parent's reconciliation workInProgress.sibling = current.sibling; workInProgress.index = current.index; workInProgress.ref = current.ref; return workInProgress; }

整个 React 应用中,我们维护着两棵树,其实每棵树没啥差别,FiberRootNode 节点中的 current 指针指向到哪棵树,就展示那棵树。只不过是我们把当前正在展示的那棵树叫做 current,将要构建的那个叫做 workInProgress。这两棵树中互相的两个节点,通过 alternate 属性进行互相的指向。

4.3 普通 React 类型 element 转为 fiber #

将单个普通 React 类型的 element 转为 fiber 节点,是 createFiberFromElement(),其又调用了 createFiberFromTypeAndProps()。

这里将其进行了细致的划分,如 类组件 ClassComponent,普通 html 标签 HostComponent,strictMode 等

export function createFiberFromTypeAndProps(type: any, // React$ElementType,element的类型
key: null | string,
pendingProps: any,
owner: null | Fiber,
mode: TypeOfMode,
lanes: Lanes,
): Fiber { let fiberTag = IndeterminateComponent; // 我们还不知道当前fiber是什么类型 // The resolved type is set if we know what the final type will be. I.e. it's not lazy. // 如果我们知道最终类型type将是什么,则设置解析的类型。 let resolvedType = type; if (typeof type === 'function') { // 当前是函数组件或类组件 if (shouldConstruct(type)) { // 类组件 fiberTag = ClassComponent; } else { // 还是不明确是什么类型的组件,啥也没干 } } else if (typeof type === 'string') { // type是普通的html标签,如div, p, span等 fiberTag = HostComponent; } else { // 其他类型,如fragment, strictMode等,暂时省略 } // 通过上面的判断,得到 fiber 的类型后,则调用 createFiber() 函数,生成 fiber 节点 const fiber = createFiber(fiberTag, pendingProps, key, mode); fiber.elementType = type; // fiber中的 elmentType 与 element 中的 type 一样, fiber.type = resolvedType; // 测试环境会做一些处理,正式环境与 elementType 属性一样,type 为 REACT_LAZY_TYPE,resolveType 为 null fiber.lanes = lanes; return fiber; }

我们在之前讲解函数 beginWork() 时,当 fiber 节点没明确类型时,判断过 fiber 节点的类型,那时候是执行 fiber 节点里的 function,根据返回值来判断的。这里就不能再执行 element 中的函数了,否则会造成多次执行。

如当用函数来实现一个类组件时:

function App() {}
App.prototype = React.Component.prototype;

// React.Component
Component.prototype.isReactComponent = {};

可以看到,只要函数的 prototype 上有 isReactComponent 属性,他就肯定是类组件。但若没有这个属性,也不一定就会是函数组件,还得通过执行后的结果来判断(就是之前 beginWork()里的步骤了)。

React 源码中,采用了shouldConstruct(type)来判断。

/**
* 判断用函数实现的组件是否是类组件
* @param Component
* @returns {boolean}
*/
function shouldConstruct(Component: Function) { /**
* 类组件都是要继承 React.Component 的,而 React.Component 的 prototype 上有一个 isReactComponent 属性,值为{}
* 文件地址在: https://github.com/wenzi0github/react/blob/1cf8fdc47b360c1f1a079209fc4d49026fafd8a4/packages/react/src/ReactBaseClasses.js#L30
* 因此只要判断 Component.prototype 上是否有 isReactComponent 属性,即可判断出当前是类组件还是函数组件
*/
const prototype = Component.prototype; return !!(prototype && prototype.isReactComponent); }

我们再来汇总梳理下类型的判断:

  1. 若 element.type 是函数,则再通过 shouldConstruct() 判断,若明确类型是类组件,则 fiberTag 为 ClassComponent;若不是类组件,则还是认为他是未知组件的类型 IndeterminateComponent,后续再通过执行的结果判断;
  2. 若 element.type 是字符串,则认为是 html 标签的类型 HostComponent;
  3. 若是其他的类型,如 REACT_FRAGMENT_TYPE, REACT_SUSPENSE_TYPE 等,则单独调用对应的方法创建 fiber 节点;

若是前两种类型的,则会调用 createFiber() 创建新的 fiber 节点:

/**
* 通过上面的判断,得到fiber的类型后,则调用createFiber()函数,生成fiber节点。
* createFiber()内再执行 `new FiberNode()` 来初始化出一个fiber节点。
*/
const fiber = createFiber(fiberTag, pendingProps, key, mode); fiber.elementType = type; // fiber中的elmentType与element中的type一样, fiber.type = resolvedType; // 测试环境会做一些处理,正式环境与elementType属性一样,type为 REACT_LAZY_TYPE,resolveType为null fiber.lanes = lanes;

无论是复用之前的节点,还是新创建的 fiber 节点,到这里,我们总归是把 element 结构转成了 fiber 节点。

5. 处理单个的文本节点 reconcileSingleTextNode #

文本节点处理起来相对来说比较简单,它本身就是一个字符串或者数字,没有 key,没有 type。

<p>this is text in p tagp>
<p>1234567<span>in spanspan>abcdefp>

如上面的样例中,this is text in p tag就是单个的文本节点,但第 2 个 p 节点中,虽然123456, abcdef也是文本节点,不过这并不是单独的文本节点,而是与 span 标签组成了一个数组(span 标签里的in span也是单个的文本节点):

React中多个元素组成的数组

这种情况,我们会在第 6 节中进行说明,这里我们只处理单独的文本节点(第 1 行 p 标签里的文本)。

// 调度文本节点
function reconcileSingleTextNode(returnFiber: Fiber,
currentFirstChild: Fiber | null,
textContent: string,
lanes: Lanes,
): Fiber { // There's no need to check for keys on text nodes since we don't have a // way to define them. // 这里不再判断文本节点的key,因为文本节点就来没有key,也没有兄弟节点 if (currentFirstChild !== null && currentFirstChild.tag === HostText) { // We already have an existing node so let's just update it and delete // the rest. // 若当前第1个子节点就是文本节点,则直接删除后续的兄弟节点 deleteRemainingChildren(returnFiber, currentFirstChild.sibling); const existing = useFiber(currentFirstChild, textContent); // 复用这个文本的fiber节点,重新赋值新的文本 existing.return = returnFiber; return existing; } // The existing first child is not a text node so we need to create one // and delete the existing ones. // 若不存在子节点,或者第1个子节点不是文本节点,直接将当前所有的节点都删除,然后创建出新的文本fiber节点 deleteRemainingChildren(returnFiber, currentFirstChild); const created = createFiberFromText(textContent, returnFiber.mode, lanes); created.return = returnFiber; return created; }

这里我们要理解文本 fiber 节点的两个特性:

  1. 文本节点没有 key,无法通过 key 来进行对比;
  2. 文本节点只有一个节点,没有兄弟节点;若新节点在只有一个文本节点时,之前的树中有多个 fiber 节点,除第 1 个节点决定是否复用外,其他的可以全部删除;

6. 处理并列多个元素 reconcileChildrenArray #

当前将要构建的 element 是一个数组,即并列多个节点要进行处理。这种情况要比之前处理单个节点复杂的多,因为可能会存在末尾新增、中间插入、删除、节点移动等情况,比如要考虑的情况有:

  1. 新列表和旧列表都是顺序排布的,但新列表更长,这里在新旧对比完成后,还得接着新建新增的节点;
  2. 新列表和旧列表都是顺序排布的,但新列表更短,这里在新旧对比完成后,还得删除旧列表中多余的节点;
  3. 新列表中节点的顺序发生了变化,那就不能按照顺序一一对比了;

在 fiber 结构中,并列的元素会形成单向链表,而且也没有双指针。在 fiber 链表和 element 数组进行对比时,只能从头节点开始比较:

  1. 同一个位置(索引相同),保持不变或复用的可能性比较大;
  2. newChildren 遍历完了,说明 oldFiber 链表的节点有剩余,需要删除;
  3. oldFiber 所在链表遍历完了,新数组 newChildren 可能还有剩余,直接创建新节点;
  4. 无法按照顺序一一比较,可能发生了节点的移动,这里把旧 fiber 节点存入到 map 中;

其实还存在一种特殊的情况,oldFiber.index > newIdx,旧 fiber 节点的索引比当前索引 newIdx 大,说明之前的 element 有存在无法转为 fiber 的元素。

fiber 节点上的 index 索引值,是来自于数组中的下标,若这个下标对应的 jsx 元素无法转为 fiber 节点,则会造成该下标索引值的空缺。假如下标为 1 的 jsx 元素为 null,那么转成的 fiber 链表的索引值会变成:0->2->3。新数组在遍历时,是从 0 开始,按照顺序遍历的,同时旧 fiber 链表也跟着往后自动,当新索引 newIdx 到 1 时,oldFiber 移动到下一个节点的索引就是 2 了。然后机会出现 oldFiber.index > newIdx 的情况。

当出现这种情况时,我们直接把 oldFiber 节点设置为了 null,然后在执行 updateSlot() 时创建出新的 fiber 节点。等待 newIdx 与 oldFiber.index 相等时,再进行相同位置的比较。

reconcileChildrenArray 的流程图,也可以直接查看在线链接

reconcileChildrenArray 的流程图

接下来我们分步骤讲解一下。

6.1 相同索引位置对比 #

同一个位置(索引相同),保持不变或复用的可能性比较大。不过也只能说可能性比较大,在实际开发中什么情况都会存在,我们先以最简单的方式来处理。

let resultingFirstChild: Fiber | null = null; // 新构建出来的fiber链表的头节点
let previousNewFiber: Fiber | null = null; // 新构建出来链表的最后那个fiber节点,用于构建整个链表

let oldFiber = currentFirstChild; // 旧链表的节点,刚开始指向到第1个节点
let lastPlacedIndex = 0; // 表示当前已经新建的 Fiber 的 index 的最大值,用于判断是插入操作,还是移动操作等
let newIdx = 0; // 表示遍历 newChildren 的索引指针
let nextOldFiber = null; // 下次循环要处理的fiber节点

for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
  if (oldFiber.index > newIdx) {
    /**
* oldIndex 大于 newIndex,那么需要旧的 fiber 等待新的 fiber,一直等到位置相同。
* 那当前的 newChildren[newIdx] 则直接创建新的fiber节点
* 当 oldFiber.index > newIdx 时,说明旧 element 对应的newIdx的位置的 fiber 为null,这时将 oldFiber 设置为null,
* 然后调用 updateSlot() 时,就不再考虑复用的问题了,直接创建新的节点。
* 下一个旧的fiber还是当前的节点,等待 newIdx 索引相等的那个 child
*/
nextOldFiber = oldFiber; oldFiber = null; } else { // 旧 fiber 的索引和n ewChildren 的索引匹配上了,获取 oldFiber 的下一个兄弟节点 nextOldFiber = oldFiber.sibling; } /**
* 将旧节点和将要转换的 element 传进去,
* 1. 若 key 对应上
* 1.1 若 type 对应上,则复用之前的节点;
* 1.2 若 type 对应不上,则直接创建新的fiber节点;
* 2. 若 key 对应不上,无法复用,返回 null;
* 3. 若 oldFiber 为null,则直接创建新的fiber节点;
* @type {Fiber}
* updateSlot() 具体如何实现,我们稍后讲解
*/
const newFiber = updateSlot(returnFiber, oldFiber, newChildren[newIdx], lanes); if (newFiber === null) { /**
* 新fiber节点为 null,退出循环。
* 不过这里为null的原因有很多,比如:
* 1. newChildren[newIdx] 本身就是无法转为fiber的类型,如null, boolean, undefined等;
* 2. oldFiber 和 newChildren[newIdx] 的 key 没有匹配上;
*/
if (oldFiber === null) { oldFiber = nextOldFiber; } break; } if (shouldTrackSideEffects) { if (oldFiber && newFiber.alternate === null) { // We matched the slot, but we didn't reuse the existing fiber, so we // need to delete the existing child. // 若旧fiber节点存在,但新节点并没有复用该节点,则将该旧节点删除 deleteChild(returnFiber, oldFiber); } } /**
* 此方法是一种顺序优化手段,lastPlacedIndex 一直在更新,初始为 0,
* 表示访问过的节点在旧集合中最右的位置(即最大的位置)。
*/
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx); /**
* resultingFirstChild:新fiber链表的头节点
* previousNewFiber:用于拼接整个链表
*/
if (previousNewFiber === null) { // 若整个链表为空,则头指针指向到newFiber resultingFirstChild = newFiber; } else { // 若链表不为空,则将newFiber放到链表的后面 previousNewFiber.sibling = newFiber; } previousNewFiber = newFiber; // 指向到当前节点,方便下次拼接 oldFiber = nextOldFiber; // 下一个旧fiber节点 }

我们在循环中,尽量地去通过索引 index 和 key 等标识,来复用旧 fiber 节点。无法复用的,就创建出新的 fiber 节点。

同时,结束循环或者跳出循环的条件有多种,在循环之后,还要做出一些额外的判断。

6.2 新节点遍历完毕 #

若经过上面的循环后,新节点已全部创建完毕,这说明可能经过了删除操作,新节点的数量更少,这里我们直接把剩下的旧节点删除了就行。

// 新索引 newIdx 跟newChildren的长度一样,说明新数组已遍历完毕
// 老数组后面可能有剩余的,需要删除
if (newIdx === newChildren.length) {
  // 删除旧链表中剩余的节点
  deleteRemainingChildren(returnFiber, oldFiber);

  // 返回新链表的头节点指针
  return resultingFirstChild;
}

后续已不需要其他的操作了,直接返回新链表的头节点指针即可。

6.3 旧 fiber 节点遍历完毕 #

若经过上面的循环后,旧 fiber 节点已遍历完毕,但 newChildren 中可能还有剩余的元素没有转为 fiber 节点,但现在旧 fiber 节点已全部都复用完了,这里直接创建新的 fiber 节点即可。

// 若旧数据中所有的节点都复用了,说明新数组可能还有剩余
if (oldFiber === null) {
  // 这里已经没有旧的fiber节点可以复用了,然后我们就选择直接创建的方式
  for (; newIdx < newChildren.length; newIdx++) {
    const newFiber = createChild(returnFiber, newChildren[newIdx], lanes);
    if (newFiber === null) {
      continue;
    }
    lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);

    // 接着上面的链表往后拼接
    if (previousNewFiber === null) {
      // 记录起始的第1个节点
      resultingFirstChild = newFiber;
    } else {
      previousNewFiber.sibling = newFiber;
    }
    previousNewFiber = newFiber;
  }

  // 返回新链表的头节点指针
  return resultingFirstChild;
}

到这里,目前简单的对数组进行增、删节点的对比还是比较简单,接下来就是移动的情况是如何进行复用的呢?

6.4 节点位置发生了移动 #

若节点的位置发生了变动,虽然在旧节点链表中也存在这个节点,但若按顺序对比时,确实不方便找到这个节点。因此可以把这些旧节点放到 Map 中,然后根据 key 或者 index 获取。

/**
* 将 currentFirstChild 和后续所有的兄弟节点放到map中,方便查找
* 若该 fiber 节点有 key,则使用该 key 作为 map 的 key;否则使用隐性的index作为map的key
* @param {Fiber} returnFiber 要存储的节点的父级节点,但这个参数没用到
* @param {Fiber} currentFirstChild 要存储的链表的头节点指针
* @returns {Map} 返回存储所有节点的map对象
*/
function mapRemainingChildren(returnFiber: Fiber, currentFirstChild: Fiber): MapFiber> { /**
* 将剩余所有的子节点都存放到 map 中,方便可以通过 key 快速查找该fiber节点
* 若该 fiber 节点有 key,则使用该key作为map的key;否则使用隐性的index作为map的key
*/
const existingChildren: MapFiber> = new Map(); let existingChild = currentFirstChild; while (existingChild !== null) { if (existingChild.key !== null) { existingChildren.set(existingChild.key, existingChild); } else { existingChildren.set(existingChild.index, existingChild); } existingChild = existingChild.sibling; } return existingChildren; }

把所有的旧 fiber 节点存储到 Map 中后,就接着循环新数组 newChildren,然后从 map 中获取到对应的旧 fiber 节点(也可能不存在),再创建出新的节点。

for (; newIdx < newChildren.length; newIdx++) {
  // 从 map 中查找是否存在可以复用的fiber节点,然后生成新的fiber节点
  const newFiber = updateFromMap(existingChildren, returnFiber, newIdx, newChildren[newIdx], lanes);
  if (newFiber !== null) {
    // 这里只处理 newFiber 不为null的情况
    if (shouldTrackSideEffects) {
      // 若需要记录副作用
      if (newFiber.alternate !== null) {
        /**
* newFiber.alternate指向到current,若current不为空,说明复用了该fiber节点,
* 这里我们要在 map 中删除,因为后面会把 map 中剩余未复用的节点删除掉的,
* 所以这里我们要及时把已复用的节点从 map 中剔除掉
*/
existingChildren.delete(newFiber.key === null ? newIdx : newFiber.key); } } lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx); // 接着之前的链表进行拼接 if (previousNewFiber === null) { resultingFirstChild = newFiber; } else { previousNewFiber.sibling = newFiber; } previousNewFiber = newFiber; } } if (shouldTrackSideEffects) { // 将 map 中没有复用的 fiber 节点添加到删除的副作用队列中,等待删除 existingChildren.forEach(child => deleteChild(returnFiber, child)); } // 返回新链表的头节点指针 return resultingFirstChild;

到这里,我们新数组 newChildren 中所有的 element 结构,都已转为 fiber 节点。

7. 几个关于 fiber 的工具函数 #

我们在上面探讨前后 diff 对比时,涉及到了多个对 fiber 处理的工具函数,但都跳过去了,这里我们挑几个稍微讲解下。

我们在 diff 阶段涉及到所有对 fiber 的增删等操作,都只是打上标记而已,并不是立刻进行处理的,是要等到 commit 阶段才会处理。

7.1 删除单个节点 deleteChild #

删除单一某个 fiber 节点,这里会将该节点,存储到其父级 fiber 节点的 deletions 中。

/**
* 将returnFiber子元素中,需要删除的fiber节点放到deletions的副作用数组中
* 该方法只删除一个节点
* 当前diff时不会立即删除,而是在更新时,才会将该数组中的fiber节点进行删除
* @param returnFiber
* @param childToDelete
*/
function deleteChild(returnFiber: Fiber, childToDelete: Fiber): void { if (!shouldTrackSideEffects) { // 不需要收集副作用时,直接返回,不进行任何操作 return; } const deletions = returnFiber.deletions; if (deletions === null) { // 若副作用数组为空,则创建一个 returnFiber.deletions = [childToDelete]; returnFiber.flags |= ChildDeletion; } else { // 否则直接推入 deletions.push(childToDelete); } }

7.2 批量删除多个节点 deleteRemainingChildren #

跟上面的 deleteChild 很像,但这个函数会把从某个节点开始到结尾所有的 fiber 节点标记为删除状态。

/**
* 删除 returnFiber 的子元素中,currentFirstChild及后续所有的兄弟元素
* 即把 currentFirstChild 及其兄弟元素,都放到 returnFiber 的 deletions 的副作用数组中,等待删除
* 这是一个批量删除节点的方法
* @param returnFiber 要删除节点的父级节点
* @param currentFirstChild 当前要删除节点的起始节点
* @returns {null}
*/
function deleteRemainingChildren(returnFiber: Fiber, currentFirstChild: Fiber | null): null { if (!shouldTrackSideEffects) { // 不需要收集副作用时,直接返回,不进行任何操作 return null; } /**
* 从 currentFirstChild 节点开始,把当前及后续所有的节点,通过 deleteChild() 方法标记为删除状态
* @type {Fiber}
*/
let childToDelete = currentFirstChild; while (childToDelete !== null) { deleteChild(returnFiber, childToDelete); childToDelete = childToDelete.sibling; } return null; }

7.3 复用 fiber 节点 useFiber #

在没有任何更新时,React 中的两棵 fiber 树是一一对应的。不过当产生更新后,前后两棵 fiber 树就不一样了。

若当前要根据 element 生成一个 fiber 节点,目前有 2 种情况:

  1. current 节点不存在或 current.alternate 不存在,说明该 element 是新增的,直接新建即可;
  2. current.alternate 存在,则可以直接复用;

在 useFiber() 中,会调用 createWorkInProgress() 来尝试复用 workInProgress 节点,生成新的 fiber 节点:

export function createWorkInProgress(current: Fiber, pendingProps: any): Fiber {
  let workInProgress = current.alternate;
  if (workInProgress === null) {
    // We use a double buffering pooling technique because we know that we'll
    // only ever need at most two versions of a tree. We pool the "other" unused
    // node that we're free to reuse. This is lazily created to avoid allocating
    // extra objects for things that are never updated. It also allow us to
    // reclaim the extra memory if needed.
    /**
* 翻译:我们使用双缓冲池技术,因为我们知道我们最多只需要两个版本的树。
* 我们可以汇集其他未使用的节点,进行自由的重用。
* 这是惰性创建的,以避免为从不更新的对象分配额外的对象。
* 它还允许我们在需要时回收额外的内存
*/
workInProgress = createFiber(current.tag, pendingProps, current.key, current.mode); workInProgress.elementType = current.elementType; workInProgress.type = current.type; workInProgress.stateNode = current.stateNode; /**
* workInProgress是新创建出来的,要和current建立联系
* workInProgress 和 current通过 alternate 属性互相进行指向
*/
workInProgress.alternate = current; current.alternate = workInProgress; } else { workInProgress.pendingProps = pendingProps; // Needed because Blocks store data on type. workInProgress.type = current.type; // We already have an alternate. // Reset the effect tag. workInProgress.flags = NoFlags; // The effects are no longer valid. workInProgress.subtreeFlags = NoFlags; workInProgress.deletions = null; } /**
* 以下语句,复用current中的特性
*/
// Reset all effects except static ones. // Static effects are not specific to a render. workInProgress.flags = current.flags & StaticMask; workInProgress.childLanes = current.childLanes; workInProgress.lanes = current.lanes; workInProgress.child = current.child; workInProgress.memoizedProps = current.memoizedProps; workInProgress.memoizedState = current.memoizedState; workInProgress.updateQueue = current.updateQueue; // These will be overridden during the parent's reconciliation workInProgress.sibling = current.sibling; workInProgress.index = current.index; workInProgress.ref = current.ref; return workInProgress; }

在 createWorkInProgress() 中,若 workInProgress(current.alternate) 不存在,则新创建一个,然后与 current 建立关联;若 workInProgress 已存在,则直接复用该节点,并将 current 中的特性给到这个 workInProgress 节点。

不过在 reconcileChildFibers() 中的 useFiber() 里,复用节点时,暂时还不知道它将来的使用情况,有可能只是做为单个 fiber 节点使用,因此把 index 和 sibling 进行了重置。

/**
* 复用fiber节点的alternate,生成一个新的fiber节点
* 若alternate为空,则创建;
* 若不为空,则直接复用,并将传入的fiber属性和pendingProps的属性给到alternate上
* @param fiber
* @param pendingProps
* @returns {Fiber}
*/
function useFiber(fiber: Fiber, pendingProps: mixed): Fiber { const clone = createWorkInProgress(fiber, pendingProps); // 重置以下两个属性 clone.index = 0; clone.sibling = null; return clone; }

无论我们是复用,还是新创建的 fiber 节点,目前并不知道它将来怎么使用,所

7.4 updateSlot #

updateSlot()和 createChild()两个方法很像,但两者最大的区别就在于:是否要复用 oldFiber 节点。

  • updateSlot() 会尽量复用 oldFiber 节点,若 oldFiber 的 key 和 element 的 key 对应不上,则直接返回 null,否则复用创建;
  • createChild() 则不考虑复用的问题,直接用 element 新建出新的 fiber 节点;

里面要稍微注意的一点:若当前单个结构是一个数组类型,则会先创建一个 fragment 类型的 fiber 节点,然后再递归创建内部的结构。如:

function App() {
  const list = ['Jack', 'Tom', 'Jerry'];

  return (
    <div className="App">
<ul>
{list.map(username => (
<li key={username}>{username}li>

))}
<li>Emmali>
<li>Miali>
ul>
div> ); }

代码中,数组 list.map()后得到的是一个数组结构,在 React 内构建 fiber 节点时,并不会把数组中的这几个 li 标签,和下面的 2 个 li 标签合到一起。实际会变成这样:

function App() {
  const list = ['Jack', 'Tom', 'Jerry'];

  return (
    <div className="App">
<ul>
<React.Fragment>
<li key="Jack">Jackli>

<li key="Tom">Tomli>
<li key="Jerry">Jerryli>
React.Fragment>
<li>Emmali>
<li>Miali>
ul>
div> ); }

具体的实现:

/**
* 创建或更新element结构 newChild 为fiber节点
* 若oldFiber不为空,且newChild与oldFiber的key能对得上,则复用旧fiber节点
* 否则,创建一个新的fiber节点
* 该updateSlot方法与createChild方法很像,但createChild只有创建新fiber节点的功能
* 而该updateSlot()方法则可以根据oldFiber,来决定是复用之前的fiber节点,还是新创建节点
* @param returnFiber
* @param oldFiber
* @param newChild
* @param lanes
* @returns {Fiber|null}
*/
function updateSlot(returnFiber: Fiber, oldFiber: Fiber | null, newChild: any, lanes: Lanes): Fiber | null { // 若key相等,则更新fiber节点;否则直接返回null const key = oldFiber !== null ? oldFiber.key : null; if ((typeof newChild === 'string' && newChild !== '') || typeof newChild === 'number') { // 文本节点本身是没有key的,若旧fiber节点有key,则说明无法复用 if (key !== null) { return null; } // 若旧fiber没有key,即使他不是文本节点,我们也尝试复用 return updateTextNode(returnFiber, oldFiber, '' + newChild, lanes); } if (typeof newChild === 'object' && newChild !== null) { // 若是一些ReactElement类型的,则判断key是否相等;相等则复用;不相等则返回null switch (newChild.$$typeof) { case REACT_ELEMENT_TYPE: { if (newChild.key === key) { // key一样才更新 return updateElement(returnFiber, oldFiber, newChild, lanes); } else { // key不一样,则直接返回null return null; } } // 其他类型暂时省略 } if (isArray(newChild) || getIteratorFn(newChild)) { // 当前是数组或其他迭代类型,本身是没有key的,若oldFiber有key,则无法复用 if (key !== null) { return null; } // 若 newChild 是数组或者迭代类型,则更新为fragment类型 return updateFragment(returnFiber, oldFiber, newChild, lanes, null); } } // 其他类型不进行处理,直接返回null return null; }

updateSlot() 着重在复用上,只有前后两个 key 匹配上,才会继续后续的流程,否则直接返回 null。

8. 总结 #

我们主要了解了不同类型的 element 转成 fiber 节点的过程,如文本类型的,React 普通类型的 ,数组类型的,等等。尤其是在数组对比时,涉及到每个元素的新增、删除、移动等操作,对比起来要复杂一些。

可以看到,当组件产生比较大的变更时,React 需要做更多的动作,来构建出新的 fiber 树,因此我们在开发过程中,若从性能优化的角度考虑,尤其要注意的是:

  1. 节点不要产生大量的越级操作:因为 React 是只进行同层节点的对比,若同一个位置的子节点产生了比较大的变动,则只会舍弃掉之前的 fiber 节点,从而执行创建新 fiber 节点的操作;React 并不会把之前的 fiber 节点移动到另一个位置;相应的,之前的 jsx 节点移动到另一个位置后,在进行前后对比后,同样会执行更多的创建操作;
  2. 不修改节点的 key 和 type 类型,如使用随机数做为列表的 key,或从 div 标签改成 p 标签等操作,在 diff 对比过程中,都会直接舍弃掉之前的 fiber 节点及所有的子节点(即使子节点没有变动),然后重新创建出新的 fiber 节点;

0

评论

博主关闭了当前页面的评论