# react的diff算法
# 起因
React
的key
相信大家都很了解, 也不用我多说.但是平时对于key
的使用可能没有那么严格, 有可能不给key
, 有可能给index
, 其实一般来说不会出什么问题, 顶多就是性能上会有一些损失, 但是在某些特定的情况下使用不当也可能会导致 bug
, 比如下面这种情况.
# key导致的bug
在一个后台管理系统中, 左侧是一个菜单可以选择不同的选项, 右侧对应了不同的视频.但是左侧菜单切换的时候, 右侧视频的封面图虽然重新加载了, 点击播放后视频的内容并不是新的, 还是上一个选项卡的视频, 看起来就像没有重新渲染一样.而排查了代码之后发现我确实没有给table
去加key
值, 而加上这个key
值之后也确实是好了.我们来看看video
那段代码.
<video controls={true} poster={poster}>
<source src={videoUrl} />
</video>
那么问题来了:
- 如果我不加
key
,react
不是应该把各个菜单的内容理解为完全不一样而重新渲染吗? video
里面的src
确实已经变了, 但是播放的视频还是老视频 一顿搜索之后, 我先找到了第二个问题的答案:video
用这种source
的写法时, 如果只改变src
的话,video
是不会重新去加载视频的, 正确的做法应该是将dom
卸载掉再重新加载, 这样才能正确拉取到对应的视频. 虽然我感觉这很不符合常识, 但结果就是这样的, 在不加key
的情况下, 去掉source
直接在video
中使用src
也能解决这个bug
.像下面这样
<video controls={true} poster={poster} src={videoUrl} />
那么我们回到第一个问题, 我没有给key
值, React
居然不会把他们都认为是不一样的dom
去重新渲染?
# diff算法
key
值有什么用呢?在 react
的 diff
过程中, 如果遇见 key
相同的两个结点, react
会认为这是两个相同的结点, 在下一次渲染中会复用这个结点, 减少渲染的内容, 从而提升性能.那, react
的 diff
算法又是什么呢?
# 传统的diff算法
我没有仔细去研究过完全找出两颗树的改动之处最小的时间复杂度是多少, 根据网上的信息来看, 目前最小的时间复杂度也到了O(n^3)
, 而React
目前的diff
算法时间复杂度为O(n)
, 他们两个都不在一个量级上, 所以 react
的 diff
势必是丢掉了一些东西的.
# react的diff算法
为了降低算法复杂度, React
针对前端开发的习惯做了一些限制:
React
只会对同级元素diff
, 如果一个dom
结点跨越了层级, 那么它是永远不可能被复用的.- 如果
dom
结点的类型发生了改变, 这个结点以及其后代都会被销毁然后重新渲染 react
可以通过key
值来判断哪些结点属于同一个结点
所以通过这三条我们可以总结出要想让我们的页面少一些不必要的渲染, 我们可以:
- 不要轻易改变
dom
层级 - 不要轻易改变
dom
类型 - 利用好
key
# diff实现
说了这么多, 那么我们就一起去看看 react
到底是怎么实现 diff
的, 而 key
在这其中又发挥了怎样的作用
# 入口函数
diff
的入口函数叫 reconcileChildFibers
, 在这之前经历了一系列的调度操作, 最后来到了diff环节, 那在reconcileChildFibers
中 react
又干了什么呢
// returnFiber是我们最后将要渲染的Fiber树
// currentFirstChild是当前的第一个子child
// newChild是将要挂载的所有的子元素
// lanes用于优先级判断
function reconcileChildFibers(returnFiber, currentFirstChild, newChild, lanes) {
// 首先判断newChild是否是Fragment
var isUnkeyedTopLevelFragment = typeof newChild === 'object' && newChild !== null && newChild.type === REACT_FRAGMENT_TYPE && newChild.key === null;
// 如果是Fragment则把里面的内容拿出来
if (isUnkeyedTopLevelFragment) {
newChild = newChild.props.children;
}
// 判断newChild是不是对象
var isObject = typeof newChild === 'object' && newChild !== null;
// 如果是对象则根据不同的类型去处理
if (isObject) {
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE:
return placeSingleChild(reconcileSingleElement(returnFiber, currentFirstChild, newChild, lanes));
case REACT_PORTAL_TYPE:
return placeSingleChild(reconcileSinglePortal(returnFiber, currentFirstChild, newChild, lanes));
}
}
// 判断是否是文本类型
if (typeof newChild === 'string' || typeof newChild === 'number') {
return placeSingleChild(reconcileSingleTextNode(returnFiber, currentFirstChild, '' + newChild, lanes));
}
// 判断是否是数组
if (isArray$1(newChild)) {
return reconcileChildrenArray(returnFiber, currentFirstChild, newChild, lanes);
}
// 判断是否可迭代
if (getIteratorFn(newChild)) {
return reconcileChildrenIterator(returnFiber, currentFirstChild, newChild, lanes);
}
// 后面都是一些错误处理
if (isObject) {
throwOnInvalidObjectType(returnFiber, newChild);
}
{
if (typeof newChild === 'function') {
warnOnFunctionType(returnFiber);
}
}
if (typeof newChild === 'undefined' && !isUnkeyedTopLevelFragment) {
// If the new child is undefined, and the return fiber is a composite
// component, throw an error. If Fiber return types are disabled,
// we already threw above.
switch (returnFiber.tag) {
case ClassComponent:
{
{
var instance = returnFiber.stateNode;
if (instance.render._isMockFunction) {
// We allow auto-mocks to proceed as if they're returning null.
break;
}
}
}
// Intentionally fall through to the next case, which handles both
// functions and classes
// eslint-disable-next-lined no-fallthrough
case Block:
case FunctionComponent:
case ForwardRef:
case SimpleMemoComponent:
{
{
{
throw Error((getComponentName(returnFiber.type) || 'Component') + "(...): Nothing was returned from render. This usually means a return statement is missing. Or, to render nothing, return null.");
}
}
}
}
} // Remaining cases are all treated as empty.
return deleteRemainingChildren(returnFiber, currentFirstChild);
}
可以看到, 这个入口函数相对于一个路由文件, 将不同的类型分派到不同的子函数去处理.那么 React
会如何处理各种类型的结点了, 我们接着看.
# 同级单个结点diff
如果同级只有一个结点, 那么会进入 reconcileSingleElement
中去处理, 我们来看看这里面做了什么操作.
function reconcileSingleElement(returnFiber, currentFirstChild, element, lanes) {
var key = element.key;
var child = currentFirstChild;
// 首先会判断之前有没有这个结点
while (child !== null) {
// 有老结点的情况下判断key
if (child.key === key) {
// 根据tag类型来处理
switch (child.tag) {
case Fragment:
{
if (element.type === REACT_FRAGMENT_TYPE) {
deleteRemainingChildren(returnFiber, child.sibling);
var existing = useFiber(child, element.props.children);
existing.return = returnFiber;
{
existing._debugSource = element._source;
existing._debugOwner = element._owner;
}
return existing;
}
break;
}
case Block:
// We intentionally fallthrough here if enableBlocksAPI is not on.
// eslint-disable-next-lined no-fallthrough
default:
{
if (child.elementType === element.type || ( // Keep this check inline so it only runs on the false path:
isCompatibleFamilyForHotReloading(child, element))) {
deleteRemainingChildren(returnFiber, child.sibling);
// 复用当前结点
var _existing3 = useFiber(child, element.props);
_existing3.ref = coerceRef(returnFiber, child, element);
_existing3.return = returnFiber;
{
_existing3._debugSource = element._source;
_existing3._debugOwner = element._owner;
}
return _existing3;
}
break;
}
} // Didn't match.
deleteRemainingChildren(returnFiber, child);
break;
} else {
deleteChild(returnFiber, child);
}
child = child.sibling;
}
// 没有老结点只能创建新结点
if (element.type === REACT_FRAGMENT_TYPE) {
var created = createFiberFromFragment(element.props.children, returnFiber.mode, lanes, element.key);
created.return = returnFiber;
return created;
} else {
var _created4 = createFiberFromElement(element, returnFiber.mode, lanes);
_created4.ref = coerceRef(returnFiber, currentFirstChild, element);
_created4.return = returnFiber;
return _created4;
}
}
可以看到, 首先会判断之前是否存在这个结点, 如果之前都不存在, 那复用更谈不上了, 所以会直接新建一个插入.
而如果存在老结点, 下面会判断 key
和 type
是否相同.注意, 在 key
相同之后, 只有 type
也相同的情况下 react
才会去复用这个结点.而如果判断出 key
或者 type
不同, react
会删掉这个结点, 去新建一个结点并返回.
# 同级多个结点diff
同级多个结点的diff
就比单个结点要复杂多了, 它会进入reconcileChildrenArray
中去处理, 我们慢慢来看.
function reconcileChildrenArray(returnFiber, currentFirstChild, newChildren, lanes) {
{
// 判断key, 如果这里出现相同的key会给一个warning
var knownKeys = null;
for (var i = 0; i < newChildren.length; i++) {
var child = newChildren[i];
knownKeys = warnOnInvalidKey(child, knownKeys, returnFiber);
}
}
// diff后的结果
var resultingFirstChild = null;
// 新fiber树的上一个fiberNode
var previousNewFiber = null;
// 老fiber树的fiberNode, 也就是未更新之前
var oldFiber = currentFirstChild;
// 当前可复用的结点的index
var lastPlacedIndex = 0;
// 新child的index
var newIdx = 0;
// 下一个老的fiberNode
var nextOldFiber = null;
// 这里用newIndex来遍历
for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
// 没想到有什么情况会进第一个分支
if (oldFiber.index > newIdx) {
nextOldFiber = oldFiber;
oldFiber = null;
} else {
// sibling是fiberNode的兄弟结点, 所以用nextOldFiber来存储下一个需要比较的fiberNode
nextOldFiber = oldFiber.sibling;
}
// 这里通过比较oldFiber和newFiber来拿到一个新的fiberNode对象
// 这个对象可能是复用的、新建的、也可能为null
var newFiber = updateSlot(returnFiber, oldFiber, newChildren[newIdx], lanes);
// 如果为null的情况下直接break,跳出循环
if (newFiber === null) {
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.
deleteChild(returnFiber, oldFiber);
}
}
// 记录下当前可复用结点的index
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
// 是否是最开始的结点
if (previousNewFiber === null) {
resultingFirstChild = newFiber;
} else {
// 设置兄弟结点为newFiber
previousNewFiber.sibling = newFiber;
}
// 链表移动, pre赋值为当前node
previousNewFiber = newFiber;
// 将old赋值为下一个需要比较的对象
oldFiber = nextOldFiber;
}
// 如果是newIdx为length跳出的循环, 证明newChild已经遍历完了
// 那么此时剩下的oldFiber一定是被删除的结点, 直接删除
if (newIdx === newChildren.length) {
deleteRemainingChildren(returnFiber, oldFiber);
return resultingFirstChild;
}
// 如果oldFiber为null了, 那么证明老的fiber已经遍历完了
// 那么剩下的newChild都是新增的结点, 直接插入
if (oldFiber === null) {
for (; newIdx < newChildren.length; newIdx++) {
var _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;
}
// 如果是中途break, 证明出现了结点变化
// 此时收集未遍历的oldFiber, 以key作为键值, map结构存储
var existingChildren = mapRemainingChildren(returnFiber, oldFiber); // Keep scanning and use the map to restore deleted items as moves.
// 遍历newChild, 用key去existingChildren中寻找对应的结点
for (; newIdx < newChildren.length; newIdx++) {
var _newFiber2 = updateFromMap(existingChildren, returnFiber, newIdx, newChildren[newIdx], lanes);
if (_newFiber2 !== null) {
if (shouldTrackSideEffects) {
if (_newFiber2.alternate !== null) {
existingChildren.delete(_newFiber2.key === null ? newIdx : _newFiber2.key);
}
}
// 找到了之后给effectTag赋值
lastPlacedIndex = placeChild(_newFiber2, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
resultingFirstChild = _newFiber2;
} else {
previousNewFiber.sibling = _newFiber2;
}
previousNewFiber = _newFiber2;
}
}
if (shouldTrackSideEffects) {
// Any existing children that weren't consumed above were deleted. We need
// to add them to the deletion list.
existingChildren.forEach(function (child) {
return deleteChild(returnFiber, child);
});
}
// 返回最后的结果
return resultingFirstChild;
}
首先, 一进来, react
就会检查一遍各个子结点上的 key
值.在warnOnInvalidKey
中, 用了 Set
来存储 key
值, 如果发现了两个一样的值, 就会抛出我们平常见的挺多的一个错误了:
error('Encountered two children with the same key, `%s`. ' + 'Keys should be unique so that components maintain their identity ' + 'across updates. Non-unique keys may cause children to be ' + 'duplicated and/or omitted — the behavior is unsupported and ' + 'could change in a future version.', key);
后面, 就是真正的 diff
逻辑了, 我先给大家解释一下react
是如何diff
的.
- 遍历新老
child
并进行对比, 如果key
,type
全都能对上, 那最好了, 这些结点就能完全复用,diff
结束 - 中途某个结点被改变了, 导致遍历中断, 这时候
react
会用Map
以key
为键去收集oldChildren
(即老结点)中没有被遍历到的结点, 随后遍历剩余新结点.这个时候key
就发挥了作用, 遍历新结点的过程中, 能不能复用老结点就是通过能不能找到key
来判断的, 如果在map
中找到了, 那么就复用, 如果找不到, 那么就新建. oldChild
被遍历完了,newChild
中还有结点, 那么证明这些结点是被插入的, 直接插入.newChild
被遍历完了,oldChild
还有结点, 那么证明这些结点是被删除的, 直接删除. 下面我们结合代码来看:
// 这里用newIndex来遍历
for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
// 没想到有什么情况会进第一个分支
if (oldFiber.index > newIdx) {
nextOldFiber = oldFiber;
oldFiber = null;
} else {
// sibling是fiberNode的兄弟结点, 所以用nextOldFiber来存储下一个需要比较的fiberNode
nextOldFiber = oldFiber.sibling;
}
// 这里通过比较oldFiber和newFiber来拿到一个新的fiberNode对象
// 这个对象可能是复用的、新建的、也可能为null
var newFiber = updateSlot(returnFiber, oldFiber, newChildren[newIdx], lanes);
// 如果为null的情况下直接break,跳出循环
if (newFiber === null) {
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.
deleteChild(returnFiber, oldFiber);
}
}
// 记录下当前可复用结点的index
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
// 是否是最开始的结点
if (previousNewFiber === null) {
resultingFirstChild = newFiber;
} else {
// 设置兄弟结点为newFiber
previousNewFiber.sibling = newFiber;
}
// 链表移动, pre赋值为当前node
previousNewFiber = newFiber;
// 将old赋值为下一个需要比较的对象
oldFiber = nextOldFiber;
}
这里是 newIndex++
, 所以是在 for
循环遍历 newChild
, 首先它会比较 oldFiber
的 index
和 newIndex
的大小, 因为他们都是从头开始遍历的, 所以正常情况下他们肯定是相等的, 也就是会走到 else
分支, 让 nextOldFiber
等于 oldFiber
的兄弟结点, 也就是当前结点的相邻结点.然后 newFiber
是通过updateSlot
得到的, 我们进去这个函数里面看看
function updateSlot(returnFiber, oldFiber, newChild, lanes) {
// 拿到key值, 没有则为null
var key = oldFiber !== null ? oldFiber.key : null;
if (typeof newChild === 'string' || typeof newChild === 'number') {
if (key !== null) {
return null;
}
return updateTextNode(returnFiber, oldFiber, '' + newChild, lanes);
}
if (typeof newChild === 'object' && newChild !== null) {
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE:
{
// 都没有给key都为null, 则也是相等, 可以复用
if (newChild.key === key) {
if (newChild.type === REACT_FRAGMENT_TYPE) {
return updateFragment(returnFiber, oldFiber, newChild.props.children, lanes, key);
}
// 得到更新后的Element
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;
}
}
}
if (isArray$1(newChild) || getIteratorFn(newChild)) {
if (key !== null) {
return null;
}
return updateFragment(returnFiber, oldFiber, newChild, lanes, null);
}
throwOnInvalidObjectType(returnFiber, newChild);
}
{
if (typeof newChild === 'function') {
warnOnFunctionType(returnFiber);
}
}
return null;
}
// 从这里面可以看到, 只有 key 相同的情况下, 才会返回一个 fiber, 否则会返回 null.我们先来看 key 相同的情况下, `updateElement`做了什么
function updateElement(returnFiber, current, element, lanes) {
// 判断是否有老结点
if (current !== null) {
// 只有type相同才能复用
if (current.elementType === element.type || (
isCompatibleFamilyForHotReloading(current, element))) {x
var existing = useFiber(current, element.props);
existing.ref = coerceRef(returnFiber, current, element);
existing.return = returnFiber;
{
existing._debugSource = element._source;
existing._debugOwner = element._owner;
}
return existing;
}
}
// 没有老结点或者type不同直接创建一个新的结点
var created = createFiberFromElement(element, returnFiber.mode, lanes);
created.ref = coerceRef(returnFiber, current, element);
created.return = returnFiber;
return created;
}
可以看到, 如果有老结点, 并且他们的 type
相同, 会复用该结点, 反之则会创建一个新结点返回.
回到我们之前的循环中, 如果新老结点的 key
相同, 那么才会返回一个 fiber
结点.而如果我们不给 key
呢?如果两个都不给 key
的话, 其实就是 null === null
, 最后的结果还是 true
, 所以也会复用.而如果 key
不一样, 那么返回了 null
, 我们的 for
循环就会 break
了, 进入下一个逻辑, 这里我们后面再说.
在拿到新的 fiberNode
之后, 此时会进入一个placeChild
函数.他的作用主要是判断各个结点的更新类型并返回最后一个可复用结点的位置.
function placeChild(newFiber, lastPlacedIndex, newIndex) {
//lastPlacedIndex初始值为0
newFiber.index = newIndex;
if (!shouldTrackSideEffects) {.
return lastPlacedIndex;
}
var current = newFiber.alternate;
// 判断当前有没有结点
if (current !== null) {
var oldIndex = current.index;
// 如果oldIndex小于最近可复用结点的index, 那么它需要右移
if (oldIndex < lastPlacedIndex) {
// Placement为一个常量, react用于判断更改类型
newFiber.flags = Placement;
return lastPlacedIndex;
} else {
// 如果它比最近可复用结点大, 那么不需要改动, 同时把lastPlacedIndex设置为该值
return oldIndex;
}
} else {
//这个fiber是新建的
newFiber.flags = Placement;
return lastPlacedIndex;
}
}
这个地方有点绕, 用大白话描述就是react
会找到一批相对位置不变的节点, 保留他们的位置而移动其他节点. 我们用个简单的例子来理解, 现在有 abcd
四个数, 我们要把它变成 dabc
.如果我们肉眼来看, 那很显然是把 d
移动到最前面, 但是 react
不是这样干的, react
是这样干的.
- 遍历
dabc
, 拿到第一个d
,d
在abcd
中为第四个数, 所以index=3
, 比lastPlacedIndex=0
大, 位置保持不变, 并修改lastPlacedIndex
为3
- 遍历
dabc
, 拿到第二个a
,a
在abcd
中为第一个数, 所以index=0
, 小于lastPlacedIndex=3
, 为了保保证顺序, 需要把a
右移到d
后面去 - 遍历
dabc
, 拿到第三个b
,b
在abcd
中为第二个数, 所以index=1
, 小于lastPlacedIndex=3
, 为了保证顺序, 需要把b
右移到d
后面去 - 遍历
dabc
, 拿到第三个c
,c
在abcd
中为第一个数, 所以index=2
, 小于lastPlacedIndex=3
, 为了保证顺序, 需要把c
右移到d
后面去 所以react
其实是把abc
移动到了d
后面去, 当然换一种方法也是可以的, 但是对于react
来说, 不可能根据实际情况去判断如何移动, 所以只会采取一种固定的方法来进行修改.
我们接着看后面的代码, 如果newIndex
等于newChild
的长度了, 那么证明我们已经把newChild
遍历完了.但是此时oldChild
还有剩余, 那就证明我们这次操作删除了某些结点, 所以直接把剩下的结点删掉.
而如果oldChild
已经遍历完了, 但是newChild
还有, 那么证明我们添加了结点, 此时我们直接添加剩余结点即可.
但是如果我们中途退出了循环, 则证明我们修改了中途的结点, 此时会通过一个mapRemainingChildren
函数去收集没有遍历完的老结点, 用Map
以key
作为键去存储, 如果没有key
则用index
当key
.
function mapRemainingChildren(returnFiber, currentFirstChild) {
// 新建一个map
var existingChildren = new Map();
// 拿到第一个fiber
var existingChild = currentFirstChild;
// 遍历
while (existingChild !== null) {
if (existingChild.key !== null) {
// 有key则用key
existingChildren.set(existingChild.key, existingChild);
} else {
// 没有则用index
existingChildren.set(existingChild.index, existingChild);
}
// 获取兄弟fiber
existingChild = existingChild.sibling;
}
return existingChildren;
}
随后再次循环剩下的newChild
, 去找其中对应的结点, 并通过placeChild
函数标记我们需要做的操作.
for (; newIdx < newChildren.length; newIdx++) {
// 这是根据之前的map找到的结点
var _newFiber2 = updateFromMap(existingChildren, returnFiber, newIdx, newChildren[newIdx], lanes);
if (_newFiber2 !== null) {
if (shouldTrackSideEffects) {
if (_newFiber2.alternate !== null) {
existingChildren.delete(_newFiber2.key === null ? newIdx : _newFiber2.key);
}
}
// 标记结点修改类型
lastPlacedIndex = placeChild(_newFiber2, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
resultingFirstChild = _newFiber2;
} else {
previousNewFiber.sibling = _newFiber2;
}
// 链表移动
previousNewFiber = _newFiber2;
}
}
这里有一个updateFromMap
, 这里主要就是从之前的map
中利用key
来匹配对应的结点, 如果匹配到了则可以复用, 匹配不到就创建一个新的结点.
# 总结
其实, 最重要的几个点就是:
key
值不给的情况下,react
会利用index
来做判断, 并不会粗暴的舍弃所有dom
- 只有
key
和type
都相同的情况下,react
才会去复用结点 - 改变
list
顺序时,react
是通过从上往下移的顺序去改变的, 所以我们尽可能少把后面的结点移动到前面, 因为这其实会导致该结点前面的结点全部移动到后面, 并不是单纯的把这一个结点提到前面.
最后, 我们的整个diff
过程就走完了.我们再次回到最初的问题, 如果我不给key
值, react
不会把他们完全扔掉再构造吗?答案是不会.react
会用index
当作key
值, 而此时结点类型也没有改变, 所以react
会复用该结点, 只是改变了他的props
, 也就导致了video没
有取得正确的视频源.最后我们再来总结一般整个流程:
- 进入入口函数以后根据不同的类型去处理
- 如果是
element
那么直接就可以更新了, 此时会根据key
和type
判断是否可以复用(都不传key
,key
也是相等) - 如果是数组则进行特殊处理
- 首先循环一对一比较
oldFiber
和newChild
, 如果完全一样那么都可以复用, 顺利结束 - 如果
oldFiber
遍历完了newChild
还有, 那么是新加了结点, 直接把剩下的结点插入 - 如果
newChild
遍历完了oldFiber
还有, 那么是删除了结点, 直接把剩下的结点删除 - 如果是中途退出了循环, 那么证明有结点被改变了, 此时收集剩下的
oldFiber
, 用Map
存储, 以key
为键值, 如果没有key
则用index
- 遍历剩下的
newChild
, 在map
中找对应的可复用结点, 找到了之后比较type
, 相同则能复用.如果找不到或者type
不同, 则创建新的结点 - 最后返回
diff
后的fiber
这里我们只梳理了核心流程, 你可以注意到源码中其实还有一些其他类型的比较和一些优化措施, 这些就留给大家自己摸索了.