React核心源码解析
- diff
- [Update 状态更新](#Update 状态更新)
- [Concurrent Mode](#Concurrent Mode)
diff
一共两个阶段
- render:内存中的更新,主要是通过递归的过程,来将react变化的部分,在内存中找到哪些元素是需要发生变化的,针对需要发生变化的内容,会打上标,也就是
Update
,针对这些需要Update的组件,会拿着上一次与当前这一次的节点,通过Fiber
(专门的数据结构存储当前的组件/模块)进行此次与上一次元素更新之间对比。
diff 的过程,其实就是Update Fiber
的过程,diff的结果就是生成一个经过Update更新之后的新的Fiber的节点。 - commit:同步的过程,同步渲染到浏览器端/客户端的过程
- 针对不同类型的元素
从
html
<div>
<Component />
</div>
到
html
<span>
<Component />
</span>
这是不同类型元素的展示,在Fiber中,定义当前组件类型不一致的时候,是需要将当前树全部销毁重建的
因此,在开发过程中,需要尽可能减少元素的变化。
- 针对相同类型元素
从
html
<ul className="hello">
<li>1</li>
<li>2</li>
</ul>
到
html
<ul className="world">
<li>3</li>
<li>4</li>
<li>5</li>
</ul>
就是在render
的阶段中,相同类型的节点进行diff
判断,并不会立马更新。生成mutation
要变化的内容,针对不同diff进行汇总,然后在commit
过程中,对dom的节点直接进行操作。
但是 上述例子中,如果是针对第一个元素<li>之前进行添加的话,会将所有的子列表全部销毁掉,重新创建,
就比如,从
html
<ul className="hello">
<li>1</li>
<li>2</li>
</ul>
到
html
<ul className="world">
<li>3</li>
<li>1</li>
<li>2</li>
</ul>
这样的情况
这时候,通过key
的方式去解决,并且尽可能保证每个key是唯一的,不要去使用index作为key来传输
跟一个真实DOM相关联的是:
- current fiber:当前的fiber节点,表达当前DOM的 内存对象
- workInProgress fiber:内存中变化的节点,表达接下来在内存中需要进行操作的对象
- DOM:在commit过程中生成的真实的dom
- JSX:真实代码
通过current fiber
和JSX
比较,更新到workInProgress fiber
,进行current fiber和workInProgress fiber的互换,拿着workInProgress fiber替换生成到真实DOM
。
- 同级元素的比较
- 不同类型元素 销毁当前节点&所有子节点
- key
javascript
// 根据newChild类型选择不同diff函数处理
function reconcileChildFibers(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChild: any,
): Fiber | null {
//这里的newChild是fiber节点,不是dom元素了
const isObject = typeof newChild === 'object' && newChild !== null;
//如果是对象的话,那么就是单一的节点
if (isObject) {
// object类型,可能是 REACT_ELEMENT_TYPE 或 REACT_PORTAL_TYPE
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE:
// 调用 reconcileSingleElement 处理
// // ...省略其他case
}
}
if (typeof newChild === 'string' || typeof newChild === 'number') {
// 调用 reconcileSingleTextNode 处理
// ...省略
}
//如果是数组的话,则是多个节点
if (isArray(newChild)) {
// 调用 reconcileChildrenArray 处理
// ...省略
}
// 一些其他情况调用处理函数
// ...省略
// 以上都没有命中,删除节点
return deleteRemainingChildren(returnFiber, currentFirstChild);
}
单一节点比较diff
javascript
function reconcileSingleElement(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
element: ReactElement
): Fiber {
const key = element.key;
let child = currentFirstChild;
// 首先判断是否存在对应DOM节点
while (child !== null) {
// 上一次更新存在DOM节点,接下来判断是否可复用
// 首先比较key是否相同
if (child.key === key) {
// key相同,接下来比较type是否相同
switch (child.tag) {
// ...省略case
default: {
if (child.elementType === element.type) {
// type相同则表示可以复用
// 返回复用的fiber
return existing;
}
// type不同则跳出switch
break;
}
}
// 代码执行到这里代表:key相同但是type不同
// 将该fiber及其兄弟fiber标记为删除
deleteRemainingChildren(returnFiber, child);
break;
} else {
// key不同,将该fiber标记为删除
deleteChild(returnFiber, child);
}
child = child.sibling;
}
// 创建新Fiber,并返回 ...省略
}
针对单一的节点:
- key是否一样
- 一样
- type 一样:同一个DOM
- type不一样:当前节点和所有的兄弟节点sibling全都删除
- 不一样
- 直接删除
- 一样
举例:
全部删除:
javascript
// 更新前
<div>a</div>
// 更新后
<p>a</p>
// 更新前
<div key="xxx">a</div>
// 更新后
<div key="ooo">a</div>
只更新内容:
javascript
// 更新前
<div key="xxx">a</div>
// 更新后
<div key="xxx">b</div>
多节点比较diff
isArray方法比较
- 节点更新
javascript
// 更新前
<ul>
<li key="0" className="before">0<li>
<li key="1">1<li>
</ul>
// 更新后 情况1 ------ 节点属性变化
<ul>
<li key="0" className="after">0<li>
<li key="1">1<li>
</ul>
// 更新后 情况2 ------ 节点类型更新
<ul>
<div key="0">0</div>
<li key="1">1<li>
</ul>
节点属性变化,只需要更新即可
节点类型变化,则当前<li>和兄弟节点全部推倒重建
- 节点新增和删除
javascript
// 更新前
<ul>
<li key="0">0<li>
<li key="1">1<li>
</ul>
// 更新后 情况1 ------ 新增节点
<ul>
<li key="0">0<li>
<li key="1">1<li>
<li key="2">2<li>
</ul>
// 更新后 情况2 ------ 删除节点
<ul>
<li key="1">1<li>
</ul>
- 节点位置变化,顺序调整
javascript
// 更新前
<ul>
<li key="0">0<li>
<li key="1">1<li>
</ul>
// 更新后
<ul>
<li key="1">1<li>
<li key="0">0<li>
</ul>
这里key不一样,全部删除重建
两轮遍历比较
第一轮比较
- let i = 0,遍历newChildren,将newChildren[i]与oldFiber比较,判断DOM节点是否可复用;
JSX:newChildren[i]
currentFiber:oldFiber - 如果可复用,i++,继续比较newChildren[i]与oldFiber.sibling,可以复用则继续遍历;
- 如果不可复用,分两种情况:
a. key不同导致不可复用,立即跳出整个遍历,第一轮遍历结束;
b. key相同type不同导致不可复用,会将oldFiber标记mutation为DELETION,并继续遍历; - 如果newChildren遍历完(即 i === newChildren.length - 1 )或者oldFiber遍历完(即oldFiber.sibling === null),跳出遍历,第一轮遍历结束;
javascript
// 更新前
<li key="0">0</li>
<li key="1">1</li>
<li key="2">2</li>
// 更新后
<li key="0">0</li>
<li key="2">1</li>
<li key="1">2</li>
// 第一个节点可复用,遍历到key === 2的节点发现key改变,不可复用
// 跳出遍历,等待第二轮遍历处理
// oldFiber: key === 1、key === 2未遍历
// newChildren剩下key === 2、key === 1未遍历
javascript
// 更新前
<li key="0" className="a">0</li>
<li key="1" className="b">1</li>
// 更新后 情况1 ------ newChildren与oldFiber都遍历完
<li key="0" className="aa">0</li>
<li key="1" className="bb">1</li>
// 更新后 情况2 ------ newChildren没遍历完,oldFiber遍历完
// newChildren剩下 key==="2" 未遍历
<li key="0" className="aa">0</li>
<li key="1" className="bb">1</li>
<li key="2" className="cc">2</li>
// 更新后 情况3 ------ newChildren遍历完,oldFiber没遍历完
// oldFiber剩下 key==="1" 未遍历
<li key="0" className="aa">0</li>
第二轮比较
-
newChildren和oldFiber都遍历完了,针对单一节点标记mutation,需要发生变化的元素记录在render阶段中,加update更新
-
newChildren剩下了,oldFiber没剩下,意味着新增了
将
剩余的reset newChildren
直接添加到workInProgress Fiber
新增元素打标,将
mutation
记为Placement
-
newChildren 没剩下 oldFiber 剩下了,意味着删除了
在
workInProgress Fiber
中将旧的循环遍历掉,标记mutation
是DELETION
-
newChildren和oldFiber都剩下了
第二轮遍历:
const exisitingChildren = map(),通过map.get获取oldFiber剩下的所有存在existingChildren中
- key:oldFiber中的key
- value:oldFiber中的value
遍历 newChildren 剩下的
exisitingChildren.has(newChildren[i].key),有的话获取这个元素,然后删除掉
lastPlacedIndex:最后一个可复用的节点,再当前oldFiber的索引位置
Update 状态更新
触发状态更新的方式
- 初始化的render
- setState
- useState
- forceUpdate
进行状态更新时候会创建一个Update 对象
,存储变更相关联的内容
包含到对应的fiber
,在beginWork
的过程中,找到这个fiber
,记录到当前更新的数组队列updateQueue
里
Update是在render beginWork
找到要更新的元素,Update置入
要更新的节点中,去触发它的state
typescript
// 一次更新的内容
const update: Update<*> = {
eventTime, //获取当前的执行时间,判断执行更新的耗时
lane, //优先级任务
suspenseConfig,
tag: UpdateState, //包裹住更新的状态,updateState | ReplaceState | forceUpdate
payload: null, //不同的更新元素带有的参数是什么
callback: null,
next: null, // 也是一个链表,下一次更新的内容
};
//一次更新的 update1 next->update2 next->update3
//div1 div2
fiber 节点 updateQueue指代的是div1,div2的变化
typescript
//一个更新的fiber的节点
const queue: UpdateQueue<State> = {
baseState: fiber.memoizedState, //fiber中本身的state
firstBaseUpdate: null, //第一个更新的
lastBaseUpdate: null, //最后一个更新的
shared: {
pending: null, //关联链表的方式,第一个更新,第二个更新,...最后一个更新,通过这个来关联的
},
effects: null,
};
pending候车的上车
先上u3

这都是在render中做的,进入到commit阶段后,不管是谁,都不能被中断了,因为都已经在视图中了
因此,说的 异步可中断,说的都是在内存中能够做到的事情
u2优先级高于u1
但是链表:u1 --- u2 这个顺序始终不会变
也就意味着,由于优先级,先执行的是u2,然后再执行一次 u1 和 u2。由此,高优先级的任务可能会触发两次
Concurrent Mode
协调模式
- Fiber 异步可中断
- Scheduler 协调器,结合可分片,异步可中断
- lane 优先级
- 优先级不同
- 优先级表达的方式 batch批次执行
- 方便计算
划分二进制31位内容