Fancy Front End Fancy Front End
  • 开始上手
  • 基础
  • 调度器(Scheduler)
  • 更新器(Updater)
  • 渲染器(Render)
  • 更新周期
  • hooks 原理
  • 总结
  • 📙 React源码漂流记
  • 开始上手
  • 基础
  • reactivity
  • runtime-core
  • runtime-dom
  • Awesome Web
  • Awesome NodeJS
话题
  • 导航
  • Q&A
  • 幻灯片
  • 关于
  • 分类
  • 标签
  • 归档
博客 (opens new window)
GitHub (opens new window)

Jonsam NG

让有意义的事变得有意思,让有意思的事变得有意义
  • 开始上手
  • 基础
  • 调度器(Scheduler)
  • 更新器(Updater)
  • 渲染器(Render)
  • 更新周期
  • hooks 原理
  • 总结
  • 📙 React源码漂流记
  • 开始上手
  • 基础
  • reactivity
  • runtime-core
  • runtime-dom
  • Awesome Web
  • Awesome NodeJS
话题
  • 导航
  • Q&A
  • 幻灯片
  • 关于
  • 分类
  • 标签
  • 归档
博客 (opens new window)
GitHub (opens new window)
  • 开始上手
  • plan 计划
  • 基础

  • 调和(Reconciliation)

  • 调度器(Scheduler)

  • 更新器(Updater)

  • 渲染器(Render)

  • hooks原理

  • 总结

  • React源码漂流记

    • 开始上手
    • Plan 计划
    • 前言
    • React 源码漂流记:ReactElement 与基础概念
    • React 源码漂流记:ReactChildren 与节点操纵
    • React 源码漂流记:React 整体结构和理念初认识
    • React 源码漂流记:React 调和器核心源码解读(一)
    • React 源码漂流记:React 调和器核心源码解读(二)
    • React 源码漂流记:React 调和器核心源码解读(三)
    • React 源码漂流记:React 调和器核心源码解读(四)
    • React 源码漂流记:React 调和器核心源码解读(五)
      • 目录
      • 前言
      • bailoutOnAlreadyFinishedWork
      • cloneChildFibers
      • reconcileChildren
      • reconcileChildFibers
      • reconcileChildrenArray
      • reconcileSingleElement
      • reconcileSingleTextNode
      • placeSingleChild
      • updateSlot
      • updateFromMap
      • placeChild
      • deleteChild
      • 扩展
      • 问题
      • 总结
      • 参考
    • React 源码漂流记:React 调和器核心源码解读(六)
    • React 源码漂流记:React 调和器核心源码解读(七)
    • React 源码漂流记:React 调和器核心源码解读(八)
    • React 源码漂流记:React 调和器核心源码解读(九)
    • React 源码漂流记:React 调和器核心源码解读(十)
    • React 源码漂流记:React 调度器核心源码解读(一)
    • 带着原理重读 React 官方文档(一)
    • 带着原理重读 React 官方文档(二)
  • react
  • React源码漂流记
jonsam
2022-07-25
目录

React 源码漂流记:React 调和器核心源码解读(五)

标签: React17精简

# 目录

  • 目录
  • 前言
  • bailoutOnAlreadyFinishedWork
  • cloneChildFibers
  • reconcileChildren
  • reconcileChildFibers
  • reconcileChildrenArray
  • reconcileSingleElement
  • reconcileSingleTextNode
  • placeSingleChild
  • updateSlot
  • updateFromMap
  • placeChild
  • deleteChild
  • 扩展
    • 函数式组件和类组件在 reconcileChildren 之前做了什么?
    • workInProgress Tree 是如何初始化的?
  • 问题
  • 总结
  • 参考

# 前言

在上文中,我们探讨了在 beginWork 函数中针对不同的类型(内部类型)的 React 组件所采取的不同的调和策略:复用和重新调和。在本文中,我们继续来探讨在捕获阶段中组件的复用机制以及组件的具体的调和过程。本文将主要围绕着 cloneChildFibers 和 reconcileChildren 两个核心函数展开探讨。

# bailoutOnAlreadyFinishedWork

通过上文分析,我们已经了解了无论是函数式组件还是类组件,在提前退出时都是调用 bailoutOnAlreadyFinishedWork 函数。要想深入了解 cloneChildFibers 的作用,我们可能要先了解此函数的原理。

