最近在学习慕课网 手写 React 高质量源码迈向高阶开发,之前自己也尝试看过源码,不过最终放弃了
放弃的最主要原因是 react
内部的调用链太长了,每天在缕清调用链上都花了不少时间,createRoot
都没有看完
最近看到慕课网有一个 react
源码课,就想着跟着课程然后在自己源码,看看这次能够看到什么地步
它这个课程前八章 是 react@16
的源码,从第九章开始才是 react@18
的源码
React
源码系列:
react@16
- 第 1 篇:createElement 和 render 函数实现原理
- 第 2 篇:函数组件和类组件及 ref 和 setState 的实现
- 第 3 篇:优化渲染过程之
dom diff
- 第 4 篇:类组件增强——生命周期函数
- 第 5 篇:性能优化—— PureComponent 和 memo
- 第 6 篇:hooks——useEffect、useState、useMemo 等源码实现
react@18
- 第 1 篇:begionWork 前的准备工作:jsxDEV,createRoot,render 源码实现
- 第 2 篇:beginWork 工作原理
- 第 3 篇:4 张图带你看懂 beginWork 和 completeWork 工作过程
- 第 4 篇:一张图看懂 React 合成事件原理
- 第 5 篇:commitWork 工作原理
- 第 6 篇:Fiber 架构的 DOM Diff 原理
- 第 7 篇:图解 useState 原理
- 第 8 篇:useEffect 和 useLayoutEffect 源码实现
问题存在
我们在 setState
时,会触发试图的更新
之前的实现是将整个 dom
树替换掉,这里的 oldDOM
是函数组件或者类组件 render
出来的 dom
树,这里是整体替换
// 这里的 oldDOM 是函数组件或者类组件 render 出来的 dom 树
function updateDomTree(oldDOM, newVNode) {
let parentNode = oldDOM.parentNode;
parentNode.removeChild(oldDOM);
let newDOM = createDOM(newVNode);
parentNode.appendChild(newDOM);
}
这样做,性能是比较差的,因为我们只是改变了一小部分,但是却要替换整个 dom
树
这里我们要做的是实现 diff
算法,只更新改变的部分
我们知道 react
在渲染时分为三个步骤
- 生成虚拟
DOM
- 将虚拟
DOM
转换为真实DOM
- 将真实
DOM
挂载到页面上
不管是初始化渲染,还是更新渲染,都会经过这三个步骤
初始化渲染时,是 0 - 1
的过程,所有节点都要渲染,这里没有什么可以优化的地方
但是在更新时,更新的部分只是一小块,没有必要重新渲染整个 dom
树,所以这里就有优化空间了
所以就有了 diff
算法,diff
算法的目的就是找出两个对象的差异
diff 算法
diff
在没有 key
的情况下比较,如下结构:
// 变化前
<div className="a">a</div>
<spn className="b">b</spn>
<p className="c">c</p>
<img className="d"/>
// 变化后
<img className="d"/>
<div className="b" style={{color: "red"}}>b</div>
<div className="a">a</div>
变化前的 div.a
不知道和变化后的哪个 div
进行比较,只能按照顺序进行比较,也就是说 div.a
和 img.d
进行比较
发现 div.a
和 img.d
不一样,那就删除 div.a
,并创建 img.d
的标签,这是比较低效的
因为 div.a
和 img.d
在原有的虚拟 DOM
上是存在的,而现在需要重新创建
变化前的 div.b
和变化后的 div.b
进行比较,发现变化后的 div.b
多了一个 style
属性,直接更新属性就行了
基于这种情况,我们可以得出结论:按照顺序比较,如果不一样,就删除原来的,创建新的
这种比较方式相比我们直接替换整个 dom
树,性能是有提升的,但是 react
并没有满足于此
它引入了 key
,通过 key
来找到变化前后的节点,这样就不需要按照顺序比较了,拿到变化前的 key
找到变化后的 key
,这两个直接进行比较
代码优化
在开始 diff
算法之前,先把之前写的代码优化一下
之前我们在处理文本时,直接使用 document.createTextNode
,后面处理文本节点的地方有点多,需要将它统一一下,方便后续处理
首先我们创建一个 toVNode
函数,用来处理文本节点
const toVNode = (node) => {
// 如何 node 是 string 或者 number,就认为是文本节点
// 返回一个对象,对象中有 type 和 props 属性
// 否则返回 node
return typeof node === "string" || typeof node === "number"
? { type: REACT_TEXT, props: { text: node } }
: node;
};
在 createDOM
中,我们增加一个文本节点的判断
if (type === REACT_TEXT) {
dom = document.createTextNode(props.text);
}
在 createElement
中,对 children
处理时,先进行判断一下,如果是文本节点,返回一个文本节点对象
if (arguments.length > 3) {
// 将 children 中的每一项都使用 toVNode 方法进行处理
props.children = Array.prototype.slice.call(arguments, 2).map(toVNode);
} else {
// 对 children 使用 toVNode 方法进行处理
props.children = toVNode(children);
}
之前在 mountArray
中和 createDOM
中处理文本节点的地方都可以将它删除
在 mountArray
中需要为 children
增加一个 index
属性,方便后面进行 diff
比较
function mountArray(children, parent) {
// ...
for (let i = 0; i < children.length; i++) {
// 为每一个子元素添加 index 属性
children[i].index = i;
// ...
}
}
updateTree
diff
算法从 updateTree
开始
我们如何知道现在是要删除、新增、还是修改呢?
我们可以通过 oldVNode
和 newVNode
的来进行判断
但是之前 updateTree
只接收两个参数:oldDOM
和 newVNode
所以现在要改成三个参数:oldVNode
、newVNode
、oldDOM
更新有这五种情况:
- 新节点,旧节点都不存在
- 新节点存在,旧节点不存在
- 新节点不存在,旧节点存在
- 新节点存在,旧节点也存在,但是类型不一样
- 新节点存在,旧节点也存在,类型一样 --> 我们需要进行深入的比较
基于这五种情况,可以使用一个对象来表示,最后一种情况可以认为是默认情况
const typeMap = {
// 不需要进行任何操作
NO_OPERATE: !oldVNode && !newVNode,
// 新增节点
ADD: !oldVNode && newVNode,
// 删除节点
DELETE: oldVNode && !newVNode,
// 替换节点
REPLACE: oldVNode && newVNode && oldVNode.type !== newVNode.type,
};
如何知道当前是哪种情况呢?
使用 filter
方法,过滤出是 true
的结果,然后取第一个
const UPDATE_TYPE = Object.keys(typeMap).filter((key) => typeMap[key])[0];
使用 switch
语法根据不同的情况进行不同的操作:
NO_OPERATE
:不需要进行任何操作- 因为
oldVNode
和newVNode
都不存在
- 因为
DELETE
:删除节点- 因为
oldVNode
存在,newVNode
不存在,删除节点交给removeVNode
函数处理
- 因为
ADD
:新增节点- 因为
oldVNode
不存在,newVNode
存在 - 直接调用
createDOM
创建新节点,并添加到父节点中
- 因为
REPLACE
:替换节点- 因为
oldVNode
和newVNode
都存在,但是类型不一样 - 先删除旧节点,再创建新节点,添加到父节点中
- 删除节点,直接调用
removeVNode
函数
- 删除节点,直接调用
- 因为
- 默认情况:新旧节点都一样,且类型也相同
- 这种情况下,我们需要对它内部的属性,子元素进行比较,是
DOM Diff
的核心,我们将它放到deepDOMDiff
函数讲解
- 这种情况下,我们需要对它内部的属性,子元素进行比较,是
switch (UPDATE_TYPE) {
case "NO_OPERATE":
break;
case "DELETE":
removeVNode(oldVNode);
break;
case "ADD":
oldDOM.parentNode.appendChild(createDOM(newVNode));
break;
case "REPLACE":
removeVNode(oldVNode);
oldDOM.parentNode.appendChild(createDOM(newVNode));
break;
default:
// 深度的 dom diff,新老虚拟 DOM 都存在且类型相同
deepDOMDiff(oldVNode, newVNode);
break;
}
removeVNode
函数比较简单,直接调用 findDOMByVNode
找到 dom
节点,然后删除即可
function removeVNode(VNode) {
// 找到 dom 节点
const currentDOM = findDOMByVNode(VNode);
// 删除 dom 节点
if (currentDOM) currentDOM.remove();
}
deepDOMDiff
deepDOMDiff
函数是 DOM Diff
的核心
它是在新老节点都一样的情况下,对其内部的属性、子元素进行比较
所以它有这 4
种情况:
- 原生节点,比如
div
、span
、p
、img
等 - 函数组件
- 类组件
- 文本节点
let diffTypeMap = {
// 原生节点,type 是一个字符串
ORIGIN_NODE: typeof oldVNode.type === "string",
// 类组件,type 是一个函数,但是有 IS_CLASS_COMPONENT 属性
CLASS_COMPONENT:
typeof oldVNode === "function" && oldVNode.type.IS_CLASS_COMPONENT,
// 函数组件,type 是一个函数
FUNCTION_COMPONENT: typeof oldVNode === "function",
// 文本节点,type 是一个字符串,值是 REACT_TEXT
TEXT: oldVNode.type === REACT_TEXT,
};
然后通过 filter
方法过滤出是 true
的结果,取第一个
因为上面的 4
种情况是互斥的,所以只会有一个结果
const DIFF_TYPE = Object.keys(diffTypeMap).filter((key) => diffTypeMap[key])[0];
通过 switch
判断不同的情况,然后进行不同的操作:
ORIGIN_NODE
:原生节点CLASS_COMPONENT
:类组件FUNCTION_COMPONENT
:函数组件TEXT
:文本节点
文本节点
文件节点更新比较简单
首先通过 findDOMByVNode
找到 oldVNode
所对应的 oldDOM
,然后将 oldDOM
赋值给 newVNode
的 dom
属性
然后直接更新 newVNode.dom.textContent
,新的文本在 newVNode.props.text
// 找到 oldDOM 节点
// 赋值给 newVNode.dom
newVNode.dom = findDOMByVNode(oldVNode);
// 更新文本节点的值
newVNode.dom.textContent = newVNode.props.text;
函数组件
首先需要对 getDomByFunctionComponent
做一些小改动
需要在 VNode
上挂载一个属性:oldRenderVNode
旧的渲染树
function getDomByFunctionComponent(VNode) {
let renderVNode = type(props);
// 将 renderVNode 挂载到 VNode.oldRenderVNode 上
VNode.oldRenderVNode = renderVNode;
// ...
}
然后通过findDOMByVNode
找到oldVNode
所对应的oldDOM
,然后将 oldDOM
赋值给newVNode
的dom
属性
这一步和文本节点一样
然后调用新的 VNode
获取到新的渲染树,再次调用 updateDomTree
更新 DOM
(递归)
最后将新的 DOM
赋值给 newVNode.oldRenderVNode
function updateFunctionComponent(oldVNode, newVNode) {
// 找到 oldDOM 节点
// 赋值给 newVNode.dom
let oldDOM = (newVNode.dom = findDOMByVNode(oldVNode));
if (!oldDOM) return;
// 从 newVNode 中获取 type、props
const { type, props } = newVNode;
// 调用 type 函数,传入 props 获取新的 DOM 结构
let newRenderVNode = type(props);
// 递归调用 updateDomTree,更新整个 DOM 树
updateDomTree(oldVNode.oldRenderVNode, newRenderVNode, oldDOM);
// 将 newRenderVNode 赋值给 newVNode.oldRenderVNode
newVNode.oldRenderVNode = newRenderVNode;
}
类组件
类组件也是,需要对 getDomByClassComponent
做一些小改动
需要在 VNode
上挂载一个属性:classInstance
类组件实例
function getDomByClassComponent(VNode) {
let instance = new type(props);
// 将 instance 挂载到 VNode.classInstance 上
VNode.classInstance = instance;
// ...
}
类组件更新比较简单,我们获取到旧的组件实例后,将这个实例赋值给 newVNode.classInstance
然后调用实例上的 launchUpdate
方法,忘记的可以查看这篇文章:React 源码:函数组件和类组件及 ref 和 setState 的实现#立即更新预处理
launchUpdate
内部会进行 state
合并,并更新视图
function updateClassComponent(oldVNode, newVNode) {
// 对于当前界面,旧的实例是与页面上已渲染的组件是相对应的,在生命周期函数中,会尝试比较 newProps 和 oldProps
const classInstance = (newVNode.classInstance = oldVNode.classInstance);
classInstance.updater.launchUpdate();
}
不过这里为什么要舍去 newVNode
上的实例,转而使用 oldVNode
上的实例呢?
因为这是为之后的生命周期函数做考虑,
对于当前界面,旧的实例是与页面上已渲染的组件是相对应的,在生命周期函数中,会尝试比较 newProps
和 oldProps
这个在之后的生命周期函数章节中会提到,这里先暂时不表
原生节点
原生节点的更新也是 DOM Diff
的核心,也是最复杂的
其主要核心是 updateChildren
函数,这个函数我们会在下面讲解
首先我们需要找到 oldVNode
所对应的 oldDOM
,然后将 oldDOM
赋值给 newVNode
的 dom
属性
然后调用 setPropsForDOM
更新 DOM
属性
最后调用 updateChildren
更新子元素,传入三个参数
currentDOM
:DOM
,oldVNode
对应的真实DOM
oldVNode.props.children
:旧的虚拟DOM
的子节点newVNode.props.children
:新的虚拟DOM
的子节点
let currentDOM = (newVNode.dom = findDOMByVNode(oldVNode));
setPropsForDOM(currentDOM, newVNode.props);
updateChildren(currentDOM, oldVNode.props.children, newVNode.props.children);
最终代码
switch (DIFF_TYPE) {
case "ORIGIN_NODE":
let currentDOM = (newVNode.dom = findDOMByVNode(oldVNode));
setPropsForDOM(currentDOM, newVNode.props);
updateChildren(
currentDOM,
oldVNode.props.children,
newVNode.props.children
);
break;
case "CLASS_COMPONENT":
updateClassComponent(oldVNode, newVNode);
break;
case "FUNCTION_COMPONENT":
updateFunctionComponent(oldVNode, newVNode);
break;
case "TEXT":
// 找到 oldDOM 节点
// 赋值给 newVNode.dom
newVNode.dom = findDOMByVNode(oldVNode);
// 更新文本节点的值
newVNode.dom.textContent = newVNode.props.text;
break;
default:
break;
}
updateChildren
updateChildren
函数是 DOM Diff
的核心,也是最复杂的
为什么它是最复杂的呢?
无论是函数组件还是类组件,最终都会渲染成原生节点
updateChildren
函数接受三个参数:
currentDOM
:DOM
,oldVNode
对应的真实DOM
oldVNode.props.children
:旧的虚拟DOM
的子节点newVNode.props.children
:新的虚拟DOM
的子节点
首先处理 children
,将 oldVNodeChildren
和 newVNodeChildren
的 children
都处理成数组,并过滤掉空的 children
// 将 oldVNodeChildren 的 chidlren 处理成数组
oldVNodeChildren = (
Array.isArray(oldVNodeChildren) ? oldVNodeChildren : [oldVNodeChildren]
).filter(Boolean);
// 将 newVNodeChildren 的 chidlren 处理成数组
newVNodeChildren = (
Array.isArray(newVNodeChildren) ? newVNodeChildren : [newVNodeChildren]
).filter(Boolean);
然后将 oldVNodeChildren
中的每一项 children
和 oldVNode
做一个映射:
- 如果
oldVNode
有key
,就使用key
- 如果
oldVNode
没有key
,就使用index
// 保存 oldVNodeChildren 中 key 和 children 的映射关系
let oldKeyChildMap = {};
oldVNodeChildren.forEach((oldVNode, index) => {
// 如果没有 key,就使用 index
let oldKey = oldVNode && oldVNode.key ? oldVNode.key : index;
// 保存 key 和 children 的映射关系
oldKeyChildMap[oldKey] = oldVNode;
});
在这些工作准备好之后,我们应该怎么处理 newVNodeChildren
呢?
通过遍历新的 newVNodeChildren
数组,完成以下四件事:
- 可以复用但需要移动的节点
- 需要重新创建的节点
- 需要删除的节点
- 可以复用且不用移动的节点
将需要移动,创建,删除的节点找出来
我们声明一个 actions
数组用来保存遍历 newVNodeChildren
每项时 children
的状态,在这里 children
只有 MOVE
和 CREATE
两种状态
比如将 ABCDE
变为 CBEFA
// oldVNodeChildren
<ul>
<li key="A">A</li>
<li key="B">B</li>
<li key="C">C</li>
<li key="E">D</li>
<li key="D">E</li>
</ul>
🔽
// newVNodeChildren
CBEFA;
<ul>
<li key="C">C</li>
<li key="B">B</li>
<li key="E">E</li>
<li key="F">F</li>
<li key="A">A</li>
</ul>
我们要知道的一点是,这里的遍历是遍历 newVNodeChildren
,拿到 newVNode
,然后跟 oldVNodeChildren
中的 oldVNode
进行比较
我们开始遍历 newVNodeChildren
:
- 第一次遍历时:
- 拿到的
newVNode
是C
,同时lastNotChangedIndex
为-1
,所以C.key
是C
newVNode C
存在于oldKeyChildMap
所以可以拿到oldVNode
oldVNode.index
也就是C.index
为2
oldVNode.index < lastNotChangedIndex
也就是C.index < -1
=>2 < -1
不成立- 所以
C
节点不需要动,lastNotChangedIndex
取C.index
和lastNotChangedIndex
的最大值,结果为2
- 将
C
节点从oldKeyChildMap
中删除
- 拿到的
- 第二次遍历时:
- 拿到的
newVNode
是B
,同时lastNotChangedIndex
为2
,所以B.key
是B
newVNode B
存在于oldKeyChildMap
所以可以拿到oldVNode
oldVNode.index
就是B.index
为1
oldVNode.index < lastNotChangedIndex
也就是B.index < 2
=>1 < 2
成立- 所以
B
节点需要移动,lastNotChangedIndex
取B.index
和lastNotChangedIndex
的最大值,结果不变,还是为2
action
为{type: MOVE, oldVNode: B, newVNode: B, index: 1}
- 将
B
节点从oldKeyChildMap
中删除
- 拿到的
- 第三次遍历时:
- 拿到的
newVNode
是E
,同时lastNotChangedIndex
为2
,所以E.key
是E
newVNode E
存在于oldKeyChildMap
所以可以拿到oldVNode
oldVNode.index
就是E.index
为4
oldVNode.index < lastNotChangedIndex
也就是E.index < 2
=>4 < 2
不成立- 所以
E
节点不需要动,lastNotChangedIndex
取E.index
和lastNotChangedIndex
的最大值,结果为4
- 将
E
节点从oldKeyChildMap
中删除
- 拿到的
- 第四次遍历时:
- 拿到的
newVNode
是F
,同时lastNotChangedIndex
为4
,F.key
是F
newVNode F
不存在oldKeyChildMap
,所以拿不到oldVNode
action
为{type: CREATE, newVNode: F, index: 3}
- 所以
F
节点需要创建,lastNotChangedIndex
不变
- 拿到的
- 第五次遍历时:
- 拿到的
newVNode
是A
,同时lastNotChangedIndex
为4
,所以A.key
是A
newVNode A
存在于oldKeyChildMap
,所以可以拿到oldVNode
oldVNode.index
就是A.index
为0
oldVNode.index < lastNotChangedIndex
也就是A.index < 4
=>0 < 4
成立- 所以
A
节点需要移动,lastNotChangedIndex
取A.index
和lastNotChangedIndex
的最大值,结果不变,还是4
action
为{type: MOVE, oldVNode: A, newVNode: A, index: 4}
- 将
A
节点从oldKeyChildMap
中删除
- 拿到的
- 遍历结束,退出循环
这段处理的源码如下:
// lastNotChangedIndex 用来保存上一次没有变化的节点的索引,初始值为 -1
let lastNotChangedIndex = -1;
let actions = [];
newVNodeChildren.forEach((newVNode, index) => {
// 将 newVNode.index 设置为 index
newVNode.index = index;
// 如果 newVNode.key 存在就用 key,否则用 index
// 这个 key 是 <div key={xxx}></div> 中的 key
let newKey = newVNode.key ? newVNode.key : index;
// 通过 key 从 oldKeyChildMap 中找到有没有 oldVNode
let oldVNode = oldKeyChildMap[newKey];
if (oldVNode) {
// 如果有,调用 deepDOMDiff 进行深度比较,里面可能还有子元素,属性需要比较
deepDOMDiff(oldVNode, newVNode);
// 如果 oldVNode.index < lastNotChangedIndex,说明这个节点需要移动
if (oldVNode.index < lastNotChangedIndex) {
actions.push({ type: MOVE, oldVNode, newVNode, index });
}
// 操作过的节点,从 oldKeyChildMap 中删除,oldKeyChildMap 中剩下的就是需要删除的节点
delete oldKeyChildMap[newKey];
// 更新 lastNotChangedIndex
lastNotChangedIndex = Math.max(lastNotChangedIndex, oldVNode.index);
} else {
// 如果没有,说明这个这个节点需要创建
actions.push({ type: CREATE, newVNode, index });
}
});
在遍历结束后,actions
数组中就有了 MOVE
和 CREATE
两种状态,如下所示:
{type: MOVE, oldVNode: B, newVNode: B, index: 1}
{type: CREATE, newVNode: F, index: 3}
{type: MOVE, oldVNode: A, newVNode: A, index: 4}
oldKeyChildMap
中剩下的是需要被删除的节点,如下所示:
{D: VNode}
将需要删除的节点从页面中删除
现在 oldKeyChildMap
中剩下的节点是需要从页面中删除的,actions
中标记为 MOVE
节点也需要从页面中删除
所以先从 actions
中找出需要移动的节点
let VNodeToMove = actions
// 过滤出 type 为 MOVE 的节点
.filter((action) => action.type === MOVE)
// 将节点对应的 oldVNode 取出来
.map((action) => action.oldVNode);
将 oldKeyChildMap
中剩下的节点和 VNodeToMove
节点合并,然后遍历这个合并后的数组,将这些节点从页面中删除
let VNodeToDelete = Object.values(oldKeyChildMap);
// 将 oldKeyChildMap 中剩下的节点和 VNodeToMove 节点合并
VNodeToMove.concat(VNodeToDelete).forEach((oldVNode) => {
// 找到 oldVNode 对应的 dom 节点
let currentDOM = findDOMByVNode(oldVNode);
// 从页面中删除 dom 节点
currentDOM.remove();
});
处理需要创建的节点和需要移动的节点
actions
中的节点最终是要渲染到页面中的,但是它有两种状态:
CREATE
:对于CREATE
的状态,我们调用createDOM
创建新的DOM
,然后将这个DOM
插入到页面中MOVE
:对于MOVE
的状态,我们调用findDOMByVNode
找到oldVNode
对应的DOM
,然后将这个DOM
移动到index
位置
具体的更新过程如下:
- 我们拿到
parentDOM
中所有的childNodes
(这里的parentDOM
是更新的root
节点,也就是return
中的root
节点)- 此时页面中还剩
C
和E
两个元素
- 此时页面中还剩
- 从
action
中拿到index
- 第一个
action
是B
,B
的index
是1
childNodes
索引为1
的childNode
是E
- 所以
B
是需要移动到E
的前面
- 第二个
action
是F
,F
的index
是3
childNodes
没有索引为3
的childNode
- 所以直接插入到
parentDOM
的最后
- 第三个
action
是A
,A
的index
是4
childNodes
没有索引为4
的childNode
- 所以直接插入到
parentDOM
的最后
- 第一个
插入到某个 childNode 之前是使用 API insertBefore,插入到最后是使用 API appendChild
actions.forEach((action) => {
let { type, oldVNode, newVNode, index } = action;
// 拿到需要更新的节点的 childNodes
let childNodes = parentDOM.childNodes;
// 通过 index 找到 childNodes 中的 childNode
let childNode = childNodes[index];
// 根据不同的 type,创建不同的 dom
const getDomForInsert = () => {
// 如果 type 是 CREATE,就创建新的 dom
if (type === CREATE) {
return createDOM(newVNode);
}
// 如果 type 是 MOVE,就找到 oldVNode 对应的 dom
if (type === MOVE) {
return findDOMByVNode(oldVNode);
}
};
// 如果 childNode 存在,就插入到 childNode 之前
if (childNode) {
// 插入到某个 childNode 之前是使用 insertBefore
parentDOM.insertBefore(getDomForInsert(), childNode);
} else {
// 如果 childNode 不存在,就插入到最后
parentDOM.appendChild(getDomForInsert());
}
});
总结
diff
算法是 react
的核心,通过比较新旧 DOM
树,找出差异,然后更新差异部分
更新分为五种情况:
- 不需要操作:新节点,旧节点都不存在
- 新增:新节点存在,旧节点不存在
- 删除:新节点不存在,旧节点存在
- 替换:新节点存在,旧节点也存在,但是类型不一样
- 更深入比较:新节点存在,旧节点也存在,类型一样 --> 我们需要进行深入的比较
DOM
树处理,主要是这四种,最终落脚点是原生节点:
- 原生节点
- 类组件
- 函数组件
- 文本节点
对于原生节点的处理,是 DOM Diff
的核心,也是最复杂的,通过 key
找到需要移动,创建,删除的节点,然后进行处理