Create by 柚子uccs 2023-02-18
react-study's Introduction
react-study's People
react-study's Issues
useEffect 返回的函数是怎么执行的
在掌握 React 组件树遍历技巧中说到 react
是怎么遍历 dom
那么在遍历的过程中,如果发现当前节点有子节点被删除了,那么 react
会怎么处理呢?
下面是源码简化:这里是完整的源码
function recursivelyTraversePassiveUnmountEffects(parentFiber: Fiber): void {
const deletions = parentFiber.deletions;
if ((parentFiber.flags & ChildDeletion) !== NoFlags) {
if (deletions !== null) {
for (let i = 0; i < deletions.length; i++) {
const childToDelete = deletions[i];
nextEffect = childToDelete;
commitPassiveUnmountEffectsInsideOfDeletedTree_begin(
childToDelete,
parentFiber
);
}
}
}
}
function commitPassiveUnmountEffectsInsideOfDeletedTree_begin(
deletedSubtreeRoot: Fiber,
nearestMountedAncestor: Fiber | null
) {
while (nextEffect !== null) {
const fiber = nextEffect;
// 执行 passive effects 返回的函数
commitPassiveUnmountInsideDeletedTreeOnFiber(fiber, nearestMountedAncestor);
const child = fiber.child;
if (child !== null) {
child.return = fiber;
nextEffect = child;
} else {
commitPassiveUnmountEffectsInsideOfDeletedTree_complete(
deletedSubtreeRoot
);
}
}
}
function commitPassiveUnmountEffectsInsideOfDeletedTree_complete(
deletedSubtreeRoot
) {
while (nextEffect !== null) {
const fiber = nextEffect;
const sibling = fiber.sibling;
const returnFiber = fiber.return;
if (fiber === deletedSubtreeRoot) {
nextEffect = null;
return;
}
if (sibling !== null) {
sibling.return = returnFiber;
nextEffect = sibling;
return;
}
nextEffect = returnFiber;
}
}
deletions
在正式开始之前,我们要了解一个 fiber
的属性:deletions
这个属性存放的是当前节点中被删除的 fiber
,这个数组是在 commit
阶段被赋值的
如果有被删除的节点,这个属性值是一个数组,如果没有被删除的节点,这个属性值是 null
const A = () => {
useEffect(() => {
return () => {
console.log("A unmount");
};
}, []);
return <div>文本A</div>;
};
const B = () => {
useEffect(() => {
return () => {
console.log("B unmount");
};
}, []);
return <div>文本B</div>;
};
如果 App
组件这样写,那么 deletions
的值是 [FiberNode, FiberNode]
const App(){
const [count, setCount] = useState(0)
return <div>
{count % 2 === 0 && <A />}
{count % 2 === 0 && <B />}
<div onClick={()=> setCount(count+1)}>+1</div>
</div>
}
如果 App
组件这样写,那么 deletions
的值是 [FiberNode]
const App(){
const [count, setCount] = useState(0)
return <div>
{count % 2 === 0 && <><A /><B /></>}
<div onClick={()=> setCount(count+1)}>+1</div>
</div>
}
对于第二种情况,react
会把 A
组件和 B
组件作为一个整体,所以 deletions
的值是 [FiberNode]
处理当前节点的 deletions
react
在遍历 fiber tree
时,会先处理当前的 fiber
的 deletions
,等处理完之后再遍历下一个 fiber
现在我们已经知道 deletions
中保存的是当前 fiber
下被删除的子节点
这时 react
会遍历 deletions
数组,然后执行每个 fiber
的 passive effect
返回的函数
但是有个问题,如果 deletions
中的 fiber
有子节点,那么这些子节点也会被删除,这时 react
会怎么处理呢?
这里分两种情况来讨论:
- 删除的
fiber
没有子节点:<div>{xxxx && <A />}</div>
- 删除的
fiber
有子节点:<div>{xxxx && <><A /><B /></>}</div>
-->
删除的 fiber 没有子节点:<div>{xxxx && <A />}</div>
这种情况比较好理解
当遍历到 div
时,因为 <A/>
节点会被卸载,所以在 div
的 deletions
保存了一个 <A/>
的 fiber
遍历 deletions
数组,执行 <A/>
的 passive effect
返回的函数
如下图所示:
删除的 fiber 有子节点:<div>{xxxx && <><A /><B /></>}</div>
这种情况就比较复杂了
当遍历到 div
时,<></>
节点会被卸载,所以在 div
的 deletions
保存了一个 <></>
的 fiber
遍历 deletions
数组,执行 fiber
的 passive effect
返回的函数,对于 <></>
来说是不存在的 passive effect
那么这个时候就要去遍历它的 child.fiber
,也就是 <A/>
和 <B/>
首先拿到第一个 fiber,也就是 <A/>
,然后执行 <A/>
的 passive effect
返回的函数,这步比较好理解
child = fiber.child;
if (child !== null) {
nextEffect = child;
}
这里遍历也是深度优先,遍历一个 child
,执行一个 passive effect
返回函数,然后再遍历下一个 child
(这边 <A />
已经是叶子节点了)
然后拿到第二个 fiber
,也就是 <B/>
,然后执行 <B/>
的 passive effect
返回的函数,这步就不太好理解了
child = fiber.child;
if (child !== null) {
nextEffect = child;
} else {
commitPassiveUnmountEffectsInsideOfDeletedTree_complete(deletedSubtreeRoot);
}
这里要注意的是:
react
在寻找有 passive effect
的 fiber
时,只遍历到有 passive effect
的 fiber
, 像 div
这种没有 passive effect
就不会遍历
但是在处理 deletions
,react
会遍历所有的 fiber
,也就是说从当前的 fiber
开始,一直往下遍历到叶子节点,这个叶子节点是指文本节点这种,往下不会有节点了(对于 A
组件来说 文本A
是文本节点)
然后在开始往上遍历,往上遍历是调用 commitPassiveUnmountEffectsInsideOfDeletedTree_complete
函数,直到遍历到 deletionRoot
,在向上遍历的过程中会检查是否有 sibling
,如果有说明 sibling
还没被处理,这样就找到了 <B/>
,然后执行 <B/>
的 passive effect
返回的函数
如下图所示:
向下遍历和向上遍历
在处理 deletions
时,对于每个 deletedNode
,都先向下遍历,然后再向上遍历
- 向下遍历:
commitPassiveUnmountEffectsInsideOfDeletedTree_begin
(深度优先,优先处理左边的节点) - 向上遍历:
commitPassiveUnmountEffectsInsideOfDeletedTree_complete
(之后再处理右边节点)
总结
1. 遍历 deletions 数组:
react
在处理deletions
时,先沿着fiber tree
向下遍历,如果有passive effect
返回的函数,则执行- 一直遍历到没有
child
的fiber
,再向上遍历,处理sibling
- 再向上遍历时,如果如果遇到
sibling
,再向下遍历,向下遍历时遇到passive effect
返回的函数,则执行 - 如此循环直到遍历到
deletedNode
,结束遍历
2. 结合掌握 React 组件树遍历技巧
- 遍历寻找有
passive effect
节点react
从根组件向下遍历,如果没有passive effect
,则不会遍历
- 遍历时,如果遇到当前节点有
deletions
时,会暂停寻找passive effect
节点- 进入遍历
deletions
数组
- 进入遍历
react 遍历 deletions 完整逻辑如下图所示:
图中绿色部分是遍历 deletionsNode
过程,红色部分是遍历寻找 passive effect
过程
React Lane 算法:一文详解 8 种 Lane 操作
什么是 lane
lane
是 react@17
中用于表示任务的优先级,是对 expirationTime
的重构
lane
是一个 32
位的二进制数,每个二进制位表示 1
种优先级,优先级最高的 SyncLane
为 1
,其次为 2
、4
、8
等
lanes
是一个整数,该整数所有为二进制位为 1
对应的优先级任务都将被执行
注意:
lane
的长度是31
位,react
这么做是为了避免符号位参与运算
react
对 lane
的操作有 8
种:
lane & lane
lane & lanes
lanes & ~lane
lanes1 & lanes2
lane | lane
lanes2 |= lanes1 & lane
lane *= 2
和lane <<= 1
lanes & -lanes
在下面将会举例详细介绍这些操作,这里先介绍一下 lane
的值:
lanes
值为 0b0000000011111111111111110000000
,表示有多个任务的优先级。
TransitionLane1
值为 0b0000000000000000000000010000000
,表示单个任务的优先级
TransitionLane2
值为 0b0000000000000000000000100000000
,表示单个任务的优先级
lane & lane
用来判断是不是同一个 lane
,两个 lane
是否有相同的位为 1
(取交集)
比如:lane & TransitionLane1
,如果 lane
的值为 0b0000000000000000000000010000000
,则输出 0b0000000000000000000000010000000
,否则输出 0
用于 getLabelForLane 函数
lane & lanes
用来判断 lanes
中是否有 lane
,是否有相同的位为 1
(取交集)
如果想判断 lanes
中是否有 lane
,进行如下计算:
将 TransitionLane1
和 lanes
进行按位与,得到 lane & lanes
,它的值是 0b0000000000000000000000010000000
,和 TransitionLane1
值相同,说明 lanes
中有 TransitionLane1
任务
用于 isTransitionLane 等函数
lanes & ~lane
用来从 lanes
中删除 lane
(取差集)
如果想去从 lanes
中删掉 lane
,具体步骤如下:
- 对
TransitionLane1
取反,得到~lane
,即0b1111111111111111111111101111111
- 对
lanes
和~lane
进行按位与运算,得到lanes & ~lane
,即0b0000000011111111111111100000000
- 这样就把
lanes
中的TransitionLane1
置为了0
,也就是去掉了这个任务 - 如果
lane
不在lanes
中,那么lanes & ~lane
的值就是lanes
,即0b0000000011111111111111110000000
用于 getNextLanes 等函数
lanes1 & lanes2
用于判断 lanes1
中是否有 lane
属于 lanes2
(取交集)
如果想判断 lanes1
中是否有 lane
属于 lanes2
,进行如下计算:
- 假设
lanes2
为SyncDefaultLanes
,它是由InputContinuousHydrationLane | InputContinuousLane | DefaultHydrationLane | DefaultLane
组成的,即0b0000000000000000000000000111100
- 当
lanes1
的3 ~ 6
位为1
,即lanes1
为0b0000000000000000000000000111100
- 则
lanes1 & lanes2
的值为lanes1
,即0b0000000000000000000000000111100
,说明lanes1
中有lanes2
中的lane
- 如果
lanes1
中没有lane
属性lanes2
,那么lanes1 & lanes2
的值为0
这种用法有种变形:lanes & (lane | lane)
用于 includesNonIdleWork、includesSyncLane 等函数
lane | lane
用于将多个 lane
合并为一个 lanes
(取并集)
合并两个 lane
,TransitionLane1 | TransitionLane2
,得到的值为 0b0000000000000000000000110000000
用于 markHiddenUpdate 等函数
lanes2 |= lanes1 & lane
用于将 lanes1
中的 lane
合并到 lanes2
中(先取交集,再取并集)
这种写法等于:lanes2 = lanes2 | (lanes1 & lane)
如果想从 lanes1
中取出 lane
,并将它合并到 lanes2
中,进行如下计算:
lanes1
为InputContinuousHydrationLane | InputContinuousLane
,即0b0000000000000000000000000001100
lanes2
为DefaultHydrationLane | DefaultLane
,即0b0000000000000000000000000110000
lane
为InputContinuousLane
,即0b0000000000000000000000000001000
lanes1 & lane
的值为InputContinuousLane
,即0b0000000000000000000000000001000
lanes2 |= lanes1 & lane
的值为DefaultHydrationLane | DefaultLane | InputContinuousLane
,即0b0000000000000000000000000111100
lanes2
中多了InputContinuousLane
这个任务
用于 markRootMutableRead、markRootPinged 等函数
lane *= 2 和 lane <<= 1
都是将 lane
左移一位,一般来说位运算比乘法运算快
TransitionLane1 *= 2
和 TransitionLane1 <<= 1
的结果都是 0b0000000000000000000000100000000
用于 getLaneLabelMap、claimNextRetryLane 等函数
lanes & -lanes
从 lanes
中找出最高优先级的 lane
如果想找出 lanes
中最高优先级的 lane
,进行如下计算:
- 对
lanes
取反,得到~lanes
,即0b1111111100000000000000001111111
- 末尾加
1
,得到-lanes
,即0b1111111100000000000000010000000
- 对
lanes
和-lanes
进行按位与运算,得到lanes & -lanes
,即0b0000000000000000000000010000000
- 这样就找出了
lanes
中最高优先级的lane
用于 getHighestPriorityLane 函数
补充说明:
下面二进制数都是 32
位带符号位二进制数
~ 是按位取反
十进制数 4
,按位取反是 -5
,记做 ~4
,计算逻辑如下:
- 将十进制数
4
转换为二进制数为0b00000000000000000000000000000100
- 按位取反,即将
1
变为0
,将0
变为1
,得到0b11111111111111111111111111111011
。 - 符号位不变,其他位取反,得到
0b10000000000000000000000000000100
- 末位加
1
,得到0b10000000000000000000000000000101
- 将二进制数转换为十进制数,得到
-5
。
js 中按取反
js
中按位取反只是一个按位取反,并不表示按位取反后的数是实际的负数
- 十进制整数
4
,转换为二进制数为0b00000000000000000000000000000100
- 按位取反,即将
1
变为0
,将0
变为1
,得到0b11111111111111111111111111111011
4 & ~4
结果是 0
因为 4
的二进制数为 0b00000000000000000000000000000100
,~4
的二进制数为 0b11111111111111111111111111111011
,不是 0b10000000000000000000000000000101
也就是说 ~4
的二进制数是 4
的二进制数取反
js 中的负数
十进制整数 14
,负数为 -14
14 & -14
结果是 2
为什么结果会是 2
呢?
因为 14
的二进制数为 0b00000000000000000000000000001110
,-14
的二进制数为 0b11111111111111111111111111110010
,不是 0b10000000000000000000000000001110
也就是说,-14
的二进制数是 14
的二进制数取反后再加 1
最后
- 在
js
中对于二进制数操作要特别小心:~
是按位取反(末尾不加一),-
取反末尾加一 -lane === (~lane + 1)
总结
lane & lane
:用来判断是不是同一个lane
(是否有相同的位为1
,取交集)lane & lanes
:用来判断lanes
中是否有lane
(是否有相同的位为1
,取交集)lanes & ~lane
:用来从lanes
中删除lane
(取差集)lanes1 & lanes2
:用于判断lanes1
中是否有lane
属于lanes2
(取交集)lane | lane
:用于将多个lane
合并为一个lanes
(取并集)lanes2 |= lanes1 & lane
:用于将lanes1
中的lane
合并到lanes2
中(先取交集,再取并集)lane *= 2
和lane <<= 1
:都是将lane
左移一位lanes & -lanes
:从lanes
中找出最高优先级的lane
ReactDOM.createRoot 被调用时在做什么(代码片段)
ReactDOM.createRoot()
时会挂载事件,内部会调用 listenToAllSupportedEvents
函数
// rootContainerElement 是 <div id="root"></div>
listenToAllSupportedEvents(rootContainerElement);
listenToAllSupportedEvents
listenToAllSupportedEvents
内部具体做了什么,大致分为 3 步:
-
listeningMarker
这变量是一个36
位随机数,这里用来保证事件只注册一次// toString(radix) radix 是进制 // 36 进制,则 0-9 用 0-9 表示,10-35 用 a-z 表示 const listeningMarker = "_reactListening" + Math.random().toString(36).slice(2);
-
遍历所有的原生事件
allNativeEvents
(如何处理原生事件名,在下面【原生事件收集一章中】)-
react
将原生事件按照是否支持冒泡,分为:allNativeEvents
支持冒泡nonDelegatedEvents
不支持冒泡
react
这么做的目的是因为,react
将所有的事件都绑定在根元素上,在react@18
中根元素是div#root
的dom
元素,不支持冒泡的事件,就绑定在这个元素上 -
单独处理
selectionchange
事件,将它绑定到document
上
-
-
listenToNativeEvent
是具体挂载事件的函数(具体在下面 【listenToNativeEvent 一章中】)
// rootContainerElement 是 <div id="root"></div>
function listenToAllSupportedEvents(rootContainerElement) {
// 保证事件只注册一次
if (!rootContainerElement[listeningMarker]) {
rootContainerElement[listeningMarker] = true;
// 遍历 set 集合 allNativeEvents,监听冒泡和捕获阶段
allNativeEvents.forEach((domEventName) => {
// We handle selectionchange separately because it
// doesn't bubble and needs to be on the document.
// 单独处理 selectionchange
if (domEventName !== "selectionchange") {
// delegate 委托,不需要委托的事件,这些事件不会冒泡
if (!nonDelegatedEvents.has(domEventName)) {
/**
* @param domEventName - 事件名,如 click
* @param isCapturePhaseListener- 是否是捕获阶段 false,非 nonDelegatedEvents
* @param target - <div id="root"><div>
*/
listenToNativeEvent(domEventName, false, rootContainerElement);
}
/**
* @param domEventName - 事件名,如 click
* @param isCapturePhaseListener- 是否是捕获阶段 true,nonDelegatedEvents
* @param target - <div id="root"><div>
*/
listenToNativeEvent(domEventName, true, rootContainerElement);
}
});
// 是否是 document 元素,ownerDocument 可以获取某个 dom 的 document 元素
const ownerDocument =
rootContainerElement.nodeType === DOCUMENT_NODE
? rootContainerElement
: rootContainerElement.ownerDocument;
if (ownerDocument !== null) {
// The selectionchange event also needs deduplication
// but it is attached to the document.
if (!ownerDocument[listeningMarker]) {
ownerDocument[listeningMarker] = true;
listenToNativeEvent("selectionchange", false, ownerDocument);
}
}
}
}
原生事件收集
react
把事件分为 5
类,分别由各事件插件进行处理:
SimpleEventPlugin
简单事件,代表事件onClick
EnterLeaveEventPlugin
鼠标进出事件,代表事件onMouseEnter
ChangeEventPlugin
表单修改事件,代表事件onChange
SelectEventPlugin
选择事件,代表事件onSelect
BeforeInputEventPlugin
输入前事件,代表事件onBeforeInput
简单事件是指,只依赖自身的事件,比如:onClick
只依赖 click
。而有些事件是依赖其他事件的,比如 onMouseEnter
依赖 ["mouseout", "mouseover"]
,react
这么做简化了开发者自己监听事件的步骤,不然开发者要实现 onMouseEnter
这种事件,开发者分别需要监听 mouseout
和 mouseover
事件
SimpleEventPlugin.registerEvents(); // click, cancel, input, keyUp => onClick, onCancel, onInput, onKeyup ...
EnterLeaveEventPlugin.registerEvents(); // mouseout, mouseover => onMouseEnter, onMouseLeave ...
ChangeEventPlugin.registerEvents(); // change, click, focusin, focusout, input, keydown, keyup, selectionchange => onChange
SelectEventPlugin.registerEvents(); // focusout, contextmenu, dragend, focusin, keydown, keyup, mousedown, mouseup, selectionchange => onSelect
BeforeInputEventPlugin.registerEvents(); // compositionend, keypress, textInput, paste => onBeforeInput ...
这 5
个插件内部都调用了 registerTwoPhaseEvent
,这个函数的作用分别为事件注册捕获和冒泡事件
function registerTwoPhaseEvent(registrationName, dependencies) {
// 冒泡 onClick
registerDirectEvent(registrationName, dependencies);
// 捕获 onCaptureClick
registerDirectEvent(registrationName + "Capture", dependencies);
}
在 registerDirectEvent
函数内部做了两件事:
registrationNameDependencies
建立映射关系- 将所有的原生事件保存到
allNativeEvents
变量中
// registrationNameDependencies 保存 react 事件和其依赖的事件的映射
// {onClick: ["click"], onCapture: ["click"], onMouseEnter: ["mouseout", "mouseover"] ... }
const registrationNameDependencies = {};
// allNativeEvents 集合中就会有 click、cancel 等 81 种事件,["click", "cancel"...]
const allNativeEvents = new Set();
function registerDirectEvent(registrationName, dependencies) {
registrationNameDependencies[registrationName] = dependencies;
for (let i = 0; i < dependencies.length; i++) {
allNativeEvents.add(dependencies[i]);
}
}
listenToNativeEvent
这个函数做了一次转发,在调用 addTrappedEventListener
之前,算出捕获还是冒泡的 flag
ps: 不知道为什么
react
有很多这种类型的函数,这种写法复用性很强吗?
/**
* @param domEventName - 事件名,如 click
* @param isCapturePhaseListener - 是否是捕获阶段,false 代表冒泡,true 代表捕获
* @param target - <div id="root"></div>,如果是 selectionChange 事件,这个值是 document
*/
function listenToNativeEvent(domEventName, isCapturePhaseListener, target) {
// 冒泡是 0,捕获是 4
let eventSystemFlags = 0;
if (isCapturePhaseListener) {
eventSystemFlags |= IS_CAPTURE_PHASE;
}
/**
* @param targetContainer - <div id="root"></div>,如果是 selectionChange 事件,这个值是 document
* @param domEventName - 事件名,如 click
* @param eventSystemFlags - 事件类型标志,冒泡是 0,捕获是 4
* @param isCapturePhaseListener - 是否是捕获阶段,false 代表冒泡,true 代表捕获
*/
addTrappedEventListener(
target,
domEventName,
eventSystemFlags,
isCapturePhaseListener
);
}
addTrappedEventListener
这函数有一部分不会运行代码,我这里直接忽略了
它主要做了 3
件事:
createEventListenerWrapperWithPriority
构造事件监听器- 判断浏览器是否支持
passive
,如果支持将touchstart/touchmove/wheel
需要将passive
设置为true
- 根据
isCapturePhaseListener
的值挂载的事件是冒泡还是捕获
react
挂载的事件永远不会卸载,并且一个事件只会挂在一次
/**
* @param targetContainer - <div id="root"></div>,如果是 selectionChange 事件,这个值是 document
* @param domEventName - 事件名,如 click
* @param eventSystemFlags - 事件类型标志,冒泡是 0,捕获是 4
* @param isCapturePhaseListener - 是否是捕获阶段
*/
function addTrappedEventListener(
targetContainer,
domEventName,
eventSystemFlags,
isCapturePhaseListener
) {
/**
* 构造事件监听器
* @return 事件监听函数,这里的 listener 函数接收一个事件参数 e,内部叫做 nativeEvent,就是 x.addEventListener("click", e => {}) 中的 e
* @param targetContainer - <div id="root"></div>,如果是 selectionChange 事件,这个值是 document
* @param domEventName - 事件名,如 click
* @param eventSystemFlags - 事件类型标志,冒泡是 0,捕获是 4
*/
let listener = createEventListenerWrapperWithPriority(
targetContainer,
domEventName,
eventSystemFlags
);
// touchstart touchmove wheel 需要将 passive 设置为 true
let isPassiveListener = undefined;
if (passiveBrowserEventsSupported) {
if (
domEventName === "touchstart" ||
domEventName === "touchmove" ||
domEventName === "wheel"
) {
isPassiveListener = true;
}
}
let unsubscribeListener;
/**
* 根据是否是捕获阶段,挂载不同的事件
* @param targetContainer - <div id="root"></div>,如果是 selectionChange 事件,这个值是 document
* @param domEventName - 事件名,如 click
* @param listener
*/
if (isCapturePhaseListener) {
if (isPassiveListener !== undefined) {
// 捕获事件,passive 为 true
unsubscribeListener = addEventCaptureListenerWithPassiveFlag(
targetContainer,
domEventName,
listener,
isPassiveListener
);
} else {
// 捕获事件,passive 为 false
unsubscribeListener = addEventCaptureListener(
targetContainer,
domEventName,
listener
);
}
} else {
if (isPassiveListener !== undefined) {
// 冒泡事件,passive 为 true
unsubscribeListener = addEventBubbleListenerWithPassiveFlag(
targetContainer,
domEventName,
listener,
isPassiveListener
);
} else {
// 冒泡事件,passive 为 false
unsubscribeListener = addEventBubbleListener(
targetContainer,
domEventName,
listener
);
}
}
}
createEventListenerWrapperWithPriority
react
将事件按照优先级分为:
- 离散事件优先级,例如:
click/input
等触发的更新任务,优先级为2
(0b0000000000000000000000000000010
) - 连续事件优先级,例如:
wheel/mouseout/mouseover
等,连续触发的事件,优先级为8
(0b0000000000000000000000000001000
) - 默认事件优先级,例如:
setTimeout
触发的更新任务,优先级为32
(0b0000000000000000000000000100000
) - 闲置事件优先级,优先级最低,优先级为
536870912
(0b0100000000000000000000000000000
)
通过 getEventPriority
函数获取事件优先级(message
事件单独处理):
- 如果是离散型,调用
dispatchDiscreteEvent
函数 - 如果是连续型,调用
dispatchContinuousEvent
函数, - 都不是,则调用
dispatchEvent
函数
dispatchDiscreteEvent
和 dispatchContinuousEvent
的作用是设置当前的事件优先级,然后再调用 dispatchEvent
完成事件函数处理
/**
* 根据优先级构造事件监听器
* @return 事件监听函数,这里的 listener 函数接收一个事件参数 e,内部叫做 nativeEvent,就是 x.addEventListener("click", e => {}) 中的 e
* @param targetContainer - <div id="root"></div>,如果是 selectionChange 事件,这个值是 document
* @param domEventName - 事件名,如 click
* @param eventSystemFlags - 事件类型标志,冒泡是 0,捕获是 4
*/
function createEventListenerWrapperWithPriority(
targetContainer,
domEventName,
eventSystemFlags
) {
// 获取事件优先级:离散型最高,连续型其次,接着默认事件
const eventPriority = getEventPriority(domEventName);
let listenerWrapper;
switch (eventPriority) {
// 离散事件
case DiscreteEventPriority: // 0b0000000000000000000000000000010
listenerWrapper = dispatchDiscreteEvent;
break;
// 连续事件
case ContinuousEventPriority: // 0b0000000000000000000000000001000
listenerWrapper = dispatchContinuousEvent;
break;
// 默认事件
case DefaultEventPriority: // 0b0000000000000000000000000100000
default:
listenerWrapper = dispatchEvent;
break;
}
/**
* @param this - null
* @param domEventName - 事件名,如 click
* @param eventSystemFlags - 事件类型标志,冒泡是 0,捕获是 4
* @param targetContainer - <div id="root"></div>,如果是 selectionChange 事件,这个值是 document
* @description dispatchEvent 函数接收四个参数,其中三个参数:domEventName、eventSystemFlags、targetContainer 这里已经绑定
* @description 还有一个 nativeEvent 参数是 DOM 事件参数,xx.addEventListener("click", e => {}) 这里的 e 就是 nativeEvent,由外部传进来
*/
return listenerWrapper.bind(
null,
domEventName,
eventSystemFlags,
targetContainer
);
}
dispatchEvent
通过判断 enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay
是为 true
分别调用 dispatchEventWithEnableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay
和 dispatchEventOriginal
但是 enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay
变量是写死的,一直为 true
ps:
react
中有很多这样的写法,我也不知道为什么要这样写
/**
* @param domEventName - 事件名称
* @param eventSystemFlags - 事件类型标志,捕获阶段是 4,冒泡阶段是 0
* @param targetContainer - <div id="root"><div>
* @param nativeEvent - 浏览器的 event
*/
function dispatchEvent(
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
targetContainer: EventTarget,
nativeEvent: AnyNativeEvent
): void {
if (!_enabled) {
return;
}
if (enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay) {
/**
* @param domEventName - 事件名称
* @param eventSystemFlags - 事件类型标志,捕获阶段是 4,冒泡阶段是 0
* @param targetContainer - <div id="root"><div>
* @param nativeEvent - 浏览器的 event
*/
dispatchEventWithEnableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay(
domEventName,
eventSystemFlags,
targetContainer,
nativeEvent
);
} else {
dispatchEventOriginal(
domEventName,
eventSystemFlags,
targetContainer,
nativeEvent
);
}
}
dispatchEventWithEnableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay
这个函数主要调用了 2
个函数:
findInstanceBlockingEvent
dispatchEventForPluginEventSystem
findInstanceBlockingEvent
主要作用是查找当前事件是否被阻塞,从当前节点递归向上遍历节点,一直到根节点,找到阻塞事件(这个组件是否有事件在执行),如果有返回这个节点的 fiber
,否者返回 null
dispatchEventForPluginEventSystem
找到祖先实例,批量调用 dispatchEventsForPlugins
(如果未开启批量,则开启批量调用)
/**
* @param domEventName - 事件名称
* @param eventSystemFlags - 事件类型标志,捕获阶段是 4,冒泡阶段是 0
* @param targetContainer - <div id="root"><div>
* @param nativeEvent - 浏览器的 event
*/
function dispatchEventWithEnableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay(
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
targetContainer: EventTarget,
nativeEvent: AnyNativeEvent
) {
// 从 nativeEvent 中获取到 e.target,然后找到对应的 fiber,将 fiber 赋值给 return_targetInst
// 查找当前事件处理函数是否被阻塞了
// 从当前组件开始,递归向上遍历组件树,找到阻塞的事件(这个组件是否有事件在执行),如果有返回这个 fiber,如果没有返回 null
// createRoot 时,blockedOn 为 null
let blockedOn = findInstanceBlockingEvent(
domEventName,
eventSystemFlags,
targetContainer,
nativeEvent
);
if (blockedOn === null) {
/**
* @param domEventName - 事件名称
* @param eventSystemFlags - 事件类型标志,捕获阶段是 4,冒泡阶段是 0
* @param nativeEvent - 浏览器的 event
* @param targetInst - return_targetInst,nativeEvent 中的 e.target 所对应的 fiber
* @param targetContainer - <div id="root"><div>
*/
// 将原生事件转化为合成事件,并将合成事件传递给后续的事件处理
dispatchEventForPluginEventSystem(
domEventName,
eventSystemFlags,
nativeEvent,
return_targetInst,
targetContainer
);
// 将连续触发类型的事件重置为无事件
clearIfContinuousEvent(domEventName, nativeEvent);
return;
}
}
findInstanceBlockingEvent
- 通过调用
getEventTarget
函数找到目标节点(e.target
) - 通过调用
getClosestInstanceFromNode
函数找到目标节点对应的(fiber
) - 通过调用
getNearestMountedFiber
函数找到目标节点最近挂载点
再正常情况下页面都只有一个事件在执行,这个函数返回返回的都是 null
// 查找当前事件是否被阻塞,从当前节点递归向上遍历节点,一直到根节点,找到阻塞事件(这个组件是否有事件在执行),如果有返回这个节点的 fiber,否者返回 null
export function findInstanceBlockingEvent(
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
targetContainer: EventTarget,
nativeEvent: AnyNativeEvent
): null | Container | SuspenseInstance {
// TODO: Warn if _enabled is false.
return_targetInst = null;
// 获取原生事件的 target,当前节点(e.target)这种
// 在 vite 创建的项目中,react logo 会有动画,这里的 nativeEvent 是 animationstart 事件
// 同时 img 还有个 load 事件
const nativeEventTarget = getEventTarget(nativeEvent);
// 获取当前节点(e.target)对应的 fiber
let targetInst = getClosestInstanceFromNode(nativeEventTarget);
if (targetInst !== null) {
/**
* @param {Fiber} targetInst - 当前节点(e.target)对应的 fiber
* @description 在 createRoot 时 nearestMounted 为 targetInst
*/
const nearestMounted = getNearestMountedFiber(targetInst);
if (nearestMounted === null) {
// This tree has been unmounted already. Dispatch without a target.
targetInst = null;
} else {
const tag = nearestMounted.tag;
// 在 createRoot 时,下面的代码不会执行
if (tag === SuspenseComponent) {
const instance = getSuspenseInstanceFromFiber(nearestMounted);
if (instance !== null) {
// Queue the event to be replayed later. Abort dispatching since we
// don't want this event dispatched twice through the event system.
// TODO: If this is the first discrete event in the queue. Schedule an increased
// priority for this boundary.
return instance;
}
// This shouldn't happen, something went wrong but to avoid blocking
// the whole system, dispatch the event without a target.
// TODO: Warn.
targetInst = null;
} else if (tag === HostRoot) {
const root: FiberRoot = nearestMounted.stateNode;
if (isRootDehydrated(root)) {
// If this happens during a replay something went wrong and it might block
// the whole system.
return getContainerFromFiber(nearestMounted);
}
targetInst = null;
} else if (nearestMounted !== targetInst) {
// If we get an event (ex: img onload) before committing that
// component's mount, ignore it for now (that is, treat it as if it was an
// event on a non-React tree). We might also consider queueing events and
// dispatching them after the mount.
targetInst = null;
}
}
}
return_targetInst = targetInst;
// We're not blocked on anything.
return null;
}
dispatchEventForPluginEventSystem
/**
* 调度事件处理函数的核心函数
* @param domEventName - 事件名称
* @param eventSystemFlags - 事件类型标志,捕获阶段是 4,冒泡阶段是 0
* @param nativeEvent - 浏览器的 event
* @param targetInst - return_targetInst,nativeEvent 中的 e.target 所对应的 fiber
* @param targetContainer - <div id="root"><div>
*/
// 找到祖先实例,批量调用 dispatchEventsForPlugins(如果未开启批量,则开启批量调用)
export function dispatchEventForPluginEventSystem(
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
nativeEvent: AnyNativeEvent,
targetInst: null | Fiber,
targetContainer: EventTarget
): void {
let ancestorInst = targetInst;
/**
* @param domEventName - 事件名称
* @param eventSystemFlags - 事件类型标志,捕获阶段是 4,冒泡阶段是 0
* @param nativeEvent - 浏览器的 event
* @param targetInst - ancestorInst,nativeEvent 中的 e.target 所对应的 fiber
* @param targetContainer - createRoot 时 <div id="root"><div>
*/
// 找到祖先实例后执行 dispatchEventsForPlugins(如果未开启批量,则开启批量调用)
batchedUpdates(() =>
dispatchEventsForPlugins(
domEventName,
eventSystemFlags,
nativeEvent,
ancestorInst,
targetContainer
)
);
}
dispatchEventsForPlugins
这个函数内部调用了 react
内部两个核心函数
extractEvents
processDispatchQueue
extractEvents
作用:从原生事件中提取合成事件,并放入 dispatchQueue
队列中
processDispatchQueue
作用:从 dispatchQueue
中取出合成事件,并根据事件类型和目标元素,找到对应的监听器并执行
dispatchQueue
是一个事件队列,包含两个属性:event
和 listeners
event
是合成对象listeners
是数组,里面包含了监听函数
在 dispatchEventsForPlugins
执行前,dispatchQueue
是个空数组,执行完之后,dispatchQueue
可能就包含事件了
/**
* @param domEventName - 事件名称
* @param eventSystemFlags - 事件类型标志,捕获阶段是 4,冒泡阶段是 0
* @param nativeEvent - 浏览器的 event
* @param targetInst - ancestorInst,nativeEvent 中的 e.target 所对应的 fiber
* @param targetContainer - createRoot 时 <div id="root"><div>
*/
function dispatchEventsForPlugins(
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
nativeEvent: AnyNativeEvent,
targetInst: null | Fiber,
targetContainer: EventTarget
): void {
// 获取原生事件的 target,e.target 这种
const nativeEventTarget = getEventTarget(nativeEvent);
// 事件类型是:DispatchEntry
// 包含两个属性:event 和 listeners
// - event 是一个 SyntheticEvent 对象
// - isPersistent: () => boolean(是否是连续的)
// - isPropagationStopped: () => boolean
// - _dispatchInstances?: null | Array<Fiber | null> | Fiber
// - _dispatchListeners?: null | Array<Function> | Function
// - _targetInst: Fiber
// - nativeEvent: Event(原生事件)
// - target?: mixed
// - relatedTarget?: mixed
// - type: string
// - currentTarget: null | EventTarget
// - listeners 是一个数组
// - instance: null | Fiber
// - listener: Function(事件函数,比如 onClick)
// - currentTarget: EventTarget
const dispatchQueue: DispatchQueue = [];
/**
* @param dispatchQueue - []
* @param domEventName - 事件名称
* @param eventSystemFlags - 事件类型标志,捕获阶段是 4,冒泡阶段是 0
* @param nativeEvent - 浏览器的 event
* @param nativeEventTarget - 原生事件对应的 target
* @param targetInst - ancestorInst,nativeEvent 中的 e.target 所对应的 fiber
* @param targetContainer - createRoot 时 <div id="root"><div>
*/
// 从原生事件中提取合成事件,并放入 dispatchQueue 队列中
// - 遍历 target(事件目标节点) 到 targetContainer(根节点) 之间的所有节点
// - 检查它们是否有注册过相应类型的事件监听器
// - 如果有,创建一个 SyntheticEvent 对象,包含了 type、target、currentTarget、eventPhase、timestamp 等。
extractEvents(
dispatchQueue,
domEventName,
targetInst,
nativeEvent,
nativeEventTarget,
eventSystemFlags,
targetContainer
);
// 从 dispatchQueue 中取出合成事件,并根据事件类型和目标元素,找到对应的监听器并执行
// - 遍历 dispatchQueue 中的每个 SyntheticEvent 对象
processDispatchQueue(dispatchQueue, eventSystemFlags);
}
extractEvents
由于 react
把事件分为 5
类,所以从原生事件中提取合成事件也也会分成五种
/**
* @param dispatchQueue - []
* @param domEventName - 事件名称
* @param eventSystemFlags - 事件类型标志,捕获阶段是 4,冒泡阶段是 0
* @param nativeEvent - 浏览器的 event
* @param nativeEventTarget - 原生事件对应的 target
* @param targetInst - ancestorInst,nativeEvent 中的 e.target 所对应的 fiber
* @param targetContainer - createRoot 时 <div id="root"><div>
*/
function extractEvents(
dispatchQueue: DispatchQueue,
domEventName: DOMEventName,
targetInst: null | Fiber,
nativeEvent: AnyNativeEvent,
nativeEventTarget: null | EventTarget,
eventSystemFlags: EventSystemFlags,
targetContainer: EventTarget
) {
SimpleEventPlugin.extractEvents(
dispatchQueue,
domEventName,
targetInst,
nativeEvent,
nativeEventTarget,
eventSystemFlags,
targetContainer
);
const shouldProcessPolyfillPlugins =
(eventSystemFlags & SHOULD_NOT_PROCESS_POLYFILL_EVENT_PLUGINS) === 0;
// 冒泡阶段执行,捕获阶段不执行
if (shouldProcessPolyfillPlugins) {
EnterLeaveEventPlugin.extractEvents(
dispatchQueue,
domEventName,
targetInst,
nativeEvent,
nativeEventTarget,
eventSystemFlags,
targetContainer
);
ChangeEventPlugin.extractEvents(
dispatchQueue,
domEventName,
targetInst,
nativeEvent,
nativeEventTarget,
eventSystemFlags,
targetContainer
);
SelectEventPlugin.extractEvents(
dispatchQueue,
domEventName,
targetInst,
nativeEvent,
nativeEventTarget,
eventSystemFlags,
targetContainer
);
BeforeInputEventPlugin.extractEvents(
dispatchQueue,
domEventName,
targetInst,
nativeEvent,
nativeEventTarget,
eventSystemFlags,
targetContainer
);
}
}
SimpleEventPlugin.extractEvents
从原生事件中提取合成事件,并放入 dispatchQueue
队列中
- 遍历
target
(事件目标节点) 到targetContainer
(根节点) 之间的所有节点 - 检查它们是否有注册过相应类型的事件监听器
- 如果有,创建一个
SyntheticEvent
对象,包含了type
、target
、currentTarget
、eventPhase
、timestamp
等
- 如果有,创建一个
- 调用
topLevelEventsToReactNames
,获取react
事件名 - 根据事件名绑定合成事件对象
SyntheticKeyboardEvent
SyntheticFocusEvent
SyntheticMouseEvent
SyntheticDragEvent
SyntheticTouchEvent
SyntheticAnimationEvent
SyntheticTransitionEvent
SyntheticUIEvent
SyntheticWheelEvent
SyntheticClipboardEvent
SyntheticPointerEvent
- 收集合成事件的监听器:遍历目标节点到祖先节点,找到所有注册了该事件类型的监听器,将它们存储在
dispatchQueue
dispatchQueue
类型是DispatchListener[]
,它有这些属性:instance: null | Fiber
listener: Function
,例如:<div onClick={xxx} />
xxx 函数currentTarget: EventTarget
,例如:button
/**
* @param dispatchQueue - []
* @param domEventName - 事件名称
* @param eventSystemFlags - 事件类型标志,捕获阶段是 4,冒泡阶段是 0
* @param nativeEvent - 浏览器的 event
* @param nativeEventTarget - 原生事件对应的 target
* @param targetInst - ancestorInst,nativeEvent 中的 e.target 所对应的 fiber
* @param targetContainer - createRoot 时 <div id="root"><div>
*/
function extractEvents(
dispatchQueue: DispatchQueue,
domEventName: DOMEventName,
targetInst: null | Fiber,
nativeEvent: AnyNativeEvent,
nativeEventTarget: null | EventTarget,
eventSystemFlags: EventSystemFlags,
targetContainer: EventTarget
): void {
// click => onClick
// domEventName: click
// reactName: onClick
const reactName = topLevelEventsToReactNames.get(domEventName);
if (reactName === undefined) {
return;
}
let SyntheticEventCtor = SyntheticEvent;
let reactEventType: string = domEventName;
// 根据事件名绑定相应的合成事件:
// - SyntheticKeyboardEvent
// - SyntheticFocusEvent
// - SyntheticMouseEvent
// - SyntheticDragEvent
// - SyntheticTouchEvent
// - SyntheticAnimationEvent
// - SyntheticTransitionEvent
// - SyntheticUIEvent
// - SyntheticWheelEvent
// - SyntheticClipboardEvent
// - SyntheticPointerEvent
switch (domEventName) {
case "keypress":
// Firefox creates a keypress event for function keys too. This removes
// the unwanted keypress events. Enter is however both printable and
// non-printable. One would expect Tab to be as well (but it isn't).
if (getEventCharCode(((nativeEvent: any): KeyboardEvent)) === 0) {
return;
}
/* falls through */
case "keydown":
case "keyup":
SyntheticEventCtor = SyntheticKeyboardEvent;
break;
case "focusin":
reactEventType = "focus";
SyntheticEventCtor = SyntheticFocusEvent;
break;
case "focusout":
reactEventType = "blur";
SyntheticEventCtor = SyntheticFocusEvent;
break;
case "beforeblur":
case "afterblur":
SyntheticEventCtor = SyntheticFocusEvent;
break;
case "click":
// Firefox creates a click event on right mouse clicks. This removes the
// unwanted click events.
if (nativeEvent.button === 2) {
return;
}
/* falls through */
case "auxclick":
case "dblclick":
case "mousedown":
case "mousemove":
case "mouseup":
// TODO: Disabled elements should not respond to mouse events
/* falls through */
case "mouseout":
case "mouseover":
case "contextmenu":
SyntheticEventCtor = SyntheticMouseEvent;
break;
case "drag":
case "dragend":
case "dragenter":
case "dragexit":
case "dragleave":
case "dragover":
case "dragstart":
case "drop":
SyntheticEventCtor = SyntheticDragEvent;
break;
case "touchcancel":
case "touchend":
case "touchmove":
case "touchstart":
SyntheticEventCtor = SyntheticTouchEvent;
break;
case ANIMATION_END:
case ANIMATION_ITERATION:
case ANIMATION_START:
SyntheticEventCtor = SyntheticAnimationEvent;
break;
case TRANSITION_END:
SyntheticEventCtor = SyntheticTransitionEvent;
break;
case "scroll":
SyntheticEventCtor = SyntheticUIEvent;
break;
case "wheel":
SyntheticEventCtor = SyntheticWheelEvent;
break;
case "copy":
case "cut":
case "paste":
SyntheticEventCtor = SyntheticClipboardEvent;
break;
case "gotpointercapture":
case "lostpointercapture":
case "pointercancel":
case "pointerdown":
case "pointermove":
case "pointerout":
case "pointerover":
case "pointerup":
SyntheticEventCtor = SyntheticPointerEvent;
break;
default:
// Unknown event. This is used by createEventHandle.
break;
}
const inCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) !== 0;
const accumulateTargetOnly =
!inCapturePhase &&
// TODO: ideally, we'd eventually add all events from
// nonDelegatedEvents list in DOMPluginEventSystem.
// Then we can remove this special list.
// This is a breaking change that can wait until React 18.
domEventName === "scroll";
/**
* @param targetFiber - targetInst,ancestorInst,nativeEvent 中的 e.target 所对应的 fiber
* @param reactName - react 事件名,例如 onClick
* @param nativeEventType - nativeEvent.type
* @param inCapturePhase - 例如:load 在捕获和冒泡阶段都会触发
* @param accumulateTargetOnly - 是不是 scroll
* @param nativeEvent - 浏览器的 event
*/
// 收集合成事件的监听器
// 遍历目标节点到祖先节点,找到所有注册了该事件类型的监听器,将它们存储在一个数组中
// 数组类型是 DispatchListener[],它有这些属性:
// - instance: null | Fiber
// - listener: Function 例如:<div onClick={xxx} /> xxx 函数
// - currentTarget: EventTarget,例如如 button
const listeners = accumulateSinglePhaseListeners(
targetInst,
reactName,
nativeEvent.type,
inCapturePhase,
accumulateTargetOnly,
nativeEvent
);
if (listeners.length > 0) {
// Intentionally create event lazily.
// 创建一个合成事件 SyntheticEvent,放到队列中
const event: ReactSyntheticEvent = new SyntheticEventCtor(
reactName,
reactEventType,
null,
nativeEvent,
nativeEventTarget
);
dispatchQueue.push({ event, listeners });
}
}
accumulateSinglePhaseListeners
遍历目标节点到祖先节点,找到所有注册了该事件类型的监听器,存储在 DispatchListener[]
/**
* @param targetFiber - nativeEvent 中的 e.target 所对应的 fiber
* @param reactName - react 事件名,例如 onClick
* @param nativeEventType - nativeEvent.type
* @param inCapturePhase - 例如:load 在捕获和冒泡阶段都会触发
* @param accumulateTargetOnly - 是不是 scroll
* @param nativeEvent - 浏览器的 event
*/
// 收集合成事件的监听器
// 遍历目标节点到祖先节点,找到所有注册了该事件类型的监听器,将它们存储在一个数组中
// 数组类型是 DispatchListener[],它有这些属性:
// - instance: null | Fiber
// - listener: Function
// - currentTarget: EventTarget
export function accumulateSinglePhaseListeners(
targetFiber: Fiber | null,
reactName: string | null,
nativeEventType: string,
inCapturePhase: boolean,
accumulateTargetOnly: boolean,
nativeEvent: AnyNativeEvent
): Array<DispatchListener> {
// 捕获阶段的事件名:onClickCapture
// 冒泡阶段的事件名:onClick
const captureName = reactName !== null ? reactName + "Capture" : null;
const reactEventName = inCapturePhase ? captureName : reactName;
let listeners: Array<DispatchListener> = [];
// 目标属性,
let instance = targetFiber;
let lastHostComponent = null;
while (instance !== null) {
// 用 vite 创建的项目
// stateNode: img, tag: 5
// stateNode: a , tag: 5
// stateNode: div, tag: 5
// stateNode: div.app, tag: 5
// stateNode: null, tag: 5
// stateNode: null, tag: 8
// stateNode: FiberRootNode, tag: 3
// tag 类型是 WorkTag,文件:packages/react-reconciler/src/ReactWorkTags.js
// stateNode 与 fiber 相关的 dom
const { stateNode, tag } = instance;
// Handle listeners that are on HostComponents (i.e. <div>)
if (
(tag === HostComponent ||
(enableFloat ? tag === HostHoistable : false) ||
(enableHostSingletons ? tag === HostSingleton : false)) &&
stateNode !== null
) {
lastHostComponent = stateNode;
// Standard React on* listeners, i.e. onClick or onClickCapture
if (reactEventName !== null) {
/**
* @param inst - targetFiber,nativeEvent 中的 e.target 所对应的 fiber
* @param registrationName - reactEventName: isCapture ? onClickCapture : onClick
*/
const listener = getListener(instance, reactEventName);
if (listener != null) {
listeners.push(
createDispatchListener(instance, listener, lastHostComponent)
);
}
}
}
if (accumulateTargetOnly) {
break;
}
instance = instance.return;
}
return listeners;
}
getListener
/**
* @param inst - targetFiber,nativeEvent 中的 e.target 所对应的 fiber
* @param registrationName - isCapture ? onClickCapture : onClick
*/
export default function getListener(
inst: Fiber,
registrationName: string
): Function | null {
const stateNode = inst.stateNode;
if (stateNode === null) {
// Work in progress (ex: onload events in incremental mode).
return null;
}
const props = getFiberCurrentPropsFromNode(stateNode);
if (props === null) {
// Work in progress.
return null;
}
// 从 props 中获取到事件
// <div onClick={xxx} />
const listener = props[registrationName];
// 如果 inst.type 是 button,input, select, textarea
// 并且 props.disabled 为 true
// 则 onClick, ... 不会有监听器
if (shouldPreventMouseEvent(registrationName, inst.type, props)) {
return null;
}
if (listener && typeof listener !== "function") {
throw new Error(
`Expected \`${registrationName}\` listener to be a function, instead got a value of \`${typeof listener}\` type.`
);
}
return listener;
}
processDispatchQueue
从 dispatchQueue
中取出合成事件,并根据事件类型和目标元素,找到对应的监听器并执行
- 遍历
dispatchQueue
中的每个SyntheticEvent
对象
export function processDispatchQueue(
dispatchQueue: DispatchQueue,
eventSystemFlags: EventSystemFlags
): void {
const inCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) !== 0;
for (let i = 0; i < dispatchQueue.length; i++) {
const { event, listeners } = dispatchQueue[i];
processDispatchQueueItemsInOrder(event, listeners, inCapturePhase);
}
rethrowCaughtError();
}
processDispatchQueueItemsInOrder
// 捕获阶段是从 root 向下传播一直到 target
// - [rootListener, ..., targetListener]
// - 模拟捕获,就需要从最后一个节点开始执行回调函数
// 冒泡阶段是从 target 向上传播传播一直到 root
// - [targetListener, ..., rootListener]
// - 模拟冒泡,就需要从第一个节点开始执行回调函数
// 在每次执行回调函数之前,检查当前是否处于批量更新模式(batchedUpdates)
// - 如果不是,则开启批量更新模式,并在所有回调函数执行完毕后再关闭批量更新模式。这样可以避免多次渲染组件,提高性能
/**
*
* @param event - 合成事件
* @param dispatchListeners - DispatchListener[]
* DispatchListener:
* - instance: null | Fiber,对应的 fiber
* - listener: Function,事件函数
* - currentTarget: EventTarget,例如 button
* @param inCapturePhase - 是否是捕获阶段
*/
function processDispatchQueueItemsInOrder(
event: ReactSyntheticEvent,
dispatchListeners: Array<DispatchListener>,
inCapturePhase: boolean
): void {
let previousInstance;
if (inCapturePhase) {
for (let i = dispatchListeners.length - 1; i >= 0; i--) {
const { instance, currentTarget, listener } = dispatchListeners[i];
if (instance !== previousInstance && event.isPropagationStopped()) {
return;
}
/**
* @param event - 例如:button 对应的 fiber
* @param listener - 例如:事件函数
* @param currentTarget - 例如:button
*/
executeDispatch(event, listener, currentTarget);
previousInstance = instance;
}
} else {
for (let i = 0; i < dispatchListeners.length; i++) {
const { instance, currentTarget, listener } = dispatchListeners[i];
if (instance !== previousInstance && event.isPropagationStopped()) {
return;
}
executeDispatch(event, listener, currentTarget);
previousInstance = instance;
}
}
}
executeDispatch
function executeDispatch(
event: ReactSyntheticEvent,
listener: Function,
currentTarget: EventTarget
): void {
const type = event.type || "unknown-event";
event.currentTarget = currentTarget;
// 用于在执行回调函数时捕获第一个错误并记录它
// 这个方法是在 React 的错误处理机制中使用的,它确保了在 React 组件中的回调函数中发生的错误不会导致整个应用程序崩溃
// 如果在回调函数中发生错误,React 会记录错误并继续执行应用程序,而不是崩溃
invokeGuardedCallbackAndCatchFirstError(type, listener, undefined, event);
event.currentTarget = null;
}
最后
react
在 createRoot
阶段所作的事情全部结束,接下来将进入 render
阶段
剖析 React 任务调度机制:scheduleCallback 实现原理
unstable_scheduleCallback 函数是 react
任务调度核心函数,主要作用是根据任务的优先级进行任务调度
它接收一个优先级(priorityLevel
)、一个回调函数(callback
)和一个延迟时间({delay: number}
)作为参数,返回一个任务对象
这个函数主要做了三件事:
- 设置任务开始时间
- 设置任务过期时间
- 构建任务队列
- 根据任务优先级,调度任务
- 延时任务和非延时任务调度
这个函数的核心内容是任务队列构建和任务调度,它们分别在【构建任务队列】和【延时任务调度和非延时任务调度】中介绍
设置任务开始时间
通过 getCurrentTime()
获取当前时间,然后加上延迟时间(如果有),得到任务开始时间
延迟时间是从外面传递进来的,可能与 startTransition
有关,它可以在不阻塞主线程的情况下执行一些低优先级的更新
var currentTime = getCurrentTime();
var startTime;
if (typeof options === "object" && options !== null) {
var delay = options.delay;
if (typeof delay === "number" && delay > 0) {
startTime = currentTime + delay;
} else {
startTime = currentTime;
}
} else {
startTime = currentTime;
}
设置任务过期时间
任务过期时间:startTime + timeout
timeout
的值取决于任务优先级(priorityLevel
),它们的对应关系如下:
ImmediatePriority
:-1
(立即执行)UserBlockingPriority
:250
(250ms
后执行)NormalPriority
:5000
(5s
后执行)LowPriority
:10000
(10s
后执行)IdlePriority
:1073741823
(1073741.823s
后执行)
react
这么设计是因为 expirationTime
的值越小,表示任务越紧急,就需要优先执行
这里有一点需要注意:
IdlePriority
这个优先级的过期时间大概在12
天,这意味着几乎不会过期,通常用于一些不紧急的后台任务,比如数据预加载、用户不可见的更新等- 这个优先级的任务会被延迟到其他更重要的任务都完成后再执行
var timeout;
switch (priorityLevel) {
case ImmediatePriority:
timeout = IMMEDIATE_PRIORITY_TIMEOUT;
break;
case UserBlockingPriority:
timeout = USER_BLOCKING_PRIORITY_TIMEOUT;
break;
case IdlePriority:
timeout = IDLE_PRIORITY_TIMEOUT;
break;
case LowPriority:
timeout = LOW_PRIORITY_TIMEOUT;
break;
case NormalPriority:
default:
timeout = NORMAL_PRIORITY_TIMEOUT;
break;
}
var expirationTime = startTime + timeout;
根据任务优先级,调度任务
新建任务
task
是一个任务,有以下几个属性:
type Task = {
id: number;
callback: Callback | null;
priorityLevel: PriorityLevel;
startTime: number;
expirationTime: number;
sortIndex: number;
isQueued?: boolean;
};
这些属性的具体作用:
id
: 使用的是一个全局变量taskIdCounter
,每次创建任务的时候都会自动加一,用于区分不同的任务callback
:一个函数,表示要执行的任务,它会在调度器安排好时间后被调用- 这个取决于你在里面做什么,比如更新组件的状态、渲染组件、或者执行其他逻辑
priorityLevel
:优先级,会影响任务何时被执行,有五种:ImmediatePriority
、UserBlockingPriority
、IdlePriority
、LowPriority
和NormalPriority
startTime
:任务开始时间,它是使用getCurrentTime
函数获取的微秒级别的当前时间expirationTime
:任务过期的时间,它是根据startTime
和priorityLevel
计算出来的。过期时间越小,表示任务越紧急,越需要优先执行sortIndex
:任务在队列中的排序索引,默认为-1
。它实际上会被设置成startTime
或expirationTime
,以便在插入队列时按照升序排列- 保证过期时间越小,越紧急的任务排在最前面
isQueued
:任务是否已经被加入到队列中。这个属性是为了避免重复插入或删除任务而设置的- 在性能分析模式下,会加入这个属性,目前这个值没有启用
如何构建任务队列(见【构建任务队列】)
任务调度
根据任务开始时间(startTime
)和当前时间(currentTime
)来判断是否是延迟任务
- 如果是,就把任务加入到
timerQueue
中,并根据任务的开始时间,安排一个定时器(requestHostTimeout
)来执行任务; - 如果不是,就把任务加入到
taskQueue
中,并根据任务的过期时间,安排一个回调函数(requestHostCallback
)来执行任务
timerQueue
:用来存放延迟任务,也就是那些还没有到达执行时间的任务。它们会在定时器触发后被执行
taskQueue
:用来存放非延迟任务,也就是那些可以立即执行的任务。它们会在浏览器空闲时被执行
如果任务的 startTime
大于 currentTime
,说明任务还没有到达执行的时间,需要等待一段时间(因为立即执行的任务开始 timeout
是 -1
)
执行过程:
-
对于延迟任务,排序索引是开始时间(升序排序,索引:
newTask.sortIndex = startTime
)- 把延迟任务放到
timerQueue
队列中(push(timerQueue, newTask)
) - 判定当前是否有其他的非延迟任务(
peek(taskQueue) === null
)或者更早的延迟任务(newTask === peek(timeQueue)
)peek(taskQueue) === null
说明非延迟任务列表没有任务newTask === peek(timeQueue)
说明刚加入的任务就是最紧急的任务- 是否已经安排了一个定时
- 是:取消定时器
- 不是:标记已经安排了一个定时器
- 为什么要这么做:说明当前的任务比之前的任务紧急
requestHostTimeout(handleTimeout, startTime - currentTime)
是设置一个定时器,在startTime - currentTime
这么长的时间后执行handleTimeout
这个函数,它会从timerQueue
中取出最紧急的延迟任务并执行它。
- 把延迟任务放到
-
对于非延迟任务,排序索引是过期时间(升序排列,排序索引:
newTask.sortIndex = expirationTime
)- 把非延迟任务放到
taskQueue
队列中(push(taskQueue, newTask)
) - 判断当前是否已经安排了一个回调函数(
!isHostCallbackScheduled
)或者正在执行工作(!isPerformingWork
)requestHostCallback(flushWork)
安排一个回调函数,在浏览器空闲时执行flushWork
这个函数,它会从taskQueue
中取出最紧急的非延迟任务并执行它。
- 把非延迟任务放到
任务调度的具体过程(见【延时任务调度和非延时任务调度】)
if (startTime > currentTime) {
newTask.sortIndex = startTime;
push(timerQueue, newTask);
if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
if (isHostTimeoutScheduled) {
cancelHostTimeout();
} else {
isHostTimeoutScheduled = true;
}
requestHostTimeout(handleTimeout, startTime - currentTime);
}
} else {
newTask.sortIndex = expirationTime;
push(taskQueue, newTask);
if (!isHostCallbackScheduled && !isPerformingWork) {
isHostCallbackScheduled = true;
requestHostCallback(flushWork);
}
}
构建任务队列
构建任务队列时,根据任务的 sortIndex
优先级,安排任务的执行顺序
timerQueue
:sortIndex
是startTime
taskQueue
:sortIndex
是expirationTime
timerQueue
和 taskQueue
队列是个最小堆,执行时每次取出堆顶任务
如何保证元素添加到堆中后,能够快速放到合适的位置呢?
构建最小堆
一个元素的父节点的索引,就是那个元素的索引除以 2
,向下取整
由于这里使用数组存放二叉堆,所以元素索引的方式:(index - 1) >>> 1
- 无符号右移,返回值一定是一个非负数
index - 1
的作用是对应数组的索引,因为数组的索引是从0
开始的
遍历条件通过判断父元素索引是否大于 0
来实现最少遍历次数
比如说当前的索引是 12
,要在 12
后面插入一个新的元素:
11 -> 5
5 -> 2
2 -> 1
通过三次遍历就能找到将一个元素放合适位置了
function push<T: Node>(heap: Heap<T>, node: T): void {
const index = heap.length;
heap.push(node);
siftUp(heap, node, index);
}
function siftUp<T: Node>(heap: Heap<T>, node: T, i: number): void {
let index = i;
while (index > 0) {
const parentIndex = (index - 1) >>> 1;
const parent = heap[parentIndex];
if (compare(parent, node) > 0) {
heap[parentIndex] = node;
heap[index] = parent;
index = parentIndex;
} else {
return;
}
}
}
从堆顶取出元素
react
从堆中取去一个元素有两种方法:
peek
pop
这两个函数的区别是:
peek
方法只是取出堆顶的元素,不会删除堆顶的元素pop
方法会删除堆顶的元素,并且重新安排堆的结构(最小堆)
这里主要看 pop
是如何实现的
删除堆顶元素后,pop
将最后一个元素放到堆顶,然后调用 siftDown
方法,重新安排最小堆
从根元素开始,比较根元素和左右子元素的大小,如果根元素比左右子元素都大,那么就交换根元素和左右子元素中较小的那个元素的位置
一个元素的子节点的索引:
left = index * 2
right = left + 1
因为这里采用的是用数组存放二叉堆,所以元素索引的方式:
left = (index + 1) * 2 - 1
right = left + 1
这里为什么用 length >>> 1
,是因为非叶子节点的个数恰好等于 length / 2
向下取整
遍历条件通过判断父元素索引是否小于 length / 2
来实现最少遍历次数
比如说当前的 length
是 13
,要取出堆顶元素
index = 0
,length = 12
,halfLength = 6
leftIndex = 1
,rightIndex = 2
- 如果
left
比node
小,index = 1
index = 1
,length = 12
,halfLength = 6
leftIndex = 3
,rightIndex = 4
- 如果
left
比node
小,index = 3
index = 3
,left = 12
,halfLength = 6
leftIndex = 7
,rightIndex = 8
- 如果
left
比node
小,index = 7
index = 7
,left = 24
,halfLength = 6
,循环结束
通过三次遍历就能找到将一个元素放合适位置了
function pop<T: Node>(heap: Heap<T>): T | null {
if (heap.length === 0) {
return null;
}
const first = heap[0];
const last = heap.pop();
if (last !== first) {
heap[0] = last;
siftDown(heap, last, 0);
}
return first;
}
function siftDown<T: Node>(heap: Heap<T>, node: T, i: number): void {
let index = i;
const length = heap.length;
const halfLength = length >>> 1;
while (index < halfLength) {
const leftIndex = (index + 1) * 2 - 1;
const left = heap[leftIndex];
const rightIndex = leftIndex + 1;
const right = heap[rightIndex];
if (compare(left, node) < 0) {
if (rightIndex < length && compare(right, left) < 0) {
heap[index] = right;
heap[rightIndex] = node;
index = rightIndex;
} else {
heap[index] = left;
heap[leftIndex] = node;
index = leftIndex;
}
} else if (rightIndex < length && compare(right, node) < 0) {
heap[index] = right;
heap[rightIndex] = node;
index = rightIndex;
} else {
return;
}
}
}
compare 函数
compare
函数的实现如下:
sortIndex
是任务的优先级,id
是任务的唯一标识- 优先级相同的情况下,再比较
id
compare(a, b) > 0
说明 a
任务的优先级更高,b
任务的优先级更低,反之亦然。
function compare(a: Node, b: Node) {
const diff = a.sortIndex - b.sortIndex;
return diff !== 0 ? diff : a.id - b.id;
}
小技巧
用数组存储最小堆,一共需要遍历多少次就可以找到目标元素:通过 Math.floor(Math.log2(length - 1))
计算
延时任务调度和非延时任务调度
延时任务调度使用 requestHostTimeout
,非延时任务调度使用 requestHostCallback
requestHostTimeout
:设置了一个回调函数在指定的延迟时间后被调用,用于在一定的时间过去后执行回调函数requestHostCallback
:设置了一个回调函数在浏览器下一个帧被调用,用于在下一帧执行回调函数
requestHostTimeout
这函数是一个 setTimeout
,在到达延时时间后执行 callback
源码简化:
const requestHostTimeout = (callback, ms) => {
setTimeout(() => {
callback(getCurrentTime());
}, ms);
};
延时任务回调函数 handleTimeout
handleTimeout
是在 requestHostTimeout
执行是传入的回调函数
执行过程:
- 将延迟队列任务中过期任务放到任务列表中
- 如果任务列表中有任务,调用
requestHostCallback(flushWork)
执行任务 - 如果任务列表中没有任务,判断延迟队列中是否有任务,如果有,调用
requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime)
设置延迟任务
总的来说延迟任务的执行逻辑就是:如果任务列表中有任务,执行任务,如果没有,判断延迟队列中是否有任务,如果有,设置延迟任务
简单说:将延时队列中的过期任务放到非延时队列中,然后调用 requestHostCallback
执行非延时任务
function handleTimeout(currentTime: number) {
isHostTimeoutScheduled = false;
advanceTimers(currentTime);
if (!isHostCallbackScheduled) {
if (peek(taskQueue) !== null) {
isHostCallbackScheduled = true;
requestHostCallback(flushWork);
} else {
const firstTimer = peek(timerQueue);
if (firstTimer !== null) {
requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
}
}
}
}
requestHostCallback
执行过程:
requestHostCallback
执行时,调用schedulePerformWorkUntilDeadline
- 当
schedulePerformWorkUntilDeadline
被调用了,会执行port2.postMessage(null)
- 当
port2.postMessage
执行时,port1.onmessage
将会被调用 port1.onmessage
函数体是performWorkUntilDeadline
,所以performWorkUntilDeadline
会被执行
react
为什么使用使用 MessageChannel
而不是 setTimeout
(setTimeout
是个托底方案):
- 因为
setTimeout
是基于时间的,如果浏览器被挂起(例如,当用户切换到其他标签或最小化窗口时),setTimeout
也会被挂起,而MessageChannel
不会 - 还有
setTimeout
的精度也不够,可能存在一定的误差 - 然后当有大量的任务需要执行时,
setTimeout
会产生很多的定时器,从而影响性能
源码简化:
let scheduledHostCallback = null;
let schedulePerformWorkUntilDeadline;
const performWorkUntilDeadline = () => {
if (scheduledHostCallback !== null) {
const hasMoreWork = scheduledHostCallback();
if (hasMoreWork) {
schedulePerformWorkUntilDeadline();
}
}
};
const channel = new MessageChannel();
const port = channel.port2;
channel.port1.onmessage = performWorkUntilDeadline;
schedulePerformWorkUntilDeadline = () => {
port.postMessage(null);
};
const requestHostCallback = (callback) => {
scheduledHostCallback = callback;
schedulePerformWorkUntilDeadline();
};
非延时任务回调函数 flushWork
调用 workLoop
函数,workLoop
函数会根据任务的优先级和过期时间,以及浏览器的空闲时间,决定是否继续执行下一个任务,还是暂停执行并交还控制权给浏览器
源码简化:
function flushWork(hasTimeRemaining: boolean, initialTime: number) {
isHostCallbackScheduled = false;
if (isHostTimeoutScheduled) {
isHostTimeoutScheduled = false;
cancelHostTimeout();
}
isPerformingWork = true;
const previousPriorityLevel = currentPriorityLevel;
try {
return workLoop(hasTimeRemaining, initialTime);
} finally {
currentTask = null;
currentPriorityLevel = previousPriorityLevel;
isPerformingWork = false;
}
}
wookLoop
wookLoop
函数在浏览器空闲时执行 taskQueue
执行逻辑:
- 如果当前任务的过期时间(
expirationTime
)大于当前时间(currentTime
),并且没有剩余的时间(!hasTimeRemaining
)或者应该让出控制权(shouldYieldToHost
),就跳出循环(注释 ①) - 如果任务没有过期,并且还有剩余时间(或者不需要让出控制权),它会执行当前任务的回调函数,并传入一个布尔值表示是否超时(注释 ②)
const continuationCallback = callback(didUserCallbackTimeout);
- 如果回调函数返回了一个新的函数,说明当前任务还没有完成,需要继续执行,那么它会把新的函数赋值给当前任务的回调,并返回
true
表示还有未完成的任务(注释 ③) - 如果回调函数没有返回新的函数,说明当前任务已经完成,那么它会从任务队列中移除当前任务(注释 ④)
- 最后,重复上述步骤,直到任务队列为空或者遇到需要暂停或者让出的情况
源码简化:
function workLoop(hasTimeRemaining: boolean, initialTime: number) {
let currentTime = initialTime;
advanceTimers(currentTime);
currentTask = peek(taskQueue);
while (
currentTask !== null &&
!(enableSchedulerDebugging && isSchedulerPaused)
) {
if (
// ① 表示当前任务过期时间大于当前时间
currentTask.expirationTime > currentTime &&
// hasTimeRemaining 表示有没有剩余时间
// shouldYidldToHost() 表示需要让出给主机
(!hasTimeRemaining || shouldYieldToHost())
) {
break;
}
// 任务没有过期并且还有剩余时间(或者不需要让出控制权)
const callback = currentTask.callback;
if (typeof callback === "function") {
// 将当前任务的回调函数清空
currentTask.callback = null;
// 设置当前的优先级
currentPriorityLevel = currentTask.priorityLevel;
// 任务是否超时
const didUserCallbackTimeout = currentTask.expirationTime <= currentTime;
// ② 执行当前任务回调函数传入是否超时,并返回一个函数
const continuationCallback = callback(didUserCallbackTimeout);
currentTime = getCurrentTime();
// ③ 如果返回了一个函数,说明当前任务还没有完成,需要继续执行
if (typeof continuationCallback === "function") {
currentTask.callback = continuationCallback;
advanceTimers(currentTime);
return true;
} else {
// ④ 没返回函数,说明当前任务已经完成,从任务队列中移除
if (currentTask === peek(taskQueue)) {
pop(taskQueue);
}
advanceTimers(currentTime);
}
} else {
// 如果任务没有回到函数,就从任务队列中移除
pop(taskQueue);
}
currentTask = peek(taskQueue);
}
}
判断 timerQueue 队列中是否有任务过期
timerQueue
队列是用来存放延时任务的
非延迟队列 taskQueue
的优先级比较高,如果一直在执行这个任务队列,可能会导致 timerQueue
中的任务过期
react
通过 advanceTimers
函数,将过期任务从 timerQueue
中取出,放入 taskQueue
中
执行逻辑:
timer.callback === null
说明这个任务已经被取消了,就用pop
函数把它从timerQueue
中移除timer.startTime <= currentTime
说明这个任务已经可以执行了,就用pop
函数把它从timerQueue
中移除,并把它的sortIndex
设置为expirationTime
,然后用push
函数把它加入到taskQueue
中
function advanceTimers(currentTime: number) {
let timer = peek(timerQueue);
while (timer !== null) {
if (timer.callback === null) {
pop(timerQueue);
} else if (timer.startTime <= currentTime) {
pop(timerQueue);
timer.sortIndex = timer.expirationTime;
push(taskQueue, timer);
} else {
return;
}
timer = peek(timerQueue);
}
}
变量介绍
这些变量的作用是保证 react
在任务时不会出现逻辑错误,我们在学习源码的时候,其实不看这些变量,也不影响理解
不过呢,既然在学习 react
源码了,还是要了解下这些变量存在的意义:
isHostCallbackScheduled
isHostTimeoutScheduled
isPerformingWork
这 3
个变量都是保证保证调度器的稳定和效率,避免不必要的重复或者中断
isSchedulerPaused
用于暂停下一个任务的调度
也就是说,一个任务如果执行了,是不会被中断或者暂停的
isHostCallbackScheduled
工作原理:
- 在调用
flushWork
函数之前,先把isHostCallbackScheduled
置为true
,然后再flushWork
调用时将isHostCallbackScheduled
设置为false
- 如果在
flushWork
函数执行过程中,requestHostCallback
函数被调用了,requestHostCallback
调用说明有新的任务被安排了,那么就需要检查当前是否有任务在执行中 - 如果有,就不会安排新的任务(超时函数触发、其他新的任务),而是等待当前任务执行完毕后再安排(等待
flushWork
执行完毕)
isHostTimeoutScheduled
用于标记是否已经设置了一个超时回调函数。如果为 true
,表示调度器已经使用 setTimeout
函数安排了一个回调函数,如果为 false
,表示调度器还没有安排回调函数
工作原理:
- 在调用
handleTimeout
函数之间,先把isHostTimeoutScheduled
置为true
,然后再handleTimeout
调用时将isHostTimeoutScheduled
设置为false
- 如果在
handleTimeout
函数执行过程中,有一个延时任务被安排了,就会调用cancelHostTimeout
函数,取消当前的定时器,然后重新安排一个定时器,这样就保证了只有一个定时器在运行
isPerformingWork
在 flushWork
函数执行时,将会标记为 true
,表示当前正在执行任务,保证在当前任务执行完成之前不会安排新的任务
在当前任务执行完之后,isPerformingWork
会被置为 false
总结
- 任务队列分为延时队列和非延时队列
- 延时队列和非延时队列都是最小堆(堆顶的任务优先级最高)
- 将延时队列中的过期任务放到非延时队列中,等待执行
- 延时队列执行时,将过期任务放到非延时队列中
- 非延时队列执行时,先检查有没过期的延时任务
getNextLanes 函数是如何找到最高优先级的任务
react-reconciler
中 getNextLanes 函数作用是:
在 react
一次任务调度中,找出优先级最高的任务
大概分为 4
步:
- 根据优先级找出优先级最高的任务
- 处理
wipLanes
和nextLanes
- 处理连续输入事件
- 处理并发过程中受影响的
lanes
其中 1/3/4
都是会改变 nextLanes
,2
直接 return wipLanes
React 源码中的一些概念
变量名称中带有 host
,一般和宿主有关,在 ReactDOM
中指浏览器相关的东西,比如 HostComponent
、HostRoot
React 架构介绍
学习 react
源码,有一个绕不过的概念 —— fiber
,fiber
是啥,仅仅通过代码是很难理解这个概念的
在了解 fiber
之前,先了解一下 react
前置知识
React@15 架构
react@15
的架构:
Reconciler
:用diff
算法找出变化的组件Renderer
:将变化的组件渲染到页面上
每当更新时,Reconciler
会做这些工作:
- 调用组件的
render
方法,生成VirtualDOM
- 将
new VirtualDOM
与old VirtualDOM
做对比,找出差异 - 通知
Renderer
更新页面
Renderer
接收到 Reconciler
的通知后,更新当前页面
由于更新是递归执行的,所以中途不能被打断,需要将调用栈挨个执行完
而且 Reconciler
和 Renderer
是交替执行的
也就是说一个组件进入 Reconciler
阶段后,用 diff
算法比对完,通知 Renderer
更新页面,再进入下一个组件的 Reconciler
,这样一点点渲染
这时如果某个组件的 Reconciler
计算时间过长(> 16.6ms,一秒 60 帧),用户就能在页面中感受到卡顿
这种方案称为 Stack Reconciler
,无法打断,一鼓作气运行到底,中途不停歇
React@16 架构
解决这个问题,最直观的方法就是分片,把之前 Reconciler
和 Renderer
一个完整的任务拆成很多小任务(分片),每个任务执行时的时间都很短(< 16.6ms),这样整个更新的过程就不会独占整个线程了(js
是单线程)
将完整的任务分片之后,这些任务该怎么执行呢?
react@16
对原先的架构进行了重构,增加了 Scheduler
,现在架构变为:
Scheduler
:任务调度,高优先级的进入Reconciler
Reconciler
:用diff
算法找出变化的组件Renderer
:将变化的组件渲染到页面上
Scheduler
浏览器提供了一个 requestIdleCallback
这个函数将在浏览器空闲时期被调用。这使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应
通过这个 api
我们就能知道当前线程是否空闲,决定自己调用相关任务
由于兼容原因,react
没有使用浏览器提供的 api
,自己实现了 Scheduler
Reconciler
在 React@16
版本中 Reconciler
从递归变成了可中断的循环
而且 Reconciler
和 Renderer
不在交替工作了
当 Scheduler
把任务交个 Reconciler
后, Reconciler
会为变化的 VirtualDOM
打上标记“增/删/改”
一个任务处在
Reconciler
时,可能会被打断(是否打断,由Scheduler
调度),打断了之后需要重新执行Reconciler
Renderer
Scheduler
和 Reconciler
工作时,都是在内存中运行的,没有通知 Renderer
,所以用户是感知不到的界面变化的
当所有的任务都完成了 Reconciler
之后,才会交给 Renderer
进行渲染
处于
Renderer
阶段时,是不能被打断的,这里会一口气把DOM
更新完成
fiber 是什么
每一个任务(分片)的数据结构就是 fiber
,在 react@16
之后逐渐用 fiber
代替 VirtualDOM
fiber
,英文意思是纤维,在计算机中的意思是纤程,它是一种比线程更精细的控制
这里 React Fiber
和 fiber
不是一个概念,react
团队将这种方案命名为 fiber
,借用了 fiber
的意思:更紧密的控制
这种方案称为 Fiber Reconciler
组件卸载时 DOM 树的自动清理机制是怎样的
通过上两讲:
我们已经知道了 react
是如何找到 passive effect
返回的函数
那么找到这个函数后,怎么执行这个函数呢
我们先来看下面这段代码:
function A() {
useEffect(() => {
return () => {
console.log("执行销毁函数 A");
};
}, []);
useEffect(() => {
return () => {
console.log("执行销毁函数 A1");
};
}, []);
return <>文本A</>;
}
一个组件中有两个 passive effect
返回的函数,react
是怎么安排执行的顺序呢?
一个组件中的 passive effect
是用链表的形式存储的
每个 effect
对象都有 destroy
和 next
属性
destroy
保存的是passive effect
返回的函数next
保存的是下一个effect
对象
最顶层的 effect
是函数组件中写在最上面的 useEffect
,通过 next
指向下一个 effect
,以此类推,最后一个 effect
的 next
指向最顶层的 effect
结构如下所示:
let effect = {
destroy: () => {
console.log("执行销毁函数 A"));
},
next: {
destroy: () => {
console.log("执行销毁函数 A1");
},
next: {
destroy: () => {
console.log("执行销毁函数 A");
},
next: { ... },
},
},
};
既然是链表,那么执行的顺序就是从最顶层的 effect
开始,依次执行 destroy
函数,最后执行最顶层的 effect
的 destroy
函数
function commitHookEffectListUnmount(
flags: HookFlags,
finishedWork: Fiber,
nearestMountedAncestor: Fiber | null
) {
const updateQueue: FunctionComponentUpdateQueue | null =
(finishedWork.updateQueue: any);
const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
if (lastEffect !== null) {
const firstEffect = lastEffect.next;
let effect = firstEffect;
do {
if ((effect.tag & flags) === flags) {
// Unmount
const destroy = effect.destroy;
effect.destroy = undefined;
if (destroy !== undefined) {
destroy();
}
}
effect = effect.next;
} while (effect !== firstEffect);
}
}
react
这里使用 do...while
进行遍历,保证所有的 effect
都被执行
释放内存
释放内存分为两个阶段:
- 第一个阶段是在向上遍历时
- 第二个阶段是在处理完成
deletions
时
detachFiberAfterEffects
上回说到 react
在处理 deletedNode
时先向下遍历,然后在向上遍历
在向上遍历的过程中会将对应所有遍历到的 fiber
的属性都置为 null
,这样可以释放一些内存
function detachFiberAfterEffects(fiber) {
const alternate = fiber.alternate;
if (alternate !== null) {
fiber.alternate = null;
detachFiberAfterEffects(alternate);
}
fiber.child = null;
fiber.deletions = null;
fiber.sibling = null;
if (fiber.tag === HostComponent) {
const hostInstance = fiber.stateNode;
if (hostInstance !== null) {
delete hostInstance[internalInstanceKey];
delete hostInstance[internalPropsKey];
delete hostInstance[internalEventHandlersKey];
delete hostInstance[internalEventHandlerListenersKey];
delete hostInstance[internalEventHandlesSetKey];
}
}
fiber.stateNode = null;
fiber.return = null;
fiber.dependencies = null;
fiber.memoizedProps = null;
fiber.memoizedState = null;
fiber.pendingProps = null;
fiber.stateNode = null;
fiber.updateQueue = null;
}
detachAlternateSiblings
当处理完 deletions
时,当前 fiber
的 alternate
及 alternate
下所有的子节点也会被置为 null
,这样可以释放一些内存
function detachAlternateSiblings(parentFiber) {
const previousFiber = parentFiber.alternate;
if (previousFiber !== null) {
let detachedChild = previousFiber.child;
if (detachedChild !== null) {
previousFiber.child = null;
do {
const detachedSibling = detachedChild.sibling;
detachedChild.sibling = null;
detachedChild = detachedSibling;
} while (detachedChild !== null);
}
}
}
根节点处理
react
每次遍历都是从根节点开始,那么根节点的处理是怎么样的呢?
在这里 掌握 React 组件树遍历技巧 我们知道 react
是通过调用 commitPassiveUnmountOnFiber
函数来寻找有 passive effect
的 fiber
按照源码去追踪,我们会发现在 recursivelyTraversePassiveUnmountEffects
函数中会调用 commitHookPassiveUnmountEffects
函数,具体解释可以查这里:commitPassiveUnmountOnFiber
function commitPassiveUnmountOnFiber(finishedWork, type) {
recursivelyTraversePassiveUnmountEffects(finishedWork);
if (finishedWork.flags & Passive) {
commitHookPassiveUnmountEffects(
finishedWork,
finishedWork.return,
HookPassive | HookHasEffect
);
}
}
react
为什么要多此一举呢?
通过不断的打断点会看到,commitHookPassiveUnmountEffects
函数会被调用两次
recursivelyTraversePassiveUnmountEffects
函数处理的是 finishedWork.chile
,而 commitHookPassiveUnmountEffects
函数处理的是 finishedWork
因为 react
是从根节点开始遍历的,所以 commitHookPassiveUnmountEffects
只处理根节点的 passive effect
的返回函数
总结
react
从根组件开始遍历,寻找passive effect
的fiber
- 在遍历时,会检查每个
fiber
的deletions
- 如果有则暂停
passive effect
的遍历,先处理deletions
- 处理完
deletions
后,再继续遍历passive effect
的fiber
- 如果有则暂停
- 在处理
deletions
时,会先向下遍历,然后再向上遍历- 向下遍历时,执行
passive effect
的返回函数 - 向上遍历时
- 如果遇到
sibling
,则会沿着sibling
向下遍历 - 将
fiber
的所有属性置为null
,释放内存 - 直到遇到
deletedNode
结束处理deletions
- 如果遇到
- 向下遍历时,执行
- 根节点的
passive effect
返回的函数会单独处理
useEffect 返回的函数是怎么执行的(代码片段)
commitPassiveUnmountOnFiber
这段代码的主要逻辑是:在 commit
阶段,卸载 fiber
节点的 passive effect
- 如果
fiber
有子节点,那么调用recursivelyTraversePassiveUnmountEffects
,这个函数会递归地卸载子节点的passive effect
。recursivelyTraversePassiveUnmountEffects
函数内部会调用commitPassiveUnmountOnFiber
(注释 ①) - 如果
fiber
节点本身有Passive
的标志,会调用commitHookPassiveUnmountEffects
来卸载passive effect
(注释 ②)
刚看到这段代码会很疑惑
因为在 recursivelyTraversePassiveUnmountEffect
函数中调用了 commitHookPassiveUnmountEffects
,那为什么这里还要再调用呢?
是因为注释 ② 这段代码只在根节点有效,但是这段代码会一直执行(react
更新是从根节点一层一层往下的)
function commitPassiveUnmountOnFiber(finishedWork) {
switch (finishedWork.tag) {
case FunctionComponent: {
// ① fiber 节点的子节点会走这里
recursivelyTraversePassiveUnmountEffects(finishedWork);
// ② fiber 节点本身
if (finishedWork.flags & Passive) {
commitHookPassiveUnmountEffects(
finishedWork,
finishedWork.return,
HookPassive | HookHasEffect
);
}
break;
}
}
}
深入探究 React 原生事件的工作原理
ReactDOM
是 react
的 dom
渲染器,它从 react-dom/client
中导出
当我们调用 ReactDOM.createRoot
方法时,createRoot
函数会调用 listenToAllSupportedEvents
方法
listenToAllSupportedEvents
函数定义在 react-dom-bindings/src/events/DOMPluginEventSystem
文件中
当执行 DOMPluginEventSystem
时,react
会进行事件注册:
SimpleEventPlugin.registerEvents();
EnterLeaveEventPlugin.registerEvents();
ChangeEventPlugin.registerEvents();
SelectEventPlugin.registerEvents();
BeforeInputEventPlugin.registerEvents();
可以看到,react
将事件分为五类,分别由不同的事件插件进行处理:
SimpleEventPlugin
处理简单事件,例如onClick
EnterLeaveEventPlugin
处理鼠标进出事件,例如onMouseEnter
ChangeEventPlugin
处理修改事件,例如onChange
SelectEventPlugin
处理选择事件,例如onSelect
BeforeInputEventPlugin
处理输入前事件,例如onBeforeInput
这些事件插件负责将浏览器原生事件转换为 react
事件,并将其分发到正确的组件中
事件注册前准备
下面以 SimpleEventPlugin
为例,讲解事件注册的流程。了解了 SimpleEventPlugin.registerEvents
,其他四个事件也就更容易理解了。
虽然这个函数名叫做 registerEvents
,但实际上这个函数还没到注册事件的步骤,它只是准备事件名。为此,这个函数会提供三个变量(见【获取所有事件】)。
简单事件
简单事件指只依赖自身的事件,例如 onClick
只依赖 click。
有些事件依赖其他事件,例如 onMouseEnter
依赖 ["mouseout", "mouseover"]
。react
这么做简化了开发者自己监听事件的步骤。否则,开发者自己实现 onMouseEnter
这种事件,需要分别监听 mouseout
和 mouseover
事件。
获取所有事件
react
中的事件都是硬编码的,保存在 simpleEventPluginEvents
变量中
const simpleEventPluginEvents = ["click", "mousedown", "mouseenter", ...];
遍历 simpleEventPluginEvents
列表,react
将事件名处理成 domEventName
和 reactEventName
,也就是 react
事件名和 dom
事件名的对应关系。
domEventName
:dom
事件名全小写,例如mousedown
、mouseenter
等reactEventName
:react
事件名是驼峰形式,例如onClick
、onMouseDown
等
在 react
中,为了处理 dom
事件,react
定义了一些与原生 dom
事件名对应的 react
事件名。
这些事件名以 on
开头,比如 onClick
、onFocus
等。同时,react
还定义了一些事件之间的依赖关系,即某些事件需要依赖于其他事件才能正常工作
为了实现这些功能,react
使用了三个变量(详细的事件名在文章底部):
topLevelEventsToReactNames
:这个变量是一个Map
,保存着原生dom
事件名和对应的react
事件名之间的映射关系。例如,click
事件对应着onClick
事件。一共有75
个映射关系registrationNameDependencies
:这个变量是一个普通的对象,保存着react
事件名和依赖事件名之间的关系。例如,onClick
事件依赖于click
事件。一共有166
个依赖关系。allNativeEvents
:这个变量是一个Set
,保存了所有原生dom
事件名。一共有81
个事件名。
特殊处理
这里有 7
个事件不在 SimpleEventPluginEvents
变量中,因为它们是需要特殊处理的。
这些事件包括:
onAnimationEnd
onAnimationIteration
onAnimationStart
onDoubleClick
onFocus
onBlur
onTransitionEnd
其中,与 Animation
和 Transition
相关的事件分别是 AnimationEvent
和 TransitionEvent
。
由于浏览器兼容性的问题,react-dom
通过函数 getVendorPrefixedEventName
来实现对它们的兼容性处理(源码)
另外,对于 onDoubleClick
、onFocus
和 onBlur
这三个事件,它们的 reactEventName
与对应的 domEventName
不同,因此需要特殊处理:
onDoubleClick
对应的domEventName
是dbclick
onFocus
对应的domEventName
是focusin
onBlur
对应的domEventName
是focusout
这些细节处理有助于确保 react
应用程序在不同浏览器上的正确运行
事件注册
在 react
中,事件注册是非常重要的,因为它关系到组件的交互和性能
react
事件注册来自于 listenToAllSupportedEvents
函数
事件注册分为 3
种情况:
- 绑定在
document
- 绑定在
div#root
- 绑定在目标元素
target
从
react@18
开始,事件绑定在页面根元素中(也就是div#root
),不再绑定在document
上
绑定在 document
只有 selectionchange
事件是绑定在 document
绑定在 target
这些事件是不会冒泡的(不需要委托),它们的事件是绑定在事件事件发生的元素身上,这部分事件有普通事件和媒体事件组成,都是硬编码在代码中
mediaEventTypes
:play
、pause
nonDelegatedEvents
,load
、scroll
绑定在 div#root
从 allNativeEvents
中排除掉 nonDelegatedEvents
事件,剩下的事件都是绑定在 div#root
上
注册
在事件注册前判断浏览器是否支持 passive
,如果支持,则将 passive
设置为true
,浏览器永远不会调用 event.preventDefault()
,用于提升性能
这个属性用于 touchstart
、touchmove
、wheel
事件中
然后调用函数分别注册事件,源码
addEventBubbleListener
:冒泡事件addEventCaptureListener
:捕获事件addEventCaptureListenerWithPassiveFlag
:捕获事件,passive
为true
addEventBubbleListenerWithPassiveFlag
: 冒泡事件,passive
为true
在注册事件时事件监听器 react
会经过一系列的处理,最后返回一个 (e) => { .... }
react
处理的这一步,我们叫做合成事件
事件优先级
react
将事件优先级分为 4
种:
- 离散事件优先级,例如:点击事件,
input
输入等触发的更新任务,优先级最高SyncLane
- 连续事件优先级,例如:滚动事件,拖动事件等,连续触发的事件 优先级次之,为
InputContinuousLane
- 默认事件优先级,例如:
setTimeout
触发的更新任务,为DefaultLane
- 闲置事件优先级,优先级最低,为
IdleLane
react
在事件注册时根据事件名设置不同的优先级,getEventPriority
特殊处理了 message
事件(它的优先级是根据 Scheduler
回调来调度的)
事件队列(合成事件)
这一部分是 react-dom
的核心,react
这么做的目的是为了解决各浏览器之间的差异
事件队列 dispatchQueue
包含两个参数:
event
:对应的是react
合成事件,见【收集合成事件】listeners[]
:目标节点对应的事件监听器,从目标节点开始,一直到祖先节点,见【收集事件监听器】
收集合成事件
合成事件 SyntheticEvent
是 react
最核心的一部分
提取合成事件也分为 5
个插件:
SimpleEventPlugin.extractEvents(...)
EnterLeaveEventPlugin.extractEvents(...)
ChangeEventPlugin.extractEvents(...)
SelectEventPlugin.extractEvents(...)
BeforeInputEventPlugin.extractEvents(...)
其中 EnterLeaveEventPlugin
、ChangeEventPlugin
、SelectEventPlugin
、BeforeInputEventPlugin
插件的提取事件只在冒泡阶段执行
这些函数内部,react
都会创建一个基本的合成事件,然后再根据事件名分成若干种合成事件
这些合成事件都是由 createSyntheticEvent
函数创建的
react
按照事件名分成了 12
种合成事件:
SyntheticEvent
SyntheticKeyboardEvent
SyntheticFocusEvent
SyntheticMouseEvent
SyntheticDragEvent
SyntheticTouchEvent
SyntheticAnimationEvent
SyntheticTransitionEvent
SyntheticUIEvent
SyntheticWheelEvent
SyntheticClipboardEvent
SyntheticPointerEvent
SyntheticCompositionEvent
SyntheticInputEvent
合成事件有一些基本属性(其他属性对应不同的对应不同的实例,可以点击上面查看源码):
_reactName
:react
事件名_targetInst
:target
对应的fiber
type
:原生事件类型,比如click
nativeEvent
:原生事件信息,event
target
:原生节点,比如onClick
preventDefault
:react
实现stopPropagation
:react
实现persist
:react
实现isPersistent
:react
实现
除了这些属性之外,react
把 nativeEvent
中信息都提取到了合成事件中,通过下面这段代码实现
// Interface 是 react 定义的各种合成事件
// nativeEvent 是原生事件
for (const propName in Interface) {
if (!Interface.hasOwnProperty(propName)) {
continue;
}
const normalize = Interface[propName];
// normalize 如果存在,说明 propName 对应的属性在合成事件中是一个函数
// react 这么设计的原因是为了消除不同浏览器之间的差异
// 见[WheelEventInterface 事件]
if (normalize) {
this[propName] = normalize(nativeEvent);
} else {
this[propName] = nativeEvent[propName];
}
}
WheelEventInterface
这里用 WheelEventInterface
事件来说明 react
为什么要定义 normalize
源码
比如 wheel
事件中的 deltaY
属性:
- 在
webkit
中是wheelDeltaY
- 在
IE9
以下是wheelDelta
react
把这些可能存在兼容问题的属性值写成了函数,没有兼容问题的属性值写成了 0
当代码执行到这里时,react
会根据它自身的判断来达到消除浏览器之间的差异
收集事件监听器
遍历目标节点(target
)到祖先节点(div#root
),找到所有注册了该事件类型的监听器,将它们存储在一个数组中:
listeners = [targetListener, ..., rootListener]
监听器的属性包括:
instance
:dom
节点对应的fiber
listener
:监听器函数currentTarget
:目标节点target
在收集监听器的函数中,react
从目标节点开始向上遍历,instance.return
得到的值是当前节点的父节点
// 目标节点
let instance = targetFiber;
while (instance !== null) {
// 用 vite 创建的项目
// stateNode: img, tag: 5
// stateNode: a , tag: 5
// stateNode: div, tag: 5
// stateNode: div.app, tag: 5
// stateNode: null, tag: 5
// stateNode: null, tag: 8
// stateNode: FiberRootNode, tag: 3
// tag 类型是 WorkTag,文件:packages/react-reconciler/src/ReactWorkTags.js
// stateNode 与 fiber 相关的 dom
const { stateNode, tag } = instance;
// 省略若干代码 ...
const listener = getListener(instance, reactEventName);
if (listener != null) {
listeners.push(
createDispatchListener(instance, listener, lastHostComponent)
);
}
// 省略若干代码 ...
// 当前节点的父节点
instance = instance.return;
}
当监听器和合成事件都准备好后,将他们放入 dispatchQueue
队列中
执行事件队列
执行事件队列分为冒泡和捕获阶段
在上一节中我们得到事件监听器是这样存储的
listeners = [targetListener, ..., rootListener]
再执行事件队列时,需要判断当前处于什么阶段:
- 如果当前是捕获阶段,从根节点开始(也就是从队列的最后一个开始执行)
- 如果当前是冒泡节点,从目标节点开始(也就是从队列的第一个开始执行)
在执行回调函数时,React
会记录错误并继续执行应用程序,它确保了在 React
组件中发生的错误不会导致整个应用程序崩溃
总结
- 在调用
listenToAllSupportedEvents
函数时,react-dom-bindings/src/events/DOMPluginEventSystem
被执行,初始化事件挂载相关的参数topLevelEventsToReactNames
registrationNameDependencies
allNativeEvents
- 事件注册,将事件挂载到对应的节点
- 处理事件监听器
- 合成事件
- 事件监听器收集
- 事件触发时执行事件
一个不理解的地方
事件队列是一个数组 Array<DispatchEntry>
我测试了多种事件,dispatchQueue
都没有出现多项的情况,冒泡或者捕获的事件时存放在 listeners
属性中的,所以我不知道在什么情况下会 dispatchQueue
出现多项
react 中事件名映射
react 中的任务(代码片段)
task(任务)
task 是 react
中的一个任务,它有下面几个属性:
type Task = {
id: number;
callback: Callback | null;
priorityLevel: PriorityLevel;
startTime: number;
expirationTime: number;
sortIndex: number;
isQueued?: boolean;
};
id
: 使用的是一个全局变量taskIdCounter
,每次创建任务的时候都会自动加一,用于区分不同的任务callback
:一个函数,表示要执行的任务,它会在调度器安排好时间后被调用- 这个取决于你在里面做什么,比如更新组件的状态、渲染组件、或者执行其他逻辑
priorityLevel
:优先级,会影响任务何时被执行,有五种:ImmediatePriority
、UserBlockingPriority
、IdlePriority
、LowPriority
和NormalPriority
startTime
:任务开始时间,它是使用getCurrentTime
函数获取的微秒级别的当前时间expirationTime
:任务过期的时间,它是根据startTime
和priorityLevel
计算出来的。过期时间越小,表示任务越紧急,越需要优先执行expirationTime
的计算公式为:startTime + timeout
,timeout
的值取决于priorityLevel
,它们的对应关系如下:ImmediatePriority
:-1
(立即执行)UserBlockingPriority
:250
(250ms
后执行)NormalPriority
:5000
(5s
后执行)LowPriority
:10000
(10s
后执行)IdlePriority
:1073741823
(1073741.823s
后执行)
react
这么设计是因为expirationTime
的值越小,表示任务越紧急,越需要优先执行
sortIndex
:任务在队列中的排序索引,默认为-1
。它实际上会被设置成startTime
或expirationTime
,以便在插入队列时按照升序排列- 保证过期时间越小,越紧急的任务排在最前面
isQueued
:任务是否已经被加入到队列中。这个属性是为了避免重复插入或删除任务而设置的- 在性能分析模式下,会加入这个属性,目前这个值没有启用
IdlePriority 这个优先级的过期时间大概在 12 天,这意味着几乎不会过期,通常用于一些不紧急的后台任务,比如数据预加载、用户不可见的更新等
这个优先级的任务会被延迟到其他更重要的任务都完成后再执行
React dom 遍历逻辑
下面的 dom
结构react
内部是如何遍历的
const App = () => {
return (
<div>
<button>+1</button>
<A count={0} />
</div>
);
};
const A = (props) => {
useEffect(() => {
console.log(props.count);
}, [props.count]);
return <div>{props.count}</div>;
};
react
内部遍历核心逻辑:
- 在
render
时调用commitPassiveUnmountOnFiber
函数 commitPassiveUnmountOnFiber
处理不同的WorkTag
,并调用recursivelyTraversePassiveUnmountEffects
recursivelyTraversePassiveUnmountEffects
根据当前Fiber
的子节点有没有passive effect
(useEffect
,useLayoutEffect
)来决定是否遍历当前Fiber
的子节点- 如果子节点有
passive effect
,则优先遍历子节点 (深度优先),直到找到最终的叶子节点,退出当前循环 - 然后进入兄弟节点,开始遍历兄弟节点的子节点
- 具体从哪个兄弟节点开始遍历,
react
选择的是离退出循环的那个叶子节点的父节点,检查有没有子节点,以此循环遍历
- 具体从哪个兄弟节点开始遍历,
- 直到最后找到所有有
passive effect
的节点
- 如果子节点有
commitPassiveUnmountOnFiber(root.current);
function commitPassiveUnmountOnFiber(finishedWork) {
// 省略了处理不同的 WorkTag
recursivelyTraversePassiveUnmountEffects(finishedWork);
}
function recursivelyTraversePassiveUnmountEffects(parentFiber) {
// 省略了其他处理
if (parentFiber.subtreeFlags & PassiveMask) {
let child = parentFiber.child;
while (child !== null) {
commitPassiveUnmountOnFiber(child);
child = child.sibling;
}
}
}
所以对于这段 dom
的遍历逻辑是:
- 首先从根组件开始
FiberRootNode
,取到current
- 也就是说
FiberRootNode.current
是div#root
这是一个fiber
,它的tag
是3
- 也就是说
- 由于
App
的子组件有passive effect
,所以会进入App
组件,它的tag
是0
App
组件中节点是<div>
,<div>
的tag
是5
<div>
下面有两个子元素<button>
、<A>
- 先遍历
<button>
它的tag
是5
<button>
内部只有一个文本节点,没有passive effect
- 所以
react
不遍历了(跳出当前遍历的循环,也就是button
这条不在遍历了)
- 所以
- 跳出循环后,查看
button
的兄弟节点,它的兄弟节点是<A>
,<A>
的tag
是0
- 由于
<A>
节点的子节点没有passive effect
,所以跳出循环,结束整个遍历
总结
- 从跟节点开始遍历
- 当前组件的子组件有没有
passive effect
- 采取深度优先
- 如果
dom
节点内有函数组件,则这个dom
会被遍历,否则不会遍历 - 如果当前
fiber
下的所有子fiber
都没有passive effect
,则这一整个都链表都不会被遍历 - 如果当前
fiber
只有dom
,则这些dom
也不会遍历
总的来说组件会不会别遍历看 fiber
有没有 passive effect
:
- 有,一定会被遍历
- 没有,下面两种情况会被遍历,其他情况不会被遍历
- 是
passive effect
的父组件 - 和
passive effect
组件是兄弟组件
- 是
passive effect
指的是 useEffect
,useLayoutEffect
遍历逻辑如下图所示
图中画绿色勾的都会被遍历,红色勾是遍历的顺序
一些看源码的感想
今年是我做前端第五年了,我才第一次看源码,才看了几天,也就是这几天让我感受很大,后悔没有早点看源码
其实也不能说之前没有看源码,只是说之前看源码是通过别人讲解,比如手写 react
这种,虚拟 dom
工作原理这种
通过这种形式了解源码,我现在感觉意义不大,因为一个功能的实现有好几种,他和你讲的方式不一定是源码的实现方式
你听他讲完了之后,你了解了原理,然后你就认为源码也是这样实现的(有些厉害点的人可能会想到,源码会处理些边界问题)
但是呢,你没有看源码,你就只能停留在这一步,你根本不知道源码在背后做了哪些事情
还有一些大 v 会说,我是不看源码的,遇到问题了才去看
基本说这种话的人,源码都不知道看了多少了,只是当下用的技术的源码没有去看,他敢说,我从写代码以来没有看过任何源码吗?
因为设计模式,数据结构,算法这些是不变的,所以各种框架/库的诞生,用作者理解的方式去解决现有的问题
当你有了积累,在出现新的技术后,你也能快速明白它的工作原理,不再需要去看源码了
下面是我看源码的一些方式,用最笨的方法:
react
使用 flow
写的,没法直接调试,我看了网上的几种调试方式:
react
打包后的代码调试,这种方法在调试时无法进入源码,离源码就会有距离- 在创建一个
react
下面,在这个项目下clone
一份react
源码,通过配置,让项目中的react
找到源码中的react
我就是用第 2 种方式学习的,只是第二种方式需要将 flow
项目的代码删除,启动项目的时候就能在源码中调试了
阅读没有 flow
的源码时,会有很多心智负担,类型可以帮你理解代码中的边界,取值的范围,所以删除了 flow
的源码不建议阅读
那怎么办呢?有 flow
的代码没法调试,没有 flow
的代码不适合阅读
那就结合呗,一份用来阅读,一份用来调试,当你看不懂时,用调试走一走这块的逻辑
如何配置调试项目,具体可以看:React 源码调试方式
环境准备好之后,就要开始阅读源码了,在阅读源码的时候一定要做笔记
- 画出函数之间跳转的流程图
- 在函数/变量上写好各个阶段它的取值
- 如果有精力,将 1、2 两步整理成笔记
因为 react
有很多文件,而且函数拆的很细,一些变量/函数的名字换来换去,文件之间跳来跳去,你不记录,很快你就不知道要干嘛了
网上很多人说,看源码的时候不要在意细节,我认为这种方式不适合第一次阅读源码的人
我说的第一次阅读源码指的是之前从没有看过源码,这次第一次阅读源码
第一次看源码,你可能不了解设计模式,数据结构,算法,还有一些名词,如果你不在意这些细节,后面的代码你根本看不下去
所以第一次看源码的人,一些你不了解的名词,要去网上查一查,了解它是干什么的,代码中的的注释都要认真去看
最后阅读源码特别废精力,不是短时间就能完成的,时间长了就会懈怠
每次想看又不想看的时候跟自己说,就看五分钟,如果五分钟后能进入状态,就接着看下去,如果五分钟还是看不下去,就果断放下去干你想干的事情
React 源码调试方式
-
使用
pnpm create vite
创建react
项目 -
在
src
下面下载react
源码# [email protected] git clone https://github.com/facebook/react.git
-
修改
vite
配置,react
、react-dom
、react-dom-bindings
、react-reconciler
、scheduler
、shared
是react
的核心包,修改alias
让它用src/react
下的包import path from "path" resolve: { alias: { react: path.posix.resolve("src/react/packages/react"), "react-dom": path.posix.resolve("src/react/packages/react-dom"), "react-dom-bindings": path.posix.resolve( "src/react/packages/react-dom-bindings" ), "react-reconciler": path.posix.resolve( "src/react/packages/react-reconciler" ), scheduler: path.posix.resolve("src/react/packages/scheduler"), shared: path.posix.resolve("src/react/packages/shared"), }, },
-
vite
环境配置define: { __DEV__: true, __EXPERIMENTAL__: true, __PROFILE__: true, },
-
删除
flow
相关的代码pnpm i flow-remove-types prettier -g # 删除 flow 注解 flow-remove-types --out-dir src/react src/react # 格式化代码 prettier src/react --ignore-unknown --write
-
修改
src/main.jsx
中React
和ReactDOM
的导入方式import * as React from "react"; import * as ReactDOM from "react-dom/client";
-
在
src/react/packages/react-reconciler/src/ReactFiberHostConfig.js
文件中修改。如果没生效,删除/node_modules/@vitejs
,重启服务// 报错 Uncaught SyntaxError: The requested module '/src/react/packages/react-reconciler/src/ReactFiberHostConfig.js?t=1676712542480' does not provide an export named 'getChildHostContext' // 注释 // throw new Error('This module must be shimmed by a specific renderer.'); // 修改为 export * from "react-dom-bindings/src/client/ReactDOMHostConfig";
-
在
src/react/packages/shared/ReactComponentStackFrame.js
文件中修改。如果没生效,删除/node_modules/@vitejs
,重启服务// 报错 ReactComponentStackFrame.js:27 Uncaught TypeError: Cannot destructure property 'ReactCurrentDispatcher' of 'ReactSharedInternals_default2' as it is undefined. // 注释 // import ReactSharedInternals from 'shared/ReactSharedInternals'; // 修改为 import ReactSharedInternals from "../react/src/ReactSharedInternals";
-
配置
jsconfig.json
给vscode
使用{ "compilerOptions": { "baseUrl": "./", "paths": { "react/*": ["src/react/packages/react/*"], "react-dom/*": ["src/react/packages/react-dom/*"], "react-dom-bindings/*": ["src/react/packages/react-dom-bindings/*"], "react-reconciler/*": ["src/react/packages/react-reconciler/*"], "scheduler/*": ["src/react/packages/scheduler/*"], "shared/*": ["src/react/packages/shared/*"] } }, "exclude": ["node_modules", "dist"] }
-
vscode
增加调试文件vscode/launch.json
{ "version": "0.2.0", "configurations": [ { "type": "chrome", "request": "launch", "name": "针对 localhost 启动 Chrome", "url": "http://localhost:5173", "webRoot": "${workspaceFolder}" } ] }
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.