// src/react/packages/react-reconciler/src/ReactFiberBeginWork.new.js
function bailoutOnAlreadyFinishedWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
  // ......
  markSkippedUpdateLanes(workInProgress.lanes);

  // Check if the children have any pending work.
  // 检查子树是否有相应的更新,通过 childLanes 判断,如果没有更新,则可以提前结束子树的捕获过程,并开始向上冒泡
  if (!includesSomeLane(renderLanes, workInProgress.childLanes)) {
    // The children don't have any work either. We can skip them.
    return null;
  }
  // This fiber doesn't have work, but its subtree does. Clone the child
  // fibers and continue.
  // 子树有更新,复制子树的 fibers 并返回子节点继续捕获
  cloneChildFibers(current, workInProgress);
  return workInProgress.child;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

分析如下:

  • markSkippedUpdateLanes 函数将本次渲染期间累计的未处理的更新的 lanes 合并到 workInProgress.lanes 中。这是因为在调度到真正渲染期间,可能会有新的 lanes 被收集到。
  • 这里有一个性能的优化,如果 renderLanes 中不包含 workInProgress 节点的 childLanes ,即子 FiberTree 没有更新,则可以通过返回 null ,提前结束捕获。

# cloneChildFibers

此函数将复用当前 Fiber 节点的所有下级子节点。

