Open creamidea opened 2 years ago
React18 已经内置,packages/react-reconciler/src/ReactFiberHooks.new.js 3 个参数
mountSyncExternalStore 函数
如果没有 block 的优先级,那么就要放入源一致性检查
fiber.flags |= StoreConsistency
if (!includesBlockingLane(root, renderLanes)) {
pushStoreConsistencyCheck(fiber, getSnapshot, nextSnapshot);
}
这个检查在函数 performConcurrentWorkOnRoot 里,在 render 结束之后(The render completed.)会做一次检查,renderWasConcurrent && !isRenderConsistentWithExternalStores(finishedWork)
为真(A store was mutated in an interleaved event),再次调用 renderRootSync
。
获取当前对应的 hook,hook.memoizedState = nextSnapshot
等价于使用了 useEffect(subscribeToStore, [subscribe])
fiber.flags = PassiveEffect | PassiveStaticEffect;
effect.tag = HookHasEffect | HookPassive
hook.memoizedState = effect
// effect hook
create = subscribeToStore.bind(null, fiber, inst, subscribe)
deps = [subscribe]
后续调用,等价于 useEffect(updateStoreInstance) 其中没有设置 memoizedState 这步 updateStoreInstance 用于更新内部记录的外部源最新值。该函数被存储在 fiber.updateQueue*
pushEffect(
HookHasEffect | HookPassive,
updateStoreInstance.bind(null, fiber, inst, nextSnapshot, getSnapshot),
undefined,
null,
);
在 commitPassiveMountOnFiber 通过 commitHookEffectListMount(HookPassive | HookHasEffect, finishedWork) 函数内被读取消费
https://juejin.cn/post/6974025581557973005
import { useEffect, useState } from "react";
import { createPortal } from "react-dom";
function ToastButton() {
const [show, setShow] = useState(false);
useEffect(() => {
const handler = () => {
setShow(false);
};
document.addEventListener("click", handler);
return () => {
document.removeEventListener("click", handler);
};
}, [show]);
console.log('## ToastButton', show)
return (
<div>
<button onClick={() => {
setShow(true)
}}>toast button</button>
{show && <div>toast</div>}
</div>
);
}
function PortalButton() {
const [show, setShow] = useState(false);
return (
<div>
<button onClick={() => setShow(true)}>portal button</button>
{show && createPortal("portal", document.body)}
</div>
);
}
export function App() {
return <>
<ToastButton />
<PortalButton />
</>
}
toast 文案在点击 toast button 之后显示立即消失,视觉上就没有出现过
分析如下:
onClick 在捕获阶段冒泡阶段被触发执行,设置为 true 进入更新逻辑 setState
ensureRootIsScheduled
includesSomeLane(pendingPassiveEffectsLanes, SyncLane)
这里看这次更新是不是有同步,
有就调用 flushPassiveEffects。意味着这里会同步执行了 effect,即这里在 body 上注册监听事件 handler
以上都是同步,所以此时还只是冒泡到 div#root,继续往上来到 body,此时就会触发设置 false 的回调函数
toast 被删除pendingPassiveEffectsLanes 设置也是在 commitRoot 阶段 根据 rootDoesHavePassiveEffects 为真值时,设置
https://juejin.cn/post/7073692220313829407
import { useState } from "react";
export function App() {
const [num, updateNum] = useState(0);
console.log("App render", num);
return (
<div onClick={() => updateNum(1)}>
<Child />
</div>
);
}
function Child() {
console.log("child render");
return <span>child</span>;
}
App render 0
child render
App render 1
child render
此时 fiber(App),fiber.lanes = 1 和 fiber.alternate.lanes = 1 设置值
(猜测原因是:dispatchSetState
时绑定的 fiber 是第一次渲染时绑定的 fiber,引入并发之后,可能被中断,就要保证中断之后 lanes 上的信息不消失。所以 2 个都要设置)
在 begin work 阶段和 renderWithHook 内,都会重置 workInProgress 的 lanes workInProgress.lanes = NoLanes
。alternate 的 lanes 不会被重置
performUnitOfWork 内
begin work 阶段,判断 oldProps === newProps
为真(其父节点 HostRoot 是 cloneChildFibers
直接复制,所以 props 是相同的)。但是还是进入 updateFunctionComponent
处理,只是 didReceiveUpdate
为 false。didReceiveUpdate
将在 renderWithHooks
里,如同注释所说,Component 里调用 useState
等 Hooks 时,会根据 state 的变化情况,处理 didReceiveUpdate
// renderWithHooks
var children = Component(props, secondArg); // Check if there was a render phase update
// updateReducer(useState)
if (!objectIs(newState, hook.memoizedState)) {
markWorkInProgressReceivedUpdate();
}
处理完 begin work 之后,会执行下面的代码,将「未来」的 props 变成「现在」的 props
unitOfWork.memoizedProps = unitOfWork.pendingProps;
completeWork childLanes 冒泡处理是在 completeWork 内处理
App render 1
fiber(App) 没能进入 eagerState 处理逻辑,因为 alternate.lanes 不为 0
begin work 处理 fiber(App) 时,oldProps === newProps
为真(其父节点 HostRoot 是 cloneChildFibers
直接复制,所以 props 是相同的)。但是还是进入 updateFunctionComponent
处理,只是 didReceiveUpdate
为 false。
在 updateFunctionComponent
后半段代码
if (current !== null && !didReceiveUpdate) {
// 处理 workInProgress 的 updateQueue 和 flags
// 处理 current 的 lanes,去掉当前的更新优先级
bailoutHooks(current, workInProgress, renderLanes);
// !includesSomeLane(renderLanes, workInProgress.childLanes) 判断成功
// 直接返回 null。于是,fiber(Child) 进入 bailout 处理。没有让 Child 渲染。
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
}
于是,fiber(Child) 进入 bailout 处理。没有让 Child 渲染。
(None)
进入 eagerState 的处理逻辑,没有 state, props, context
变化,于是什么都不会发生 :)
import { useEffect, useState, useTransition } from "react";
export function App() {
console.log("render App");
const [count, setCount] = useState(0);
const [isPending, startTransition] = useTransition();
console.log('render', count, isPending)
useEffect(() => {
// debugger
// strict mode + development mode 时,执行 2 次,模拟 OffScreen
// https://reactjs.org/docs/strict-mode.html#ensuring-reusable-state
console.log('run effect');
}, []);
return (
<>
<button
onClick={() => {
startTransition(() => {
// 优先级 64
setCount(c => c + 1)
})
}}
>
{isPending
? 'pending...'
: `App Change ${count}`
}
</button>
</>
);
}
didReceiveUpdate
判断是否有 props, state, context
变化
第一次赋值,发生在 beginWork
,主要目的:有 props
和 context
的变化
第二次赋值,发生在 useState
阶段,主要目的:有 state
的变化
root.finishedWork = null;
和 root.finishedLanes = NoLanes;
root.callbackNode = null;
和 root.callbackPriority = NoLane;
workInProgressRoot
, workInProgress
, workInProgressRootRenderLanes
root.current = finishedWork;
dispatchSetState
开始hook.queue.pending
fiber.lanes === NoLanes && (alternate === null || alternate.lanes === NoLanes)
是否是第一个更新
scheduleUpdateOnFiber
调度任务 performSyncWorkOnRoot
或 performConcurrentWorkOnRoot
被执行
bailoutOnAlreadyFinishedWork
(省略一些特殊组件的处理)render()
或 FunctionComponentuseState(updateState - updateReducer)
就会被执行
bailoutHooks
(去掉 PassiveEffect 标记,在 flushPassiveEffects 内不执行)
if (!is(newState, hook.memoizedState)) {
markWorkInProgressReceivedUpdate();
}
符合条件的更新,会将 performConcurrentWorkOnRoot
放入调度器。符合分片的任务会执行 renderRootConcurrent
function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
if (workInProgressRoot !== root || workInProgressRootRenderLanes !== lanes) {
// workInProgressRoot, workInProgressRootRenderLanes 发生变化,说明在这之前调用了 prepareFreshStack 或者 commit 被执行
// 说明任务变了,被高优先级的任务抢占了,任务重新开始(第一次也会进入这里)
prepareFreshStack(root, lanes);
}
do {
try {
workLoopConcurrent();
break;
} catch (thrownValue) {
handleError(root, thrownValue);
}
} while (true);
// ...
}
workLoopConcurrent 里面有时间分片的逻辑 shouldYield
:现在是固定的时间分片时长 5ms。如果当前任务执行时间大于 5ms 就会退出 while
循环回到 performConcurrentWorkOnRoot,退出码为 RootInProgress
function workLoopConcurrent() {
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress);
}
}
退出码为 RootInProgress,再次进入 ensureRootIsScheduled
,此时 existingCallbackPriority === newCallbackPriority
为真,就直接退出 ensureRootIsScheduled
。
此时代码回到 performConcurrentWorkOnRoot
if (root.callbackNode === originalCallbackNode) {
// The task node scheduled for this root is the same one that's
// currently executed. Need to return a continuation.
return performConcurrentWorkOnRoot.bind(null, root);
}
callbackNode 和 originalCallbackNode 一致,没有发生任务抢占。将当前任务返回,让调度器下次调用。从而实现时间分片的能力,从 devtool performance 输出来看,就是 5 ms 的 Call
在分片的间隙,给浏览器有时间去处理其他任务。此时 timer 定时器触发高优先级更新,高优先级任务一般是同步任务 performSyncWorkOnRoot(在一个宏任务内),最后会调用 ensureRootIsScheduled。如果还有任务,即 root.pendingLanes
不为空,那么就会创建新的调度任务放入调度器等待执行(也就是被中断的任务需要从头开始,有些组件会被执行 2 次)。
如果高优先级任务是异步任务,那么就会进入调度器。调度器进行排序入队,开始执行。
A:会被取消。首先有一个背景知识点:全局只会有一个 root
所以在后续任务进入 ensureRootIsScheduled
,const existingCallbackNode = root.callbackNode;
就是获取上一个任务(最后一个时间分片的任务),接下来判断优先级,「肯定」是新任务的优先级更高,所以先取消上一个任务,再创建一个新的任务。在新的任务末尾,会重新调用 ensureRootIsScheduled
,将中断的任务重新安排唤起。
// 渲染几次?
// 渲染 3 次
// 第一次 mount
// 第二次 useEffect 注册的函数运行触发更新(状态值变了,is(newState, hook.memoizedState) 为 false。useState)
// 第三次 还是 useEffect 注册的函数运行触发更新(状态值没变,进入 bailout 阶段 bailoutHooks,此时去掉了 Passive Effect。updateFunctionComponent)
// 后续不打印,因为 useEffect 不运行
import { useEffect, useState } from "react";
export function App() {
const [, updateNum] = useState(0);
useEffect(() => {
updateNum(1)
})
console.log('render App');
return 'App';
}
// add 函数内部的函数 batching 执行,所以每次增加 1
// 在 React18 实现里面,会在 ensureRootIsScheduled 内注册 micro task: performSyncWorkOnRoot
// 也就是在一个宏任务内执行。且由于是 click 触发更新,lanes 是 1。不会被进入到异步任务调度
// 在 React17 里,会在 dispatchDiscreteEvent 里面调用 flush sync queue(里面会先取消注册的 schedule 调度,然后开始同步执行)
// 所以也没有进入 异步任务调度 执行
// 如果是在 componentDidMount 通过 setTimeout 触发更新
// 传统模式,首次渲染 2 (setTimeout 里面是同步执行)
// 更新优先级 16。该更新级由 getCurrentEventPriority 返回 DefaultEventPriority,没有事件就返回默认优先级 16
// 进入 schedule 调度逻辑进行调度 performConcurrentWorkOnRoot 执行(但是不会时间分片,16 属于 Block 范围)
// 并发模式,首次渲染 1
// 更新优先级 1。该更新级由 getCurrentEventPriority 返回 getCurrentUpdatePriority,CurrentUpdatePriority 会在合成事件系统里 dispatchDiscreteEvent 设置 setCurrentUpdatePriority(DiscreteEventPriority)
// 进入 schedule 调度逻辑进行调度 performConcurrentWorkOnRoot 执行(但是不会时间分片,16 属于 Block 范围)
import { Component } from "react";
export class App extends Component {
state = {
count: 0,
};
add = () => {
this.setState({
count: this.state.count + 1,
});
this.setState({
count: this.state.count + 1,
});
}
componentDidMount() {
setTimeout(this.add)
}
render() {
return (
<div>
<div>{this.state.count}</div>
<button onClick={this.add}>Add</button>
</div>
);
}
}
import { useEffect, useLayoutEffect, useState } from "react";
export function App() {
const [state, setState] = useState(0)
useLayoutEffect(() => {
setState(1)
}, [])
useEffect(() => {
setState(2)
setState(3)
}, [])
console.log('render', state)
return <div>{state}</div>
}
React 18 并发模式
render 0
render 1
render 3
原因:useEffect 注册的函数是在 flushPassiveEffects
中执行(由 useLayoutEffect 中 setState 注册的 performSyncWorkOnRoot 调用)
export function flushPassiveEffects(): boolean {
if (rootWithPendingPassiveEffects !== null) {
...
// 最高优先级为 DefaultEventPriority,也就是 16
const priority = lowerEventPriority(DefaultEventPriority, renderPriority);
...
try {
ReactCurrentBatchConfig.transition = null;
// 设置当前总更新的优先级为 16
// 这里表明,从 useEffect 里触发最高为 16
// 这里的优先级会影响 dispatchSetState 里 requestUpdateLane 的返回值,这个例子返回 16
setCurrentUpdatePriority(priority);
return flushPassiveEffectsImpl();
} finally {
...
}
}
return false;
}
从该代码可以得知,useEffect 里面触发的更新最高更新也只会是 16。这样就会和 useLayoutEffect 触发的更新「错开」(因为 useLayoutEffect 触发的更新为 1)。这就导致 React 会先执行完 useLayoutEffect 触发的更新,也就是第二次输出 render 1
。然后在其 commit 阶段的最后,调用 ensureRootIsScheduled 函数,将还未执行的 16 优先级注册到调度器内,所以第三次输出就是 performConcurrentWorkOnRoot 函数引发的。
legacy 模式
render 0
render 3
传统模式和并发模式的区别在于 requestUpdateLane 始终返回的是 1。也就是 useEffect 和 useLayoutEffect 的优先级一致。那么在 useState 计算状态的时候,就不会跳过,这也就是为什么第二次渲染 3。也因为优先级相同,在 commit 阶段的最后,root.pendingLanes
为 0,这样 ensureRootIsScheduled 也就不会安排后续任务。所以总共输出 2 次。
function requestUpdateLane(fiber) {
...
if ((mode & ConcurrentMode) === NoMode) {
return SyncLane;
} else if ( (executionContext & RenderContext) !== NoContext && workInProgressRootRenderLanes !== NoLanes) {
...
}
...
}
首次渲染时,DOM 是一次性插入
简要的更新过程
以 FunctionComponent 和 一次 click 内 setState 为例
setState
省略 React 处理事件过程,直接进入 onClick 处理函数调用 setState,这里的 setState 就是 dispatchSetState 函数。
fiber.lanes === NoLane && (!fiber.alternate || fiber.alternate.lanes === NoLane
),则会被设置为 true,并计算出 state 记录。如果计算的 state 和当前 state 一致,则直接退出本次更新scheduleUpdateOnFiber
if 分支处理
ensureRootIsScheduled 以 Root 为参数,安排调度 performSyncWorkOnRoot
if 分支处理:没有获取到调度优先级
nextLanes === NoLanes 说明没有需要做的更新,重置相关变量,退出更新
getHighestPriorityLane(nextLanes) 取出最高优先级的任务作为本次更新的任务调度优先级
if 分支处理:本次更新和当前更新(如果有)比较
scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root))
supportsMicrotasks
,那么安排一下flushSyncCallbacks
(这里面会执行 performSyncWorkOnRoot,也就是将 React 工作安排在同一个浏览器 task 里面)scheduleCallback(ImmediateSchedulerPriority, flushSyncCallbacks)
,这就会被安排到下一个 task 立即执行scheduleCallback(schedulerPriorityLevel, performConcurrentWorkOnRoot.bind(null, root))
安排到下一个浏览器 taskperformSyncWorkOnRoot
开始遍历 root 前的一些准备工作
flushPassiveEffects 如果存在 rootWithPendingPassiveEffects(这个会在 commitRoot 阶段被设置。如果存在说明上一次任务执行完,调度的 effect 还没有执行。那么就需要在本次更新任务执行前,执行完成。留给本次更新一个干净的环境),则执行
getNextLanes 再次获取当前任务的优先级 if 分支判断:优先级
不包含 SyncLane,则将本次更新调用 ensureRootIsScheduled 重新安排,并退出本次更新,等重新调度
包含 SyncLane,调用 renderRootSync 开始遍历
👆函数执行完成有一个退出码
React 核心工作:Render + Commit
Render 阶段
Commit 阶段
重置当前任务的一些状态
处理已经完成的赛道(任务)
其他状态
是否有副作用,有的话安排调度 useEffect 等。👇会判断是否有同步,如果有那么就会同步执行 useEffect
如果有内容更新,比如 DOM 变化等。首先设置执行状态
executionContext |= CommitContext
。然后是 3 大步骤:commitBeforeMutationEffects
getSnapshotBeforeUpdate
,方向是 child -> parentcommitMutationEffects
case HostComponent
时没有break
也没有return
,于是就会进入👇 HostText 的处理)commitLayoutEffects
设置 useEffect 执行需要的状态
再次安排调度 ensureRootIsScheduled,确定没有需要更新的内容
如果本次任务有同步的优先级,那么同步执行 useEffect(针对的是离散型事件,比如点击,让其同步执行)
处理剩余的任务 remainingLanes
循环调用检查,就是那个 50 次
调用 flushSyncCallbacks,确保同步任务都被执行完成(If layout work was scheduled, flush it now.)
结束
此时使用
updateState
,来到updateReducer
处理函数