基于 [email protected] concurrent 并发模式
注意这里的「并发」只是单线程下的可以交替执行不同任务,而非并行执行任务
分为两个部分 render 阶段(调度和调和)和 commit 阶段(更新渲染)
所有的内部执行由调用模仿 requestIdleCallback
的 workloop
函数,每隔一段时间执行一次,该函数检查当前全局变量 nextUnitOfWork
是否存在 fiber 节点
- 调用
performUnitOfWork
,初始时候根据 current 树的 root 生成 wip 树的 root ,并将 wip root 设置为nextUnitOfWork
- 进入 beginWork 和 completeWork 的 Render 阶段
- 当
nextUnitOfWork
为空,但是workInProgressRoot
wip 树存在时候,表明当前新的 fiber 树已构建完成。就进入commit
阶段
Render 阶段是生成新的 fiber 树,主要包括 beginWork 以及 completeWork 两个阶段
- beginWork :
reconcile
调和- 根据当前
nextUnitOfWork
指向的 fiber 节点上的 fiber.type 进行不同操作并获取到最新的虚拟 DOM 生成新的 fiber 节点- 原生 HTML 不进行操作(不需要生成新的 vdom )
- 调用函数组件(执行 useState 等 hook )得到新的 vdom
- diff 算法对比新旧 vdom ,根据 diff 结果给 fiber 打上 flag/effect tag (如果存在 useEffect 等副作用函数也会被打 effect tag )
- 根据当前
- completeWork :
- 根据 fiber.tag 组件类型来执行不同逻辑,更新/创建 DOM ,如果
fiber.stateNode
为空,那么就会调用 api 创建 DOM - 会收集所有带有 flag/effect tag 的 fiber 到单向链表中即 effectList
- 根据 fiber.tag 组件类型来执行不同逻辑,更新/创建 DOM ,如果
从根节点开始,向下再向上
1. rootFiber beginWork
2. App Fiber beginWork
3. div Fiber beginWork
4. "i am" Fiber beginWork
5. "i am" Fiber completeWork
6. span Fiber beginWork
7. span Fiber completeWork
8. div Fiber completeWork
9. App Fiber completeWork
10. rootFiber completeWork // 没有 KaSong 是因为 React 默认对静态文本节点进行优化了
commit 阶段就是遍历 effectList 链表并执行对应的逻辑
- before mutation (执行 DOM 操作之前):遍历 effectList 执行,异步调用 useEffect
- mountation (执行 DOM 操作) :遍历 effectList 进行 DOM 操作
- layout (执行 DOM 操作后):同步调用 useLayoutEffect
- R16 之前的架构,只有 vdom 树,fiber 树/链表可以看作是 vdom 树的扩展
- fiber 节点看作是 React 中最小可执行单元
- fiber 树本质上是 fiber 链表(由于树递归不可中断,所以改为链表可中断)
mini-react
中实现的 hook 链表是简略版,源码中的是有环链表
function dispatchAction(queue, action) {
const update = {
action,
next: null,
};
if (queue.pending === null) {
queue.pending = update;
update.next = update;
} else {
update.next = queue.pending.next;
queue.pending.next = update;
}
queue.pending = update;
// 设置 nextUnitOfWork 为 wip root ,开始调度
commitRender()
}
queue.pending = u1 ---> u0
^ |
| |
---------
queue.peneding = u1
策略大体上和 Vue 一致
-
只比较同层节点
- 如果节点的 type 不同,则直接默认不能复用
- 如果这个节点是函数/类组件的话会调用
React.memo
中的回调函数来决定是否向子节点进行 diff
-
type 相同,如果有 key 的话也会加入 key 进行比较(最终目的都是尽可能复用节点)( React 不会自动添加 key )
假如如下
使用三个变量来辅助 diff 算法
- 那么此时 diff 过程如下
- 节点B:此时 maxIndex=0,oldIndex=1;满足 maxIndex< oldIndex,因此B节点不动,此时maxIndex= Math.max(oldIndex, maxIndex),就是1
- 节点A:此时maxIndex=1,oldIndex=0;不满足maxIndex< oldIndex,因此A节点进行移动操作,此时maxIndex= Math.max(oldIndex, maxIndex),还是1
- 节点D:此时maxIndex=1, oldIndex=3;满足maxIndex< oldIndex,因此D节点不动,此时maxIndex= Math.max(oldIndex, maxIndex),就是3
- 节点C:此时maxIndex=3,oldIndex=2;不满足maxIndex< oldIndex,因此C节点进行移动操作,当前已经比较完了
key 用于 diff 算法(用于同层位置比较),但部分情况下有 key 不一定比无 key 性能好,如下 innerText
性能比移动 dom 更好
1.加key
<div key='1'>1</div> <div key='1'>1</div>
<div key='2'>2</div> <div key='3'>3</div>
<div key='3'>3</div> ========> <div key='2'>2</div>
<div key='4'>4</div> <div key='5'>5</div>
<div key='5'>5</div> <div key='4'>4</div>
操作:节点2移动至下标为2的位置,节点4移动至下标为4的位置。
2.不加key
<div>1</div> <div>1</div>
<div>2</div> <div>3</div>
<div>3</div> ========> <div>2</div>
<div>4</div> <div>5</div>
<div>5</div> <div>4</div>
操作:修改第1个到第5个节点的innerText
例如给 div 绑定 onClick
事件,但是在浏览器中该 DOM 的 click event 绑定的是 noop
React 会将所有事件按需绑定到 root 根节点上 上,通过冒泡的形式触发 document 上的事件,并不会将事件绑定到真实的 DOM 上。同时一个事件可能有多个事件绑定在 document 上,如 onChange
,此时 document 上可能有 blur
change
input
等事件绑定,如下
这么做的原因主要是跨平台的考虑,同时兼容不同浏览器,保证 React 的事件行为是一致的
默认 R17 会将 setState 合并更新,即多次 setState 最后只会有一次的 setState ,但如果在 setTimeout Promise 或者原生 DOM 事件中就会失效,同时打印内容入下(每次 setState 同时出发 render 阶段和 commit 阶段)
const [state, setState] = useState(1);
useEffect(() => {
console.log(state, 'render');
}, [state]);
return (
<>
<div
onClick={() => {
setTimeout(() => {
setState(2); // -->
console.log(state); // 2
setState(3);
console.log(state); // 3
setState(5);
});
}}
>
qweqwewq
</div>
</>
);
同时可以使用 flushSync
来提高优先级来破坏批量更新
flushSync(() => {
setCounter((c) => c + 1);
});
因为内部用的是 isBatchUpdate
变量来决定当前是否启动合并更新,在函数调用前设置 isBatchUpdate = true ,函数执行完成之后设置 isBatchUpdate = false
所以在异步函数中由于是 isBatchUpdate = false 所以就无法进行批量更新
R17 中可以使用 unstable_bactchUpdate api 来实现批量更新
R18 之后对其进行优化,在 setTimeout 中的也进行批量更新
即不用全局变量,而是改为优先级,同一个宏任务/微任务的优先级 lane 是相同的,所以两个 setState 的优先级 lane 是相同的,从而实现批量更新
- 兼容性考虑
- 只有 20fps 的间隔也就是一秒只会调用 20 次
- 非 DOM 环境:使用
setTimeout(()=>{})
进行执行 - DOM 环境:使用
requestAniamtion
setTimeout
postmessage
进行模拟行为
必须配合 React.memo 或是 shouldComponentUpdate 对比 props return true 就不重新进行渲染
是有一定的成本的(因为增加了额外的 deps 变化判断),不配合 React.memo 的话就是负优化
注意,props 即使没有变化,也会重新执行子组件函数,除非子组件函数添加上 React.memo
- 函数传递给的子组件过多、或者比较深的层级,一旦变化导致执行多个,可以考虑使用 useCallback 进行优化
一般情况下我不用,等到性能问题出现之后或是如上的特殊情况下才配合 React.memo 用
默认情况下只会对 props 进行浅比较,若有需要的话就传递第二个函数参数,自定义比较
React.memo(App,function(preProps,newProps){
return true // 不进行重新渲染
}) //
相对简单,useMemo 一般用于计算复杂/耗时逻辑得出的状态,以避免重新执行函数的时候重复进行复杂计算
strictMode
dev
下 useEffect
默认执行两次
- React 模拟立刻卸载和重新挂载组件
- 为了让开发者尽可能写不影响应用正常运行的回调函数(铺垫未来新功能)
strictMode 辅助 dev ,会提示一些废弃 api 等
R17 中是通过内部全局变量进行统一标记
import React, { useState, useEffect, useDeferredValue } from 'react';
const App: React.FC = () => {
const [list, setList] = useState<any[]>([]);
useEffect(() => {
setList(new Array(10000).fill(null));
}, []);
// 使用了并发特性,开启并发更新
const deferredList = useDeferredValue(list);
return (
<>
{deferredList.map((_, i) => (
<div key={i}>{i}</div>
))}
</>
);
};
export default App;
普通情况下(非并发)
import React, { useState, useEffect } from 'react';
const App: React.FC = () => {
const [list, setList] = useState<any[]>([]);
useEffect(() => {
setList(new Array(10000).fill(null));
}, []);
return (
<>
{list.map((_, i) => (
<div key={i}>{i}</div>
))}
</>
);
};
export default App;
- react forget 编译器,相当于自动添加 useMemo useCallback React.memo 等函数
- useOptimistic 乐观更新
改造/强化子组件,例如 icon 给 input 框架上,就把 icon 的逻辑从子组件剥离出
- 组件定位:业务组件、通用组件
- 传参的考虑,通用组件就支持更多 props 传入,业务组件的话就少一些 props
- 数据流考虑:props 传递,还是 context 获取全局
- 内部数据变化是否不要影响外部的更新,例如将数据用 useRef
- 样式覆盖方案: .module.css 还是 inline-style
手把手教你实现史上功能最丰富的简易版 React
聊一聊Diff算法(React、Vue2.x、Vue3.x)