React
# 1. 宏观包结构
# 2. 事件机制
为了解决跨浏览器的兼容性问题,SyntheticEvent 实例将被传递给你的事件处理函数,SyntheticEvent 是 React 跨浏览器的浏览器原生事件包装器,它还拥有和浏览器原生事件相同的接口,包括 stopPropagation() 和 preventDefault()。
- 监听原生事件:对齐 DOM 元素和 fiber 元素。
- 收集 listeners:遍历 fiber 树,收集所有监听本事件 listener 函数。
- 派发合成事件:构造合成事件,遍历 listeners 进行派发。
React 实际上并不将事件附加到子节点本身。React 使用单个事件侦听器侦听顶层的所有事件。这对性能有好处,也意味着 React 在更新 DOM 时不需要跟踪事件监听器。
React 16 React 事件系统工作原理 - 网易云音乐大前端团队 (opens new window)
- React 将所有事件类型都注册到 document 上。
- 所有原生事件的 listener 都是 dispatchEvent 函数。
- 同一个类型的事件 React 只会绑定一次原生事件,例如无论我们写了多少个 onClick,最终反应在 DOM 事件上只会有一个 click listener(采用 Set 数据结构存储判断)。
- React 并没有将我们业务逻辑里的 listener 绑在原生事件上,也没有去维护一个类似 eventlistenermap 的东西存放我们的 listener。
React 16 及更早版本,合成事件对象的事件处理函数全部被调用之后,所有属性都会被置为 null。这时,如果我们需要在事件处理函数运行之后获取事件对象的属性,可以使用 React 提供的 e.persist() 方法,保留所有属性。
React 17 不使用事件池,不会存在 “所有属性都会被置为 null” 的问题。
const handleChange = e => { // e.persist() console.log(e.target) // button setTimeout(() => console.log(e.target), 100) // null }
1
2
3
4
5类组件:事件触发过程中,dispatchEvent 调用了 invokeGuardedCallback 方法。回调函数是直接调用的,并没有指定调用的组件,所以不进行手动绑定的情况下直接获取到的 this 是 undefined。
批量更新与同步更新。
批量更新:
- 在 React Legacy 模式中,经过 batchedUpdates 执行的调度会开启批量更新(scheduleUpdateOnFiber (opens new window)、batchedUpdates (opens new window))。
- React 管控合成事件的回调函数(onClick、onFocus 等)默认经过 batchedUpdates 函数执行。
- react-dom 包中导出了 batchedUpdates 函数(取别名为 unstable_batchedUpdates),开发者可以通过此函数开启批量更新(packages/react-dom/src/client/ReactDOM (opens new window))。
- Blocking 和 Concurrent 模式均为批量更新。
// 更新调度 export function scheduleUpdateOnFiber() { // ... if (lane === SyncLane) { // Legacy 模式 if (omit) { } else { // 批量更新 ensureRootIsScheduled(root, eventTime) schedulePendingInteractions(root, lane) // 执行上下文为空时进行同步更新 if (executionContext === NoContext) { // 同步更新 resetRenderTimer() flushSyncCallbackQueue() } } } else { // 并发模式(Blocking | Concurrent) // ... // 批量更新 ensureRootIsScheduled(root, eventTime) schedulePendingInteractions(root, lane) } // ... } // 批处理函数 export function batchedUpdates() { const prevExecutionContext = executionContext executionContext |= BatchedContext // 位掩码方式增加 BatchedContext try { // 此时,执行 scheduleUpdateOnFiber 的 executionContext !== NoContext return fn(a) } finally { executionContext = prevExecutionContext if (executionContext === NoContext) { // Flush the immediate callbacks that were scheduled during this batch resetRenderTimer() flushSyncCallbackQueue() } } }
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同步更新:
- 在 React Legacy 模式中,非 React 管控的事件的回调函数(addEventListener、Promise、MessageChannel、setTimeout)不经过 batchedUpdates 函数执行,也就是同步更新。
React 会在派发事件时打开批量更新,此时 1、2 在合成事件内,只会触发一次 render。setTimeout 为异步任务,此时 3、4 是同步的,均会触发一次 render。
类组件打印顺序:0 -> 0 -> render -> flushSync -> render -> 3 -> render -> 4 -> batched -> 4 -> 4 -> render
import React, { Component } from 'react' import { unstable_batchedUpdates } from 'react-dom' class App extends Component { state = { num: 0, } handleChange = () => { const setState = this.setState.bind(this) setState({ num: 1 }) console.log(this.state.num) // 0 setState({ num: 2 }) console.log(this.state.num) // 0 setTimeout(() => { console.log('flushSync') setState({ num: 3 }) console.log(this.state.num) // 3 setState({ num: 4 }) console.log(this.state.num) // 4 }) setTimeout(() => { unstable_batchedUpdates(() => { console.log('batched') setState({ num: 5 }) console.log(this.state.num) // 4 setState({ num: 6 }) console.log(this.state.num) // 4 }) }) } render() { console.log('render') return ( <> <button onClick={this.handleChange}>button</button> </> ) } }
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- 函数组件打印顺序:0 -> 0 -> render -> flushSync -> render -> 0 -> render -> 0 -> batched -> 0 -> 0 -> render
- Hooks 闭包原因,不能在 setTimeout 获取到 num 最新的值
import React, { useState } from 'react' import { unstable_batchedUpdates } from 'react-dom' const App = () => { const [num, setNum] = useState(0) const handleChange = () => { setNum(1) console.log(num) // 0 setNum(2) console.log(num) // 0 setTimeout(() => { console.log('flushSync') setNum(3) console.log(num) // 0 setNum(4) console.log(num) // 0 }) setTimeout(() => { unstable_batchedUpdates(() => { console.log('batched') setNum(5) console.log(num) // 0 setNum(6) console.log(num) // 0 }) }) } console.log('render') return ( <> <button onClick={handleChange}>button</button> </> ) }
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
39React 17 事件系统新特性。
- React 将不再向 document 附加事件处理器。而会将事件处理器附加到渲染 React 树的根 DOM 容器中
ReactDOM.render(<App />, rootNode);
。将顶层事件绑定在 rootNode 上而不是 document 上能够解决我们遇到的多版本共存问题,对微前端方案是个重大利好。 - 支持了原生捕获事件的支持,对齐了浏览器原生标准,同时 onScroll 事件不再进行事件冒泡。
- 取消事件复用。
- React 将不再向 document 附加事件处理器。而会将事件处理器附加到渲染 React 树的根 DOM 容器中
在 processDispatchQueueItemsInOrder 函数中,根据捕获(capture)或冒泡(bubble)的不同,采取了不同的遍历方式(packages/react-dom/src/events/DOMPluginEventSystem (opens new window))。
- capture 事件:从上至下调(由外向里)用 fiber 树中绑定的回调函数,所以倒序遍历 dispatchListeners。
- bubble 事件:从下至上调(由里向外)用 fiber 树中绑定的回调函数,所以顺序遍历 dispatchListeners。
事件触发顺序。
17 版本之前 | 17 版本 |
---|---|
html 捕获 | 父元素 React 事件捕获 |
父元素原生捕获 | 子元素 React 事件捕获 |
子元素原生捕获 | root 捕获 |
子元素原生冒泡 | 父元素原生捕获 |
父元素原生冒泡 | 子元素原生捕获 |
html 冒泡 | 子元素原生冒泡 |
父元素 React 事件捕获 | 父元素原生冒泡 |
子元素 React 事件捕获 | 子元素 React 事件冒泡 |
子元素 React 事件冒泡 | 父元素 React 事件冒泡 |
父元素 React 事件冒泡 | root 冒泡 |
# 3. Fiber 架构
Fiber 是对 React 核心算法的重构,facebook 团队使用两年多的时间去重构 React 的核心算法,在 React 16 以上的版本中引入了 Fiber 架构。
React 15 架构可以分为两层:
- Reconciler(协调器)- 负责找出变化的组件
- Renderer(渲染器)- 负责将变化的组件渲染到页面上
React 16 架构可以分为三层:
- Scheduler(调度器)- 调度任务的优先级,高优任务优先进入 Reconciler
- Reconciler(协调器)- 负责找出变化的组件
- Renderer(渲染器)- 负责将变化的组件渲染到页面上
React 15 中 Reconciler 是递归处理虚拟 DOM 的,递归过程是不能中断的。如果组件树的层级很深,递归会占用线程很多时间,造成卡顿。
React 16 使用双缓存 Fiber 树(链表结构)处理虚拟 DOM,调度的过程可以被中断,将控制权交回浏览器,让位给高优先级的任务,浏览器空闲后再恢复调度。让 React 的更新过程变得可控,避免了之前一竿子递归到底影响性能的做法(渲染可中断需开启 Concurrent 模式)。
- 为每个任务增加了优先级,优先级高的任务可以中断低优先级的任务。然后再重新执行优先级低的任务。
- 增加了异步任务,调用 requestIdleCallback(React 使用自我实现的 requestIdleCallbackpolyfill —— Scheduler),浏览器空闲的时候执行。
- dom diff 树变成了链表,一个 dom 对应两个 fiber,对应两个队列,是为找到被中断的任务,重新执行。
任务优先级:为了避免任务被饿死,可以设置一个超时时间。这个超时时间不是死的,低优先级的可以慢慢等待,高优先级的任务应该率先被执行。目前 React 预定义了 5 个优先级。
- Immediate(-1)这个优先级的任务会同步执行,或者说要马上执行且不能中断。
- UserBlocking(250ms)这些任务一般是用户交互的结果,需要即时得到反馈。
- Normal(5s)应对哪些不需要立即感受到的任务,例如网络请求。
- Low(10s)这些任务可以放后,但是最终应该得到执行。例如分析通知。
- Idle(没有超时时间)一些没有必要做的任务(e.g. 比如隐藏的内容),可能会被饿死。
Fiber 两阶段:
协调阶段(fiber 树构造):可以认为是 Diff 阶段,这个阶段可以被中断,会找出所有节点变更,例如节点新增、删除、属性变更等等,这些变更 React 称之为 副作用(Effect)。以下生命周期钩子会在协调阶段被调用:
- constructor
- componentWillMount(废弃)
- componentWillReceiveProps(废弃)
- static getDerivedStateFromProps
- shouldComponentUpdate
- componentWillUpdate(废弃)
- render
提交阶段(fiber 树渲染):将上一个阶段计算出来的需要处理的 副作用(Effects) 一次性执行了。Legacy 模式下这个阶段必须同步执行,不能被打断。这些生命周期钩子在提交阶段被执行:
- getSnapshotBeforeUpdate(严格来说,这个是在进入 commit 阶段前调用)
- componentDidMount
- componentDidUpdate
- componentWillUnmount
# 4. Hooks
代数效应是函数式编程中的一个概念,用于将副作用从函数调用中分离。不需要关注 Hooks 怎么存储状态,只需按正常结果编写业务逻辑。
// 假设语法 function getName(user) { let name = user.name; if (name === null) { name = perform 'ask_name'; } return name; } function makeFriends(user1, user2) { user1.friendNames.add(getName(user2)); user2.friendNames.add(getName(user1)); } const arya = { name: null }; const gendry = { name: 'Gendry' }; try { makeFriends(arya, gendry); } handle (effect) { if (effect === 'ask_name') { setTimeout(() => { resume with 'Arya Stark'; }, 1000); } }
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极简 Hooks 实现 - React 技术揭秘 (opens new window)
let workInProgressHook // 当前工作的 Hook let isMount = true // true 代表首次挂载 // App 组件对应的 fiber const fiber = { memoizedState: null, // Hooks 链表 stateNode: App, // render } // 调度更新 function schedule() { // 更新前将 workInProgressHook 重置为第一个 Hook workInProgressHook = fiber.memoizedState // render const app = fiber.stateNode() isMount = false return app } // 更新 Hook(updateNum) function dispatchAction(queue, action) { // 更新队列:环状单向链表(方便取第一个元素和添加元素) const update = { action, // updateNum 回调 next: null, // 下个 update } // 创建 update 单向环状链表 // queue.pending 为最后一个添加的元素,queue.pending.next 为第一个添加的元素 if (queue.pending === null) { update.next = update } else { update.next = queue.pending.next queue.pending.next = update } queue.pending = update schedule() } function useState(initialState) { let hook if (isMount) { // 首次挂载 hook = { // 更新队列 queue: { pending: null, }, memoizedState: initialState, // Hook 当前状态(num) next: null, // 下个 Hook } // 创建 Hooks 单向链表 if (!fiber.memoizedState) { fiber.memoizedState = hook } else { workInProgressHook.next = hook } workInProgressHook = hook } else { // 更新 hook = workInProgressHook workInProgressHook = workInProgressHook.next // 移动到下一个 Hook } let baseState = hook.memoizedState // 保存更新前状态 // 判断当前 Hook 更新队列是否为空 if (hook.queue.pending) { let firstUpdate = hook.queue.pending.next // 执行更新队列函数,获取最后的 Hook 状态 do { const action = firstUpdate.action baseState = action(baseState) firstUpdate = firstUpdate.next } while (firstUpdate !== hook.queue.pending) // 清空更新队列 hook.queue.pending = null } // 更新 Hook 当前状态 hook.memoizedState = baseState return [baseState, dispatchAction.bind(null, hook.queue)] } function App() { const [num, updateNum] = useState(0) console.log(`${isMount ? 'mount' : 'update'} num: `, num) return { click() { updateNum(num => num + 1) }, } } window.app = schedule()
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
# 5. 启动模式
Legacy 模式:不支持 Concurrent(并发),不可中断渲染。
ReactDOM.render(<App />, document.getElementById('root'), dom => {}) // 支持 callback 回调,参数是一个 dom 对象
1Blocking 模式:开启部分 Concurrent 功能,可中断渲染。
ReactDOM.createBlockingRoot(document.getElementById('root')).render(<App />) // 不支持回调
1Concurrent 模式:开启所有 Concurrent 功能,可中断渲染。
ReactDOM.createRoot(document.getElementById('root')).render(<App />) // 不支持回调
1
# 6. 优先级管理
3 种类型优先级。
- fiber 优先级(LanePriority):位于 react-reconciler 包,也就是 Lane(车道模型)。
- 调度优先级(SchedulerPriority):位于 scheduler 包。
- 优先级等级(ReactPriorityLevel):位于 react-reconciler 包中的 SchedulerWithReactIntegration.js,负责上述 2 套优先级体系的转换。
Lane(车道模型):占有低位比特位的 Lane 变量对应的优先级越高。
- 最高优先级为 SyncLanePriority 对应的车道为 SyncLane = 0b0000000000000000000000000000001。
- 最低优先级为 OffscreenLanePriority 对应的车道为 OffscreenLane = 0b1000000000000000000000000000000。
const SyncLane = 0b0000000000000000000000000000001 const SyncBatchedLane = 0b0000000000000000000000000000010 const InputDiscreteHydrationLane = 0b0000000000000000000000000000100 // ... const task1 = SyncLane, task2 = SyncBatchedLane, task3 = InputDiscreteHydrationLane let batchOfTasks = task1 | task2 // => 0b0000000000000000000000000000011 // 删除单个 task batchOfTasks &= task1 // => 0b0000000000000000000000000000010 // 增加单个 task batchOfTasks |= task1 // => 0b0000000000000000000000000000011 // 比较 task1 是否在 group 中 ;(task1 & batchOfTasks) !== 0 // => true (task1 & batchOfTasks:0b0000000000000000000000000000011) // 比较 task3 是否在 group 中 ;(task3 & batchOfTasks) !== 0 // => false (task3 & batchOfTasks:0b0000000000000000000000000000000)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 7. 调度原理
requestHostCallback 请求回调后,通过 MessageChannel 发消息的方式触发 performWorkUntilDeadline 函数,最后执行回调 scheduledHostCallback。
微任务将在页面更新前全部执行完,达不到将主线程还给浏览器的目的,递归的 setTimeout() 调用会使调用间隔变为 4ms,导致浪费了 4ms(事件循环(Event Loop))。所以首选 MessageChannel ,setTimeout 作为 MessageChannel 降级处理。
let mnCount = 0
function mn() {
const mc = new MessageChannel()
mc.port1.onmessage = ({ data }) => {
console.log(++mnCount, Date.now() - data) // 大部分输出0
if (mnCount < 50) mn()
}
mc.port2.postMessage(Date.now())
}
mn()
let tnCount = 0
function tn() {
const startTime = Date.now()
setTimeout(_ => {
console.log(++tnCount, Date.now() - startTime) // 前4次输出约为1,第5次开始输出约为5
if (tnCount < 50) tn()
}, 0)
}
tn()
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 8. Fiber 树构造
双缓冲技术:把 ReactElement 转换成 fiber 树的过程中,内存里会同时存在 2 棵 fiber 树。
- 代表当前界面的 fiber 树(已经被展示出来,挂载到 fiberRoot.current 上)。如果是初次构造(初始化渲染),页面还没有渲染,此时界面对应的 fiber 树为空(fiberRoot.current = null)。
- 正在构造的 fiber 树(即将展示出来,挂载到 HostRootFiber.alternate 上,正在构造的节点称为 workInProgress)。当构造完成之后,重新渲染页面,最后切换 fiberRoot.current = workInProgress,使得 fiberRoot.current 重新指向代表当前界面的 fiber 树。
fiber 树构造循环负责构造新的 fiber 树,构造过程中同时标记 fiber.flags,最终把所有被标记的 fiber 节点收集到一个副作用队列中,这个副作用队列被挂载到根节点上(HostRootFiber.alternate.firstEffect)。此时的 fiber 树和与之对应的 DOM 节点都还在内存当中,等待 commitRoot 阶段进行渲染。
fiber 树更新阶段跟踪了创建过程中内存引用的变化情况。与初次构造最大的不同在于 fiber 节点是否可以复用,其中 bailout 逻辑是 fiber 子树能否复用的判断依据。
# 9. Fiber 树渲染
从渲染前、渲染、渲染后三个方面分解了 commitRootImpl 函数。其中最核心的渲染逻辑又分为了 3 个函数,这 3 个函数共同处理了有副作用 fiber 节点,并通过渲染器 react-dom 把最新的 DOM 对象渲染到界面上。
commitBeforeMutationEffects
dom 变更之前、处理副作用队列中带有 Snapshot(根节点与类组件)、Passive(使用 hook 的函数组件) 标记的 fiber 节点。
commitMutationEffects
dom 变更、界面得到更新。处理副作用队列中带有 Ref(清空 Ref)、Placement(新增)、Update(更新)、Deletion(删除)、Hydrating 标记的 fiber 节点。
commitLayoutEffects
dom 变更后,处理副作用队列中带有 Update、Callback(如:this.setState({}, callback))、Ref(重新设置 Ref) 标记的 fiber 节点。对于 HostComponent 节点,如有 Update 标记,需要设置一些原生状态(focus 等)。
# 10. 状态与副作用
useState 在 fiber 树构造阶段(render)执行,可以修改 Hook.memoizedState。
useLayoutEffect 和 useEffect 均在 fiber 树渲染阶段(commitRoot)执行。
- useLayoutEffect 是同步执行的,与 componentDidMount、componentDidUpdate 的调用阶段是一样的。
- useEffect 经过了调度中心,所以 useEffect 中的函数是异步执行的(调度中心首选 MessageChannel,setTimeout 降级处理)。
输出顺序:1 -> 5 -> 3 -> 7 -> 2 -> 4 -> 6。
function App() { console.log(1) // 先于 useEffect 注册 const { port1, port2 } = new MessageChannel() port1.onmessage = ({ data }) => console.log(data) port2.postMessage(2) useLayoutEffect(() => { // commitRoot 阶段同步执行 console.log(3) }) useEffect(() => { // commitRoot 阶段,通过调度中心执行(MessageChannel || setTimeout) console.log(4) }) setTimeout(_ => console.log(6)) Promise.resolve(7).then(console.log) console.log(5) return <></> }
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输出顺序:1 -> 2 -> 5 -> num: 0 -> 1 -> 5 -> num: 1 -> 3 -> 6 -> 6 -> 4。
render 执行两次,commitRoot 执行一次。
function App() { console.log(1) const [num, setNum] = useState(0) useState(_ => { console.log(2) setNum(num + 1) // 初次执行 render 后,再次进入 reconciler 流程 }) useLayoutEffect(() => { console.log(3) }) useEffect(() => { console.log(4) }) // 两次 render 即注册两次 Promise.resolve().then(_ => console.log(6)) console.log(5) return <>{console.log('num:', num)}</> }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 11. 类组件与函数组件区别
类组件 | 函数组件 |
---|---|
面向对象编程(OOP) | 函数式编程(FP),更方便单元测试,未来趋势 |
需要创建实例,会占用一定内存 | 不需要创建实例,可以节约内存 |
烦人的 this | 好用的 Hooks(小心闭包) |
拥有完整的生命周期 | 可通过 Hooks 实现一些生命周期 |
主要通过高阶组件(HOC)实现逻辑复用(还可以用继承) | 主要通过 Hooks 实现逻辑复用(也可以用 HOC) |
babel 转换后代码更多 | babel 转换后代码更少 |