export function cloneChildFibers(
  current: Fiber | null,
  workInProgress: Fiber,
): void {
  // ......
  let currentChild = workInProgress.child;
  let newChild = createWorkInProgress(currentChild, currentChild.pendingProps);
  workInProgress.child = newChild;
  newChild.return = workInProgress;
  while (currentChild.sibling !== null) {
    currentChild = currentChild.sibling;
    newChild = newChild.sibling = createWorkInProgress(
      currentChild,
      currentChild.pendingProps,
    );
    newChild.return = workInProgress;
  }
  newChild.sibling = null;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

下图是相关的数据结构的操作逻辑:

clone_child_fibers

分析如下:

  • createWorkInProgress 创建新的 workInProgress fiber 节点,并复用 current FiberNode 的属性(如果 workInProgress 节点是悬空的,则创建 Fiber 节点,见 createFiber 函数)。
  • newChild 指针不断向兄弟节点移动,直至遍历完所有的兄弟节点。

# reconcileChildren

此函数根据是否是首次渲染而决定是否追踪副作用,并且调用相应的函数对姐姐点进行调和。

export function reconcileChildren(
  current: Fiber | null,
  workInProgress: Fiber,
  nextChildren: any,
  renderLanes: Lanes,
) {
  if (current === null) {
    workInProgress.child = mountChildFibers(
      workInProgress,
      null,
      nextChildren,
      renderLanes,
    );
  } else {
    workInProgress.child = reconcileChildFibers(
      workInProgress,
      current.child,
      nextChildren,
      renderLanes,
    );
  }
}

const reconcileChildFibers = ChildReconciler(true);
const mountChildFibers = ChildReconciler(false);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

分析如下:

  • 如果是首次渲染,则不必追踪副作用,调用 mountChildFibers ,否则则调用 reconcileChildFibers 。主要这两个函数都是调用包装函数 ChildReconciler 实现的。
  • reconcileChildren 调和子节点,并赋值给 workInProgress.child 。

# reconcileChildFibers

此函数对 workInProgress 节点的 children 节点进行调和,并且标记副作用。

// src/react/packages/react-reconciler/src/ReactChildFiber.new.js
// This API will tag the children with the side-effect of the reconciliation
// itself. They will be added to the side-effect list as we pass through the
// children and the parent.
function reconcileChildFibers(
  returnFiber: Fiber,
  currentFirstChild: Fiber | null,
  newChild: any,
  lanes: Lanes,
): Fiber | null {
  // 处理 Element Fragment 解包装,类型为 REACT_FRAGMENT_TYPE
  const isUnkeyedTopLevelFragment =
    typeof newChild === 'object' &&
    newChild !== null &&
    newChild.type === REACT_FRAGMENT_TYPE &&
    newChild.key === null;
  if (isUnkeyedTopLevelFragment) {
    newChild = newChild.props.children;
  }

  // 非文本节点,包括数组和迭代器
  if (typeof newChild === 'object' && newChild !== null) {
    switch (newChild.$$typeof) {
      case REACT_ELEMENT_TYPE:
        return placeSingleChild(reconcileSingleElement(/*...*/));
      case REACT_PORTAL_TYPE:
        return placeSingleChild(reconcileSinglePortal(/*...*/));
      case REACT_LAZY_TYPE:
        return reconcileChildFibers(/*...*/);
    }

    if (isArray(newChild)) {
      return reconcileChildrenArray(/*...*/);
    }
    if (getIteratorFn(newChild)) {
      return reconcileChildrenIterator(/*...*/);
    }
    // ......
  }
  // 如果是文本节点
  if (
    (typeof newChild === 'string' && newChild !== '') ||
    typeof newChild === 'number'
  ) {
    return placeSingleChild(reconcileSingleTextNode(/*...*/));
  }

  // 如果不符合上述情况,视为清空子节点
  return deleteRemainingChildren(returnFiber, currentFirstChild);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50

分析如下:

  • 明确 newChild 是什么? newChild 本质上是 ReactElement,是 React.createElement 语法糖创建出来的对象,具体的结构可见参见 ReactElement 与基础概念 (opens new window)。
  • React.Fragment 解包装之后成为节点链表。
  • 本函数处理合法的节点类型(注意,此处不是 Fiber 类型,而是 ReactElement 类型,根据 ReactElement.$$typeof 判断):普通 Element、Portal、Lazy 节点、节点链表、节点迭代器、文本节点,其余的形式均视为非法清空所有子节点。
  • 从整体来看,子节点的处理分成两步:调和(reconcile)和置位(place)。调和的过程就是创建 workInProgress FiberTree 的 Fiber 节点的过程,而置位就是进行副作用标记的过程。
  • 对于 Lazy 节点,即延迟加载的节点,解包装出延迟的组件后,递归调用自身即可。
  • 注意: reconcileChildFibers 返回调和后的第一个子节点。
  • reconcileChildFibers 实现了从 ReactElement 到 Fiber 的飞跃。从入参 newChild 是 ReactElement,到返回值为 Fiber 可见一斑。

下面我们分别来探讨下 Element 节点链表、 REACT_ELEMENT_TYPE 单一节点和 Element 文本节点的具体原理。

# reconcileChildrenArray

标签: 重要DIFF算法

此函数对同级子节点应用 DIFF 算法,目的是通过 DIFF 算法根据 current Fiber 和 ReactElement 高效的创建 workInProgress Fiber。此过程可视为节点链表的调和过程。

注意

相关简写:

  • 原链表:current 节点链表。
  • 新数组:ReactElement 节点数组。
  • 原节点:current 节点链表中对应位置的节点(Fiber)。
  • 新节点:ReactElement 节点链表中对应位置的节点(ReactElement)。
  • 结果节点:经过 DIFF 后要置位于对应位置的节点(Fiber)。

具体过程如下:

// src/react/packages/react-reconciler/src/ReactChildFiber.new.js
function reconcileChildrenArray(
  // workInProgress Fiber
  returnFiber: Fiber,
  // current Fiber 上的第一个子节点
  currentFirstChild: Fiber | null,
  // 当前调和中的 ReactElement
  newChildren: Array<*>,
  // 调和节点的优先级
  lanes: Lanes,
): Fiber | null {
  // ......
  // 调和后的第一个子节点
  let resultingFirstChild: Fiber | null = null;
  // 上一个被置位的结果节点
  let previousNewFiber: Fiber | null = null;
  // 原节点指针,指向 current 节点链表中的节点
  let oldFiber = currentFirstChild;
  // 上一次置位的下标
  let lastPlacedIndex = 0;
  let newIdx = 0;
  // 下一轮要比较的原节点指针
  let nextOldFiber = null;
  // 以下取原链表和新数组较短的长度的部分进行 DIFF(循环原链表和新数组,知道两者之一没有更多节点)
  for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
    // 如果原节点的下标与当前置位的下标不一致,说明可能是在原节点的前面添加了新节点,因此
    // 将原节点推迟到下一次比较,本次比较原节点则悬空
    if (oldFiber.index > newIdx) {
      nextOldFiber = oldFiber;
      oldFiber = null;
    } else {
      // 如果下标一致,则下一个要比较的原节点则为当前原节点的兄弟节点
      nextOldFiber = oldFiber.sibling;
    }
    // 根据当前比较,尝试从原链表中复用节点。
    // 根据 children 的类型进行匹配,如果 key 值匹配则返回结果节点,否则返回 null。
    const newFiber = updateSlot(
      returnFiber,
      // 本次比较的原节点,可能是 null
      oldFiber,
      // 本次比较的新节点
      newChildren[newIdx],
      lanes,
    );
    // 如果没有节点可复用,则跳出循环
    if (newFiber === null) {
      if (oldFiber === null) {
        oldFiber = nextOldFiber;
      }
      break;
    }
    // 如果节点可以复用,且非初次渲染,原节点存在且未与新节点建立链接,则删除原节点
    if (shouldTrackSideEffects) {
      if (oldFiber && newFiber.alternate === null) {
        deleteChild(returnFiber, oldFiber);
      }
    }
    // 将更新后的新节点置位
    lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
    // 首个结果节点链接在 resultingFirstChild 上,后续结果结果连接在上个节点的 sibling 指针上
    if (previousNewFiber === null) {
      resultingFirstChild = newFiber;
    } else {
      previousNewFiber.sibling = newFiber;
    }
    previousNewFiber = newFiber;
    // 原节点指针移动到下一个原节点
    oldFiber = nextOldFiber;
  }
  // 如果经过上述复用之后已经没有更多新节点,DIFF 结束,删除剩余的原节点,并返回结果节点首节点
  if (newIdx === newChildren.length) {
    deleteRemainingChildren(returnFiber, oldFiber);
    // ......
    return resultingFirstChild;
  }
  // 如果没有更多原节点,后续结果无需 DIFF,可直接置位,并返回结果节点首节点
  if (oldFiber === null) {
    // 循环剩余的新节点
    for (; newIdx < newChildren.length; newIdx++) {
      // 根据新节点创建子节点
      const newFiber = createChild(returnFiber, newChildren[newIdx], lanes);
      if (newFiber === null) {
        continue;
      }
      // 对子节点进行置位
      lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
      // 将子节点链接到结果节点链表
      if (previousNewFiber === null) {
        resultingFirstChild = newFiber;
      } else {
        previousNewFiber.sibling = newFiber;
      }
      previousNewFiber = newFiber;
    }
    // ......
    return resultingFirstChild;
  }

  // 如果经过上述 DIFF,原节点和新节点均有剩余,说明提前退出了 DIFF 过程(遇到 key 值不匹配)
  // 则更换 DIFF 策略,采用节点池的方式复用节点
  const existingChildren = mapRemainingChildren(returnFiber, oldFiber);

  // 循环剩余的待置位的新节点,寻求节点复用
  for (; newIdx < newChildren.length; newIdx++) {
    // 尝试从节点池复用节点
    // 注意:这里仍然是 key 值匹配才可以复用,如果匹配不到则创建节点
    const newFiber = updateFromMap(
      existingChildren,
      returnFiber,
      newIdx,
      newChildren[newIdx],
      lanes,
    );
    // 如果有可复用节点(或者创建出节点),则复用节点并置位
    if (newFiber !== null) {
      if (shouldTrackSideEffects) {
        // 非首次渲染时,如果该节点已经与原节点链接,则删除原节点
        if (newFiber.alternate !== null) {
          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) {
    existingChildren.forEach(child => deleteChild(returnFiber, child));
  }
  // ......
  // 返回首个结果节点
  return resultingFirstChild;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141

分析如下:

  • DIFF 过程总结如下:将相应下标的原节点用于新节点进行 DIFF(严格匹配 key 值),直到原链表或者新数组没有更多节点,或者首次遇到因无可匹配 key 值( newChild.key === key ,考虑各自为 null 的情况)而无法复用节点的情况提前循环。退出循环分三种情况:如果没有更多的新节点,DIFF 结束,删除剩余的原节点,并返回结果节点首节点;如果没有更多原节点,后续结果无需 DIFF,可直接创建节点并置位,并返回结果节点首节点;如果原节点和新节点均有剩余,则采用节点池的方式复用节点,复用节点或者创建节点。
  • 节点的 DIFF 本质上是对同层级的节点的 DIFF。与 vue3 的 DIFF 算法相比,可能 vue3 的 DIFF 的效率要更高一些。React 目前的 DIFF 算法没有办法采用更高效的双向搜索(从两端同时 DIFF)的方式,这是因为 React 中同级子节点是以单向链表管理的,节点之间用 sibling 指针链接,没有反向指针导致节点无法反向回溯。那么为什么 Vue3 可以做到双向搜索呢?这是因为 vue3 的同级子节点使用数组管理的,数组原生支持正向和反向的遍历。
  • 为什么不合法的 key 值会在控制台打印警告?函数中 warnOnInvalidKey 会检查不合法的 key 值,包括重复的 key 值,并且打印警告信息。
  • 为什么 key 值很重要?因为 key 值对于复用节点很重要!更重 DIFF 算法都依赖 key 以复用原节点,以节省创建节点的内存的开销。

# reconcileSingleElement

此函数对单一 Element 子节点进行调和。

// src/react/packages/react-reconciler/src/ReactChildFiber.new.js
function reconcileSingleElement(
    returnFiber: Fiber,
    currentFirstChild: Fiber | null,
    element: ReactElement,
    lanes: Lanes,
  ): Fiber {
  const key = element.key;
  let child = currentFirstChild;
  // 遍历原节点链表寻求能够被新节点复用的原节点
  while (child !== null) {
    // 如果有key值匹配的原节点
    if (child.key === key) {
      const elementType = element.type;
      // 如果 Element 是 Fragment,且可复用原节点也是 Fragment
      // 在 reconcileChildFibers 中 Element 已经解包装过一次
      if (elementType === REACT_FRAGMENT_TYPE) {
        if (child.tag === Fragment) {
          // 删除剩余的所有的节点,因为可复用的 child 已经缓存
          deleteRemainingChildren(returnFiber, child.sibling);
          // 复用 child 并对 Element 再做一次解包装,注意这里的复用节点会复用 child 和 sibling 指针
          const existing = useFiber(child, element.props.children);
          // 将 return 指针指向 returnFiber
          existing.return = returnFiber;
          // ......
          return existing;
        }
      } else {
        // 如果 Element 不是 Fragment,且可复用原节点的类型和 Element 相同
        if (child.elementType === elementType /*......*/) {
          // 删除剩余的所有节点
          deleteRemainingChildren(returnFiber, child.sibling);
          // 复用原节点
          const existing = useFiber(child, element.props);
          // 处理 string refs
          existing.ref = coerceRef(returnFiber, child, element);
          // 将 return 指针指向 returnFiber
          existing.return = returnFiber;
          // ......
          return existing;
        }
      }
      // Didn't match.
      deleteRemainingChildren(returnFiber, child);
      break;
    } else {
      // 旧节点如果不能被复用直接删除
      deleteChild(returnFiber, child);
    }
    // 指针移到兄弟节点
    child = child.sibling;
  }
  // 如果没有可复用的节点,根据 Element 类型创建新节点(Fiber),并将之返回
  if (element.type === REACT_FRAGMENT_TYPE) {
    const created = createFiberFromFragment(/*......*/);
    created.return = returnFiber;
    return created;
  } else {
    const created = createFiberFromElement(element, returnFiber.mode, lanes);
    created.ref = coerceRef(returnFiber, currentFirstChild, element);
    created.return = returnFiber;
    return created;
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64

分析如下:

  • coerceRef 处理 String Refs ,参考 Legacy API: String Refs (opens new window)。
  • 对于单一单一 Element 子节点,在调和时需要根据 Element 的类型(Fragment 或者 普通 Element)采用不同的调和策略,如果有旧节点可复用,在调用 useFiber 复用之,如果没有则 createFiberFromFragment 或者 createFiberFromElement 创建新节点。

# reconcileSingleTextNode

此函数对单一 Element 文本子节点进行调和。

function reconcileSingleTextNode(
  returnFiber: Fiber,
  currentFirstChild: Fiber | null,
  textContent: string,
  lanes: Lanes,
): Fiber {
  // 如果原节点的首个节点刚好是文本节点,则删除其余的节点,并且复用这个文本节点
  if (currentFirstChild !== null && currentFirstChild.tag === HostText) {
    deleteRemainingChildren(returnFiber, currentFirstChild.sibling);
    const existing = useFiber(currentFirstChild, textContent);
    existing.return = returnFiber;
    return existing;
  }
  // 如果首个节点不是文本节点,则删除所有节点,创建新节点
  deleteRemainingChildren(returnFiber, currentFirstChild);
  const created = createFiberFromText(textContent, returnFiber.mode, lanes);
  created.return = returnFiber;
  return created;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

因为这里 Element 是文本类型,所以调用 createFiberFromText 函数创建 FIber 节点。需要注意的是,创建的新节点需要把 return 指针指向父节点。

# placeSingleChild

此函数对 reconcileSingleElement 创建的节点进行置位(标记副作用,Effect Tag)。

// src/react/packages/react-reconciler/src/ReactChildFiber.new.js
function placeSingleChild(newFiber: Fiber): Fiber {
  // 添加 Placement 标记
  if (shouldTrackSideEffects && newFiber.alternate === null) {
    newFiber.flags |= Placement;
  }
  return newFiber;
}
1
2
3
4
5
6
7
8

# updateSlot

此函数对 reconcileChildrenArray 中可复用的节点进行更新复用。

function updateSlot(
    returnFiber: Fiber,
    oldFiber: Fiber | null,
    newChild: any,
    lanes: Lanes,
  ): Fiber | null {
  const key = oldFiber !== null ? oldFiber.key : null;
  // 如果是文本节点则调用 updateTextNode 复用节点
  if (
    (typeof newChild === 'string' && newChild !== '') ||
    typeof newChild === 'number'
  ) {
    // ......
    return updateTextNode(returnFiber, oldFiber, '' + newChild, lanes);
  }

  if (typeof newChild === 'object' && newChild !== null) {
    switch (newChild.$$typeof) {
      case REACT_ELEMENT_TYPE: {
        if (newChild.key === key) {
          return updateElement(returnFiber, oldFiber, newChild, lanes);
        } else {
          return null;
        }
      }
      case REACT_PORTAL_TYPE: {
        if (newChild.key === key) {
          return updatePortal(returnFiber, oldFiber, newChild, lanes);
        } else {
          return null;
        }
      }
      case REACT_LAZY_TYPE: {
        const payload = newChild._payload;
        const init = newChild._init;
        return updateSlot(returnFiber, oldFiber, init(payload), lanes);
      }
    }
    // 如果是 Fragment 或者是可迭代对象
    if (isArray(newChild) || getIteratorFn(newChild)) {
      // ......
      return updateFragment(returnFiber, oldFiber, newChild, lanes, null);
    }
    // ......
  }
  return null;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

复用的策略可以总结如下:

Text Element Portal Lazy Fragment
方法 updateTextNode updateElement updatePortal updateSlot updateFragment

注意这里是严格比对 key 值相等,否则一律返回 null。对于具体的复用细节,后文详述。

# updateFromMap

此函数对 reconcileChildrenArray 中节点池中可复用的节点进行更新复用。具体逻辑与 updateSlot 一致,不在赘述。

# placeChild

此函数对 reconcileChildrenArray 中调和的节点进行置位。

function placeChild(
  newFiber: Fiber,
  lastPlacedIndex: number,
  newIndex: number,
): number {
  // 缓存下标共下次调和使用
  newFiber.index = newIndex;
  const current = newFiber.alternate;
  // ......
  // current 存在说明 current 即是将被复用的原节点
  // 如果非首次渲染,比较原节点与新节点的下标,如果新节点在原节点的后面,标记为 Placement,并返回置位的下标
  // 如果新节点不在原节点的后面,则位置无需变化,保持原位置即可
  if (current !== null) {
    const oldIndex = current.index;
    if (oldIndex < lastPlacedIndex) {
      newFiber.flags |= Placement;
      return lastPlacedIndex;
    } else {
      return oldIndex;
    }
  } else {
    // 没有被复用的节点,添加 Placement 标记
    newFiber.flags |= Placement;
    return lastPlacedIndex;
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

# deleteChild

此函数收集待删除的节点并且对 returnFiber 添加 ChildDeletion 副作用标记。

function deleteChild(returnFiber: Fiber, childToDelete: Fiber): void {
  // 初次渲染不需要 ChildDeletion 标记
  if (!shouldTrackSideEffects) return;
  // 将待删除的节点加入到 returnFiber.deletions 数组中,并且在 returnFiber 上添加 ChildDeletion 标记
  const deletions = returnFiber.deletions;
  if (deletions === null) {
    returnFiber.deletions = [childToDelete];
    returnFiber.flags |= ChildDeletion;
  } else {
    deletions.push(childToDelete);
  }
}
1
2
3
4
5
6
7
8
9
10
11
12

# 扩展

# 函数式组件和类组件在 reconcileChildren 之前做了什么?

本文中只大致介绍了函数式组件和类组件在捕获时调和过程的大致脉络,这个过程最终都会分成 cloneChildFibers 和 reconcileChildren ,而对于其中的细节只是一笔带过,如 renderWithHooks 、 constructClassInstance 、 mountClassInstance 、 updateClassInstance 、 finishClassComponent 等函数,这些逻辑跟相应的组件的关联比较大,与整个的调和过程关系不大,因此这部分内容将在后文组件机制的文章中陆续展开探讨。

# workInProgress Tree 是如何初始化的?

请看下面这个简易的获取冒泡的过程:

image

可以看出,在 WorkLoop 的捕获和冒泡过程中, workInProgress 指针是在不停移动的。在应用挂载时,进入对 workInProgress 指针所指向的节点的 beginWork 过程会调用 reconcileChildren 函数,此函数判断到为挂载(mount)阶段则会在 reconcileChildFibers 中创建 workInProgress Fiber。

# 问题

# 总结

本文介绍了 React 调和过程中冒泡阶段对 workInProgress 节点(换一种角度看也是子节点)进行调和的原理细节,包括子节点的创建和复用、DIFF 算法、副作用标记(Effect Tag)等。从整体上来看,workInProgress 的调和过程与 ReactElement 的抽象层是密不可分的,Fiber 的调和过程依赖于 ReactElement 对应用最新状态的榨取,这些最新的状态最终从内存中被收集起来,成为 Fiber 上可回溯的数据。整个捕获过程除了创建 workInProgress Fiber 之外,还担负着标记副作用的职责。副作用将为 DOM 的更新起到指导作用。

有以下几点还需注意:

  • React DIFF 算法虽然在一定程度上已经做了一些优化,但是由于数据结构本身的限制,还是有一定的可优化的空间。
  • React 的更新粒度是 FiberRoot 为基础,虽然在调和期间有一定的优化措施,比如各种节点复用,甚至是提前跳出捕获过程,但是相对于 Vue3 中基于模板分析的组件粒度的更新机制,还是有一定的性能差距的。
  • React 中的 VDOM 如何理解?这里的 VDOM 不只是 ReactElement,还应该包括 FiberTree。这是因为 ReactElement 对于维持组件状态很重要,但是 React 应用的调度、更新和渲染离不开 FiberTree 的结构。因此可以将 ReactElement 理解为组件状态层面的 VDOM,而把 FiberTree 理解为应用层面上的 VDOM。
  • 本文中已经接触到的副作用包括 Placement 、 ChildDeletion 和 ContentReset 。从理论上来说,DOM 的更新只需要两个副作用,“更新”(重新调整位置,包括插入)、“删除”。在后文中,我们将针对更新的副作用进行更深入的探究。此处 Placement 意即更新节点位置, ChildDeletion 意即删除节点, ContentReset 意即针对可视为文本节点 HostComponent 节点重置其文本内容。

# 参考

  • React 源码剖析系列 - 不可思议的 react diff (opens new window)
编辑 (opens new window)
上次更新: 2022/08/01, 20:37:47
React 源码漂流记:React 调和器核心源码解读(四)
React 源码漂流记:React 调和器核心源码解读(六)

← React 源码漂流记:React 调和器核心源码解读(四) React 源码漂流记:React 调和器核心源码解读(六)→

最近更新
01
渲染原理之组件结构与 JSX 编译
09-07
02
计划跟踪
09-06
03
开始上手
09-06
更多文章>
Theme by Vdoing | Copyright © 2022-2022 Fancy Front End | Made by Jonsam by ❤
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式