Comments (9)
useSyncExternalStore
React18 已经内置,packages/react-reconciler/src/ReactFiberHooks.new.js
3 个参数
- subscribe 函数类型,其参数是 React 给出的更新函数 handleStoreChange,当外部源有更新时可以调用,通知 React 更新
- getSnapshot 和 getServerSnapshot 是给 React 获取当前外部源最新状态的函数,用于内部新/旧比较,决定是否触发更新
mountSyncExternalStore 函数
如果没有 block 的优先级,那么就要放入源一致性检查
fiber.flags |= StoreConsistency
- fiber.updateQueue 上放入 check 的方式
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,
);
附
fiber.updateQueue 何时消费?
在 commitPassiveMountOnFiber 通过 commitHookEffectListMount(HookPassive | HookHasEffect, finishedWork) 函数内被读取消费
from creamidea.github.com.
升级React17,Toast组件不能用了
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
- 安排 performSyncWorkOnRoot 执行
- 调度微任务 flushSyncCallbacks 任务(scheduleMicrotask),flushSyncCallbacks 里面执行的函数就是上面的 performSyncWorkOnRoot
微任务是在宏任务最后阶段,还没有离开宏任务,所以还是处于 onClick 调用栈内
performSyncWorkOnRoot 「同步执行」,在 commitRoot 阶段,执行完
BeforeMutation -> Mutation -> LayoutMutation 之后,有一段代码
includesSomeLane(pendingPassiveEffectsLanes, SyncLane)
这里看这次更新是不是有同步,
有就调用 flushPassiveEffects。意味着这里会同步执行了 effect,即这里在 body 上注册监听事件 handler
以上都是同步,所以此时还只是冒泡到 div#root,继续往上来到 body,此时就会触发设置 false 的回调函数
toast 被删除
pendingPassiveEffectsLanes 设置也是在 commitRoot 阶段
根据 rootDoesHavePassiveEffects 为真值时,设置
- rootWithPendingPassiveEffects = root
- pendingPassiveEffectsLanes = lanes;
from creamidea.github.com.
React内部的性能优化没有达到极致?
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
变化,于是什么都不会发生 :)
from creamidea.github.com.
useTransition 使用
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>
</>
);
}
from creamidea.github.com.
didReceiveUpdate
didReceiveUpdate
判断是否有 props, state, context
变化
第一次赋值,发生在 beginWork
,主要目的:有 props
和 context
的变化
第二次赋值,发生在 useState
阶段,主要目的:有 state
的变化
React 几个比较,决定是否渲染
- lanes(当前和之前)
- eagerState(第一个更新才有)
- props(直接引用比较)
- context(lazy or directly)
- state(call FunctionComponent 才能比较)
React 几个关键状态重置
- renderRootSync/renderRootConcurrent 阶段可能会 prepareFreshStack(重置 rootWorkInProgress, workInProgress, ...)
- beginWork 会清除 Wip.lanes
- runWithHooks 会清除 Wip.lanes
- useState 会清除 queue.pending (setState 之后的队列)
- commit before mutation 之前
- 重置
root.finishedWork = null;
和root.finishedLanes = NoLanes;
- 重置
root.callbackNode = null;
和root.callbackPriority = NoLane;
- 同时重置
workInProgressRoot
,workInProgress
,workInProgressRootRenderLanes
- markRootFinished 处理 pendingLanes, suspendedLanes, entangledLanes, ...
- 重置
- commit mutation 之后,重置
root.current = finishedWork;
更新从 dispatchSetState
开始
- 更新 update 入队
hook.queue.pending
fiber.lanes === NoLanes && (alternate === null || alternate.lanes === NoLanes)
是否是第一个更新- eagerState 处理,状态计算之后没有发生,直接退出。(没有 perform work 进入调度)
- 进入
scheduleUpdateOnFiber
调度任务 performSyncWorkOnRoot
或 performConcurrentWorkOnRoot
被执行
- beginWork: 判断 props 是否发生变化(引用比较,浅比较)
- 没有发生变化,且 context 也没有发生变化,进入
bailoutOnAlreadyFinishedWork
(省略一些特殊组件的处理)- 子组件没有更新,返回 null,进入回溯阶段 complete work
- 有更新,clone 直接子组件,返回其第一个子组件
- 没有发生变化,且 context 也没有发生变化,进入
- 进入 updateXXX 逻辑之后,就有机会调用
render()
或 FunctionComponent - FunctionComponent 被调用之后,
useState(updateState - updateReducer)
就会被执行- 执行过程,操作更新链表,计算符合优先级的更新得出状态 例子
- useState 内部会比较新状态和旧状态是否一致。决定后续是否执行
bailoutHooks
(去掉 PassiveEffect 标记,在 flushPassiveEffects 内不执行)
if (!is(newState, hook.memoizedState)) {
markWorkInProgressReceivedUpdate();
}
- render 阶段,commit 阶段 ...
时间分片的原理
符合条件的更新,会将 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 次)。
如果高优先级任务是异步任务,那么就会进入调度器。调度器进行排序入队,开始执行。
Q: 时间分片,在每个分片最后会注册任务,那么当高优先级抢占时,这个任务会被取消吗?
A:会被取消。首先有一个背景知识点:全局只会有一个 root
所以在后续任务进入 ensureRootIsScheduled
,const existingCallbackNode = root.callbackNode;
就是获取上一个任务(最后一个时间分片的任务),接下来判断优先级,「肯定」是新任务的优先级更高,所以先取消上一个任务,再创建一个新的任务。在新的任务末尾,会重新调用 ensureRootIsScheduled
,将中断的任务重新安排唤起。
from creamidea.github.com.
// 渲染几次?
// 渲染 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';
}
from creamidea.github.com.
// 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>
);
}
}
from creamidea.github.com.
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) {
...
}
...
}
from creamidea.github.com.
首次渲染时,DOM 是一次性插入
from creamidea.github.com.
Related Issues (20)
- Vuex 的一张图
- 未来十年技术预言
- Python Import Problem HOT 1
- Nodejs 之 Worker 简单分析 HOT 4
- Nodejs 启动分析 HOT 1
- 2021 碎碎念
- 理解数学意义上的排列组合
- JavaScript 内存相关 HOT 1
- JSX 是声明式“语言”
- React v17.0.2 源码分析笔记 HOT 3
- react hydrate 模式
- webpack 的 __webpack_public_path__ 工作原理
- Rust 外部依赖和被链接 HOT 1
- React Route 中 ErrorBoundary 死循环问题 HOT 2
- React 开发总结
- React Array 检查 Key 原理 HOT 1
- React fiber(HostRoot) payload{element} 的作用
- React 事件处理 HOT 2
- 基于 Homebrew 的 bottle 机制在 Catanalina 安装 tmux
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from creamidea.github.com.