0%

事件注册

注册事件名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
// \react-dom-bindings\src\events\DOMEventNames.js

import getVendorPrefixedEventName from './getVendorPrefixedEventName';

export type DOMEventName =
| 'abort'
| 'afterblur' // Not a real event. This is used by event experiments.
// These are vendor-prefixed so you should use the exported constants instead:
// 'animationiteration' |
// 'animationend |
// 'animationstart' |
| 'beforeblur' // Not a real event. This is used by event experiments.
| 'beforeinput'
| 'blur'
| 'canplay'
| 'canplaythrough'
| 'cancel'
| 'change'
| 'click'
| 'close'
| 'compositionend'
| 'compositionstart'
| 'compositionupdate'
| 'contextmenu'
| 'copy'
| 'cut'
| 'dblclick'
| 'auxclick'
| 'drag'
| 'dragend'
| 'dragenter'
| 'dragexit'
| 'dragleave'
| 'dragover'
| 'dragstart'
| 'drop'
| 'durationchange'
| 'emptied'
| 'encrypted'
| 'ended'
| 'error'
| 'focus'
| 'focusin'
| 'focusout'
| 'fullscreenchange'
| 'gotpointercapture'
| 'hashchange'
| 'input'
| 'invalid'
| 'keydown'
| 'keypress'
| 'keyup'
| 'load'
| 'loadstart'
| 'loadeddata'
| 'loadedmetadata'
| 'lostpointercapture'
| 'message'
| 'mousedown'
| 'mouseenter'
| 'mouseleave'
| 'mousemove'
| 'mouseout'
| 'mouseover'
| 'mouseup'
| 'paste'
| 'pause'
| 'play'
| 'playing'
| 'pointercancel'
| 'pointerdown'
| 'pointerenter'
| 'pointerleave'
| 'pointermove'
| 'pointerout'
| 'pointerover'
| 'pointerup'
| 'popstate'
| 'progress'
| 'ratechange'
| 'reset'
| 'resize'
| 'scroll'
| 'scrollend'
| 'seeked'
| 'seeking'
| 'select'
| 'selectstart'
| 'selectionchange'
| 'stalled'
| 'submit'
| 'suspend'
| 'textInput' // Intentionally camelCase. Non-standard.
| 'timeupdate'
| 'toggle'
| 'touchcancel'
| 'touchend'
| 'touchmove'
| 'touchstart'
// These are vendor-prefixed so you should use the exported constants instead:
// 'transitionend' |
| 'volumechange'
| 'waiting'
| 'wheel';

export const ANIMATION_END: DOMEventName =
getVendorPrefixedEventName('animationend');
export const ANIMATION_ITERATION: DOMEventName =
getVendorPrefixedEventName('animationiteration');
export const ANIMATION_START: DOMEventName =
getVendorPrefixedEventName('animationstart');
export const TRANSITION_END: DOMEventName =
getVendorPrefixedEventName('transitionend');
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// \react-dom-bindings\src\events\EventRegistry.js

// React中的事件
export const allNativeEvents: Set<DOMEventName> = new Set();
export const registrationNameDependencies: {
[registrationName: string]: Array<DOMEventName>,
} = {};

// 事件注册
export function registerTwoPhaseEvent(
registrationName: string,
dependencies: Array<DOMEventName>,
): void {
registerDirectEvent(registrationName, dependencies);
registerDirectEvent(registrationName + 'Capture', dependencies);
}

export function registerDirectEvent(
registrationName: string,
dependencies: Array<DOMEventName>,
) {
registrationNameDependencies[registrationName] = dependencies;
for (let i = 0; i < dependencies.length; i++) {
allNativeEvents.add(dependencies[i]);
}
}

不同类型的事件注册

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// \react-dom-bindings\src\events\DOMPluginEventSystem.js

import * as BeforeInputEventPlugin from './plugins/BeforeInputEventPlugin';
import * as ChangeEventPlugin from './plugins/ChangeEventPlugin';
import * as EnterLeaveEventPlugin from './plugins/EnterLeaveEventPlugin';
import * as SelectEventPlugin from './plugins/SelectEventPlugin';
import * as SimpleEventPlugin from './plugins/SimpleEventPlugin';

// ! 注册事件
SimpleEventPlugin.registerEvents();
EnterLeaveEventPlugin.registerEvents();
ChangeEventPlugin.registerEvents();
SelectEventPlugin.registerEvents();
BeforeInputEventPlugin.registerEvents();

SimpleEventPlugin

普通事件,如 click、drag、drop 等。

1
2
3
4
// \react-dom-bindings\src\events\plugins\SimpleEventPlugin.js
import {
registerSimpleEvents,
} from '../DOMEventProperties';
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
// \react-dom-bindings\src\events\DOMEventProperties.js
const simpleEventPluginEvents = [
'abort',
'auxClick',
'cancel',
'canPlay',
'canPlayThrough',
'click',
'close',
'contextMenu',
'copy',
'cut',
'drag',
'dragEnd',
'dragEnter',
'dragExit',
'dragLeave',
'dragOver',
'dragStart',
'drop',
'durationChange',
'emptied',
'encrypted',
'ended',
'error',
'gotPointerCapture',
'input',
'invalid',
'keyDown',
'keyPress',
'keyUp',
'load',
'loadedData',
'loadedMetadata',
'loadStart',
'lostPointerCapture',
'mouseDown',
'mouseMove',
'mouseOut',
'mouseOver',
'mouseUp',
'paste',
'pause',
'play',
'playing',
'pointerCancel',
'pointerDown',
'pointerMove',
'pointerOut',
'pointerOver',
'pointerUp',
'progress',
'rateChange',
'reset',
'resize',
'seeked',
'seeking',
'stalled',
'submit',
'suspend',
'timeUpdate',
'touchCancel',
'touchEnd',
'touchStart',
'volumeChange',
'scroll',
'scrollEnd',
'toggle',
'touchMove',
'waiting',
'wheel',
];

function registerSimpleEvent(domEventName: DOMEventName, reactName: string) {
topLevelEventsToReactNames.set(domEventName, reactName);
registerTwoPhaseEvent(reactName, [domEventName]);
}

export function registerSimpleEvents() {
for (let i = 0; i < simpleEventPluginEvents.length; i++) {
const eventName = ((simpleEventPluginEvents[i]: any): string);
const domEventName = ((eventName.toLowerCase(): any): DOMEventName);
const capitalizedEvent = eventName[0].toUpperCase() + eventName.slice(1);
registerSimpleEvent(domEventName, 'on' + capitalizedEvent);
}
// Special cases where event names don't match.
registerSimpleEvent(ANIMATION_END, 'onAnimationEnd');
registerSimpleEvent(ANIMATION_ITERATION, 'onAnimationIteration');
registerSimpleEvent(ANIMATION_START, 'onAnimationStart');
registerSimpleEvent('dblclick', 'onDoubleClick');
registerSimpleEvent('focusin', 'onFocus');
registerSimpleEvent('focusout', 'onBlur');
registerSimpleEvent(TRANSITION_END, 'onTransitionEnd');
}

EnterLeaveEventPlugin

1
2
3
4
5
6
7
8
// \react-dom-bindings\src\events\plugins\EnterLeaveEventPlugin.js

function registerEvents() {
registerDirectEvent('onMouseEnter', ['mouseout', 'mouseover']);
registerDirectEvent('onMouseLeave', ['mouseout', 'mouseover']);
registerDirectEvent('onPointerEnter', ['pointerout', 'pointerover']);
registerDirectEvent('onPointerLeave', ['pointerout', 'pointerover']);
}

ChangeEventPlugin

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// \react-dom-bindings\src\events\plugins\ChangeEventPlugin.js

function registerEvents() {
registerTwoPhaseEvent('onChange', [
'change',
'click',
'focusin',
'focusout',
'input',
'keydown',
'keyup',
'selectionchange',
]);
}

SelectEventPlugin

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// \react-dom-bindings\src\events\plugins\SelectEventPlugin.js
function registerEvents() {
registerTwoPhaseEvent('onSelect', [
'focusout',
'contextmenu',
'dragend',
'focusin',
'keydown',
'keyup',
'mousedown',
'mouseup',
'selectionchange',
]);
}

BeforeInputEventPlugin

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// \react-dom-bindings\src\events\plugins\BeforeInputEventPlugin.js
function registerEvents() {
registerTwoPhaseEvent('onBeforeInput', [
'compositionend',
'keypress',
'textInput',
'paste',
]);
registerTwoPhaseEvent('onCompositionEnd', [
'compositionend',
'focusout',
'keydown',
'keypress',
'keyup',
'mousedown',
]);
registerTwoPhaseEvent('onCompositionStart', [
'compositionstart',
'focusout',
'keydown',
'keypress',
'keyup',
'mousedown',
]);
registerTwoPhaseEvent('onCompositionUpdate', [
'compositionupdate',
'focusout',
'keydown',
'keypress',
'keyup',
'mousedown',
]);
}

事件绑定 - listenToAllSupportedEvents

React 初始化渲染的时候,会调⽤函数 listenToAllSupportedEvents 来绑定事件。listenAllSupportedEvents 将事件注册在页面的根节点,也就是 div#root

主要做了两件事情:

  • 为避免重复注册事件,在 DOM 上设置一个属性,如果有这个属性说明事件已经被注册了
  • 遍历 allNativeEvents,调用 listenToNativeEvent 进行事件注册
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
export function createRoot(
container: Element | Document | DocumentFragment,
options?: CreateRootOptions,
): RootType {
// ...

// comment nodes 已弃用,这里是为了兼容FB老代码 https://github.com/facebook/react/pull/24110
const rootContainerElement: Document | Element | DocumentFragment =
container.nodeType === COMMENT_NODE
? (container.parentNode: any)
: container;

// 事件绑定
listenToAllSupportedEvents(rootContainerElement);

return new ReactDOMRoot(root);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
// \react-dom-bindings\src\events\DOMPluginEventSystem.js
// 事件绑定
export function listenToAllSupportedEvents(rootContainerElement: EventTarget) {
if (!(rootContainerElement: any)[listeningMarker]) {
// sy 防止重复绑定
(rootContainerElement: any)[listeningMarker] = true;
allNativeEvents.forEach(domEventName => {
// We handle selectionchange separately because it
// doesn't bubble and needs to be on the document.
// 单独处理selectionchange事件,因为它不会冒泡,需要在文档上处理。
if (domEventName !== 'selectionchange') {
if (!nonDelegatedEvents.has(domEventName)) {
// ! 这些事件都是委托在rootContainerElement上的
// nonDelegatedEvents中都是不需要委托的事件,也就是不需要冒泡的,如cancel、close、invalid、load、scroll、scrollend、toggle等
listenToNativeEvent(domEventName, false, rootContainerElement);
}
listenToNativeEvent(domEventName, true, rootContainerElement);
}
});

// 单独处理selectionchange事件
const ownerDocument =
(rootContainerElement: any).nodeType === DOCUMENT_NODE
? rootContainerElement
: (rootContainerElement: any).ownerDocument;
if (ownerDocument !== null) {
// The selectionchange event also needs deduplication
// but it is attached to the document.
// selectionchange事件也需要去重,但它附加在document上。
if (!(ownerDocument: any)[listeningMarker]) {
(ownerDocument: any)[listeningMarker] = true;
listenToNativeEvent('selectionchange', false, ownerDocument);
}
}
}
}

// \react-dom-bindings\src\events\DOMPluginEventSystem.js
// We should not delegate these events to the container, but rather
// set them on the actual target element itself. This is primarily
// because these events do not consistently bubble in the DOM.
// 我们不应该将这些事件委托给容器,而是应该直接在实际的目标元素上设置它们。这主要是因为这些事件在DOM中的冒泡行为并不一致。
export const nonDelegatedEvents: Set<DOMEventName> = new Set([
'cancel',
'close',
'invalid',
'load',
'scroll',
'scrollend',
'toggle',
// In order to reduce bytes, we insert the above array of media events
// into this Set. Note: the "error" event isn't an exclusive media event,
// and can occur on other elements too. Rather than duplicate that event,
// we just take it from the media events array.
// 为了减少字节数,我们将上述媒体事件数组插入到这个 Set 中。
// 注意:"error" 事件并不是一个独占的媒体事件,也可能发生在其他元素上。我们不会重复这个事件,而是直接从媒体事件数组中取出。
...mediaEventTypes,
]);
// List of events that need to be individually attached to media elements.
// 需要分别附加到媒体元素的事件列表。
export const mediaEventTypes: Array<DOMEventName> = [
'abort',
'canplay',
'canplaythrough',
'durationchange',
'emptied',
'encrypted',
'ended',
'error',
'loadeddata',
'loadedmetadata',
'loadstart',
'pause',
'play',
'playing',
'progress',
'ratechange',
'resize',
'seeked',
'seeking',
'stalled',
'suspend',
'timeupdate',
'volumechange',
'waiting',
];

listenToNativeEvent

这个函数只做一件事情:定义一个 flags 变量表示当前是冒泡还是捕获,然后调用 addTrappedEventListener

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// \packages\react-dom-bindings\src\events\DOMPluginEventSystem.js
export function listenToNativeEvent(
domEventName: DOMEventName, // 原生事件名
isCapturePhaseListener: boolean, // 是否是捕获阶段
target: EventTarget, // 事件绑定的目标节点,也就是 div#root
): void {

// 冒泡用 0 表示,捕获用 4 表示
let eventSystemFlags = 0;
if (isCapturePhaseListener) {
eventSystemFlags |= IS_CAPTURE_PHASE;
}
addTrappedEventListener(
target,
domEventName,
eventSystemFlags,
isCapturePhaseListener,
);
}

addTrappedEventListener

这个函数主要做了两件事:

  1. 创建一个监听器,用于事件挂载,具体查看 createEventListenerWrapperWithPriority
  2. 事件挂载,将事件挂载到目标节点上,也就是将事件绑定到 div#root
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
// \packages\react-dom-bindings\src\events\DOMPluginEventSystem.js
function addTrappedEventListener(
targetContainer: EventTarget, // 事件挂载节点,也就是 div#root
domEventName: DOMEventName, // 原生事件名,比如 click
eventSystemFlags: EventSystemFlags, // 4 表示捕获,0 表示冒泡
isCapturePhaseListener: boolean, // true 表示捕获阶段,false 表示冒泡阶段
isDeferredListenerForLegacyFBSupport?: boolean,
) {
// 获取对应事件,事件定义在ReactDOMEventListener.js中
// 如DiscreteEventPriority对应dispatchDiscreteEvent,ContinuousEventPriority对应dispatchContinuousEvent
// 创建一个事件监听器
let listener = createEventListenerWrapperWithPriority(
targetContainer,
domEventName,
eventSystemFlags,
);
// If passive option is not supported, then the event will be
// active and not passive.
let isPassiveListener: void | boolean = undefined;
if (passiveBrowserEventsSupported) {
// sy
// Browsers introduced an intervention, making these events
// passive by default on document. React doesn't bind them
// to document anymore, but changing this now would undo
// the performance wins from the change. So we emulate
// the existing behavior manually on the roots now.
// 浏览器引入了一种干预措施,使这些事件在document上默认为passive状态。
// React不再将它们绑定到document上,但是现在改变这一点将会撤销之前的性能优势。
// 因此,我们现在在根节点上手动模拟现有的行为。
// https://github.com/facebook/react/issues/19651
if (
domEventName === 'touchstart' ||
domEventName === 'touchmove' ||
domEventName === 'wheel'
) {
isPassiveListener = true;
}
}

// React17之后,事件委托在targetContainer,但是兼容之前的版本委托在document
targetContainer =
enableLegacyFBSupport && isDeferredListenerForLegacyFBSupport
? (targetContainer: any).ownerDocument
: targetContainer;

let unsubscribeListener;
// When legacyFBSupport is enabled, it's for when we
// want to add a one time event listener to a container.
// This should only be used with enableLegacyFBSupport
// due to requirement to provide compatibility with
// internal FB www event tooling. This works by removing
// the event listener as soon as it is invoked. We could
// also attempt to use the {once: true} param on
// addEventListener, but that requires support and some
// browsers do not support this today, and given this is
// to support legacy code patterns, it's likely they'll
// need support for such browsers.
// 当启用legacyFBSupport时,是为了当我们想要向container添加一次性事件监听器时使用。
// 这应该只与enableLegacyFBSupport一起使用,因为需要与内部FB www事件工具提供的兼容性。
// 这通过在调用后立即移除事件监听器来实现。我们也可以尝试在addEventListener上使用{once: true}参数,但这需要支持,
// 一些浏览器今天不支持这一点,考虑到这是为了支持传统代码模式,它们可能需要支持这些浏览器。
if (enableLegacyFBSupport && isDeferredListenerForLegacyFBSupport) {
const originalListener = listener;
listener = function (...p) {
removeEventListener(
targetContainer,
domEventName,
unsubscribeListener,
isCapturePhaseListener,
);
return originalListener.apply(this, p);
};
}
// TODO: There are too many combinations here. Consolidate them.

if (isCapturePhaseListener) {
// ! 捕获阶段
// sy
if (isPassiveListener !== undefined) {
// touchstart、touchmove、wheel
unsubscribeListener = addEventCaptureListenerWithPassiveFlag(
targetContainer,
domEventName,
listener,
isPassiveListener,
);
} else {
// sy
// click、contextmenu、drag、drop、input、mousedown、change等事件
unsubscribeListener = addEventCaptureListener(
targetContainer,
domEventName,
listener,
);
}
} else {
// ! 冒泡阶段
if (isPassiveListener !== undefined) {
// touchstart、touchmove、wheel
unsubscribeListener = addEventBubbleListenerWithPassiveFlag(
targetContainer,
domEventName,
listener,
isPassiveListener,
);
} else {
// click、contextmenu、drag、drop、input、mousedown、change等事件
// sy
unsubscribeListener = addEventBubbleListener(
targetContainer,
domEventName,
listener,
);
}
}
}

createEventListenerWrapperWithPriority

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// \packages\react-dom-bindings\src\events\ReactDOMEventListener.js
export function createEventListenerWrapperWithPriority(
targetContainer: EventTarget,
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
): Function {
// 根据事件名称,获取优先级。比如click、input、drop等对应DiscreteEventPriority,drag、scroll等对应ContinuousEventPriority,
// message也许处于Scheduler中,根据getCurrentSchedulerPriorityLevel()获取优先级。其它是DefaultEventPriority。
const eventPriority = getEventPriority(domEventName);
let listenerWrapper;
switch (eventPriority) {
case DiscreteEventPriority:
listenerWrapper = dispatchDiscreteEvent;
break;
case ContinuousEventPriority:
listenerWrapper = dispatchContinuousEvent;
break;
case DefaultEventPriority:
default:
listenerWrapper = dispatchEvent;
break;
}
return listenerWrapper.bind(
null,
domEventName,
eventSystemFlags,
targetContainer,
);
}

getEventPriority

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
export function getEventPriority(domEventName: DOMEventName): EventPriority {
switch (domEventName) {
// Used by SimpleEventPlugin:
case 'cancel':
case 'click':
case 'close':
case 'contextmenu':
case 'copy':
case 'cut':
case 'auxclick':
case 'dblclick':
case 'dragend':
case 'dragstart':
case 'drop':
case 'focusin':
case 'focusout':
case 'input':
case 'invalid':
case 'keydown':
case 'keypress':
case 'keyup':
case 'mousedown':
case 'mouseup':
case 'paste':
case 'pause':
case 'play':
case 'pointercancel':
case 'pointerdown':
case 'pointerup':
case 'ratechange':
case 'reset':
case 'resize':
case 'seeked':
case 'submit':
case 'touchcancel':
case 'touchend':
case 'touchstart':
case 'volumechange':
// Used by polyfills: (fall through)
case 'change':
case 'selectionchange':
case 'textInput':
case 'compositionstart':
case 'compositionend':
case 'compositionupdate':
// Only enableCreateEventHandleAPI: (fall through)
case 'beforeblur':
case 'afterblur':
// Not used by React but could be by user code: (fall through)
case 'beforeinput':
case 'blur':
case 'fullscreenchange':
case 'focus':
case 'hashchange':
case 'popstate':
case 'select':
case 'selectstart':
return DiscreteEventPriority;
case 'drag':
case 'dragenter':
case 'dragexit':
case 'dragleave':
case 'dragover':
case 'mousemove':
case 'mouseout':
case 'mouseover':
case 'pointermove':
case 'pointerout':
case 'pointerover':
case 'scroll':
case 'toggle':
case 'touchmove':
case 'wheel':
// Not used by React but could be by user code: (fall through)
case 'mouseenter':
case 'mouseleave':
case 'pointerenter':
case 'pointerleave':
return ContinuousEventPriority;
case 'message': {
// We might be in the Scheduler callback.
// Eventually this mechanism will be replaced by a check
// of the current priority on the native scheduler.
const schedulerPriority = getCurrentSchedulerPriorityLevel();
switch (schedulerPriority) {
case ImmediateSchedulerPriority:
return DiscreteEventPriority;
case UserBlockingSchedulerPriority:
return ContinuousEventPriority;
case NormalSchedulerPriority:
case LowSchedulerPriority:
// TODO: Handle LowSchedulerPriority, somehow. Maybe the same lane as hydration.
return DefaultEventPriority;
case IdleSchedulerPriority:
return IdleEventPriority;
default:
return DefaultEventPriority;
}
}
default:
return DefaultEventPriority;
}
}

事件挂载 - 捕获阶段

支持passive - addEventCaptureListenerWithPassiveFlag
1
2
3
4
5
6
7
8
9
10
11
12
13
// \packages\react-dom-bindings\src\events\EventListener.js
export function addEventCaptureListenerWithPassiveFlag(
target: EventTarget,
eventType: string,
listener: Function,
passive: boolean,
): Function {
target.addEventListener(eventType, listener, {
capture: true,
passive,
});
return listener;
}
addEventCaptureListener
1
2
3
4
5
6
7
8
9
// \packages\react-dom-bindings\src\events\EventListener.js
export function addEventCaptureListener(
target: EventTarget,
eventType: string,
listener: Function,
): Function {
target.addEventListener(eventType, listener, true);
return listener;
}

事件挂载 - 冒泡阶段

支持passive - addEventBubbleListenerWithPassiveFlag
1
2
3
4
5
6
7
8
9
10
11
12
13
// \packages\react-dom-bindings\src\events\EventListener.js
export function addEventCaptureListenerWithPassiveFlag(
target: EventTarget,
eventType: string,
listener: Function,
passive: boolean,
): Function {
target.addEventListener(eventType, listener, {
capture: true,
passive,
});
return listener;
}
addEventBubbleListener
1
2
3
4
5
6
7
8
9
// \packages\react-dom-bindings\src\events\EventListener.js
export function addEventBubbleListener(
target: EventTarget,
eventType: string,
listener: Function,
): Function {
target.addEventListener(eventType, listener, false);
return listener;
}

事件派发

dispatchDiscreteEvent

适用事件:click、drop、input、drop 等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function dispatchDiscreteEvent(
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
container: EventTarget,
nativeEvent: AnyNativeEvent,
) {
// ! 1. 记录上一次的事件优先级
const previousPriority = getCurrentUpdatePriority();
// ! 2. 记录上一次的transition
const prevTransition = ReactCurrentBatchConfig.transition;
// !3. 清空transition,transition为非紧急更新,这里不处理
ReactCurrentBatchConfig.transition = null;
try {
// !4. 设置当前事件优先级为DiscreteEventPriority
setCurrentUpdatePriority(DiscreteEventPriority);
// !5. 调用dispatchEvent,执行事件
dispatchEvent(domEventName, eventSystemFlags, container, nativeEvent);
} finally {
// !6. 恢复
setCurrentUpdatePriority(previousPriority);
ReactCurrentBatchConfig.transition = prevTransition;
}
}

事件优先级记录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// \react-reconciler\src\ReactEventPriorities.js

export opaque type EventPriority = Lane;

// 优先级从上往下,越来越小
export const DiscreteEventPriority: EventPriority = SyncLane; // 2
export const ContinuousEventPriority: EventPriority = InputContinuousLane; // 8
export const DefaultEventPriority: EventPriority = DefaultLane; // 页面初次渲染的lane 32, transition
export const IdleEventPriority: EventPriority = IdleLane;

let currentUpdatePriority: EventPriority = NoLane;

export function getCurrentUpdatePriority(): EventPriority {
return currentUpdatePriority;
}

export function setCurrentUpdatePriority(newPriority: EventPriority) {
currentUpdatePriority = newPriority;
}

dispatchContinuousEvent

适用事件:drag、mouse的各种事件等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function dispatchContinuousEvent(
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
container: EventTarget,
nativeEvent: AnyNativeEvent,
) {
const previousPriority = getCurrentUpdatePriority();
const prevTransition = ReactCurrentBatchConfig.transition;
ReactCurrentBatchConfig.transition = null;
try {
setCurrentUpdatePriority(ContinuousEventPriority);
dispatchEvent(domEventName, eventSystemFlags, container, nativeEvent);
} finally {
setCurrentUpdatePriority(previousPriority);
ReactCurrentBatchConfig.transition = prevTransition;
}
}

dispatchEvent

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
// \react-dom-bindings\src\events\ReactDOMEventListener.js

// 派发事件
export function dispatchEvent(
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
targetContainer: EventTarget,
nativeEvent: AnyNativeEvent,
): void {
// 有些场景下是禁止事件的,比如在commit阶段
if (!_enabled) {
return;
}

let blockedOn = findInstanceBlockingEvent(nativeEvent);

if (blockedOn === null) {
dispatchEventForPluginEventSystem(
domEventName,
eventSystemFlags,
nativeEvent,
return_targetInst,
targetContainer,
);

clearIfContinuousEvent(domEventName, nativeEvent);
return;
}

if (
queueIfContinuousEvent(
blockedOn,
domEventName,
eventSystemFlags,
targetContainer,
nativeEvent,
)
) {
nativeEvent.stopPropagation();
return;
}
// We need to clear only if we didn't queue because
// queueing is accumulative.
clearIfContinuousEvent(domEventName, nativeEvent);

if (
eventSystemFlags & IS_CAPTURE_PHASE &&
isDiscreteEventThatRequiresHydration(domEventName)
) {
while (blockedOn !== null) {
const fiber = getInstanceFromNode(blockedOn);
if (fiber !== null) {
attemptSynchronousHydration(fiber);
}
const nextBlockedOn = findInstanceBlockingEvent(nativeEvent);
if (nextBlockedOn === null) {
dispatchEventForPluginEventSystem(
domEventName,
eventSystemFlags,
nativeEvent,
return_targetInst,
targetContainer,
);
}
if (nextBlockedOn === blockedOn) {
break;
}
blockedOn = nextBlockedOn;
}
if (blockedOn !== null) {
nativeEvent.stopPropagation();
}
return;
}

// This is not replayable so we'll invoke it but without a target,
// in case the event system needs to trace it.
dispatchEventForPluginEventSystem(
domEventName,
eventSystemFlags,
nativeEvent,
null,
targetContainer,
);
}

findInstanceBlockingEvent

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
// \react-dom-bindings\src\events\ReactDOMEventListener.js

export function findInstanceBlockingEvent(
nativeEvent: AnyNativeEvent,
): null | Container | SuspenseInstance {
const nativeEventTarget = getEventTarget(nativeEvent);
return findInstanceBlockingTarget(nativeEventTarget);
}

export let return_targetInst: null | Fiber = null;

// Returns a SuspenseInstance or Container if it's blocked.
// The return_targetInst field above is conceptually part of the return value.
// 如果被阻塞,返回一个 SuspenseInstance 或 Container。
// 上面的 return_targetInst 字段在概念上是返回值的一部分。
export function findInstanceBlockingTarget(
targetNode: Node,
): null | Container | SuspenseInstance {
// TODO: Warn if _enabled is false.

return_targetInst = null;

// 通过 targetNode 获取最近的 Fiber 实例
let targetInst = getClosestInstanceFromNode(targetNode);

if (targetInst !== null) {
// 寻找最近的已挂载的 Fiber 实例
const nearestMounted = getNearestMountedFiber(targetInst);
if (nearestMounted === null) {
// This tree has been unmounted already. Dispatch without a target.
// 这棵树已经被卸载了。在没有目标的情况下进行派发。
targetInst = null;
} else {
const tag = nearestMounted.tag;
if (tag === SuspenseComponent) {
// 寻找最近的已挂载的 Suspense 实例
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.
// 如果在提交该组件的挂载之前收到事件(例如:图片加载完成),暂时忽略它(也就是,将其视为在非React树上的事件)。
// 我们也可以考虑将事件排队,并在挂载后分发它们。
targetInst = null;
}
}
}
return_targetInst = targetInst;
// We're not blocked on anything.
// 没有阻塞
return null;
}

dispatchEventForPluginEventSystem

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
// \react-dom-bindings\src\events\DOMPluginEventSystem.js

export function dispatchEventForPluginEventSystem(
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
nativeEvent: AnyNativeEvent,
targetInst: null | Fiber,
targetContainer: EventTarget,
): void {
let ancestorInst = targetInst;
if (
(eventSystemFlags & IS_EVENT_HANDLE_NON_MANAGED_NODE) === 0 &&
(eventSystemFlags & IS_NON_DELEGATED) === 0
) {
const targetContainerNode = ((targetContainer: any): Node);

// If we are using the legacy FB support flag, we
// defer the event to the null with a one
// time event listener so we can defer the event.
if (
enableLegacyFBSupport &&
// If our event flags match the required flags for entering
// FB legacy mode and we are processing the "click" event,
// then we can defer the event to the "document", to allow
// for legacy FB support, where the expected behavior was to
// match React < 16 behavior of delegated clicks to the doc.
domEventName === 'click' &&
(eventSystemFlags & SHOULD_NOT_DEFER_CLICK_FOR_FB_SUPPORT_MODE) === 0 &&
!isReplayingEvent(nativeEvent)
) {
deferClickToDocumentForLegacyFBSupport(domEventName, targetContainer);
return;
}
if (targetInst !== null) {
// The below logic attempts to work out if we need to change
// the target fiber to a different ancestor. We had similar logic
// in the legacy event system, except the big difference between
// systems is that the modern event system now has an event listener
// attached to each React Root and React Portal Root. Together,
// the DOM nodes representing these roots are the "rootContainer".
// To figure out which ancestor instance we should use, we traverse
// up the fiber tree from the target instance and attempt to find
// root boundaries that match that of our current "rootContainer".
// If we find that "rootContainer", we find the parent fiber
// sub-tree for that root and make that our ancestor instance.
let node: null | Fiber = targetInst;

mainLoop: while (true) {
if (node === null) {
// 事件没有对应的fiber,没法执行事件,退出
return;
}
const nodeTag = node.tag;
if (nodeTag === HostRoot || nodeTag === HostPortal) {
let container = node.stateNode.containerInfo;
if (isMatchingRootContainer(container, targetContainerNode)) {
// container和targetContainerNode相等,说明找到了对应的rootContainer
break;
}
if (nodeTag === HostPortal) {
// The target is a portal, but it's not the rootContainer we're looking for.
// Normally portals handle their own events all the way down to the root.
// So we should be able to stop now. However, we don't know if this portal
// was part of *our* root.
let grandNode = node.return;
while (grandNode !== null) {
const grandTag = grandNode.tag;
if (grandTag === HostRoot || grandTag === HostPortal) {
const grandContainer = grandNode.stateNode.containerInfo;
if (
isMatchingRootContainer(grandContainer, targetContainerNode)
) {
// This is the rootContainer we're looking for and we found it as
// a parent of the Portal. That means we can ignore it because the
// Portal will bubble through to us.
return;
}
}
grandNode = grandNode.return;
}
}
// Now we need to find it's corresponding host fiber in the other
// tree. To do this we can use getClosestInstanceFromNode, but we
// need to validate that the fiber is a host instance, otherwise
// we need to traverse up through the DOM till we find the correct
// node that is from the other tree.
while (container !== null) {
const parentNode = getClosestInstanceFromNode(container);
if (parentNode === null) {
return;
}
const parentTag = parentNode.tag;
if (
parentTag === HostComponent ||
parentTag === HostText ||
(enableFloat ? parentTag === HostHoistable : false) ||
parentTag === HostSingleton
) {
node = ancestorInst = parentNode;
continue mainLoop;
}
container = container.parentNode;
}
}
node = node.return;
}
}
}

// 批量更新
batchedUpdates(() =>
dispatchEventsForPlugins(
domEventName,
eventSystemFlags,
nativeEvent,
ancestorInst,
targetContainer,
)
);
}

batchedUpdates

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// \react-dom-bindings\src\events\ReactDOMUpdateBatching.js
export function batchedUpdates(fn, a, b) {
if (isInsideEventHandler) {
// If we are currently inside another batch, we need to wait until it
// fully completes before restoring state.
// 如果我们当前正在另一个批处理中,需要等待其完全完成后再恢复状态。
return fn(a, b);
}
isInsideEventHandler = true;
try {
return batchedUpdatesImpl(fn, a, b);
} finally {
isInsideEventHandler = false;
finishEventHandler();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// \packages\react-reconciler\src\ReactFiberWorkLoop.js
export function batchedUpdates<A, R>(fn: A => R, a: A): R {
const prevExecutionContext = executionContext;
executionContext |= BatchedContext;
try {
return fn(a);
} finally {
executionContext = prevExecutionContext;
// If there were legacy sync updates, flush them at the end of the outer
// most batchedUpdates-like method.
if (
executionContext === NoContext &&
// Treat `act` as if it's inside `batchedUpdates`, even in legacy mode.
!(__DEV__ && ReactCurrentActQueue.isBatchingLegacy)
) {
resetRenderTimer();
flushSyncWorkOnLegacyRootsOnly();
}
}
}

dispatchEventsForPlugins

这个函数主要做了两件事:

  1. extractEvents - 提取事件,将事件函数放到 dispatchQueue 队列中
  2. processDispatchQueue - 执行事件,将 dispatchQueue 队列中的事件函数依次执行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function dispatchEventsForPlugins(
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
nativeEvent: AnyNativeEvent,
targetInst: null | Fiber,
targetContainer: EventTarget,
): void {
// nativeEvent.target
const nativeEventTarget = getEventTarget(nativeEvent);
const dispatchQueue: DispatchQueue = [];
extractEvents(
dispatchQueue,
domEventName,
targetInst,
nativeEvent,
nativeEventTarget,
eventSystemFlags,
targetContainer,
);
processDispatchQueue(dispatchQueue, eventSystemFlags);
}

extractEvents

在一开始事件注册时,不同类型的事件由不同的事件插件进行注册,它们的特征可能不一样,但是不同类型的事件底层处理的逻辑是相同的,所以 extractEvents 函数调用了 SimpleEventPlugin.extractEvents 函数,用来提取事件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function extractEvents(
dispatchQueue: DispatchQueue,
domEventName: DOMEventName,
targetInst: null | Fiber,
nativeEvent: AnyNativeEvent,
nativeEventTarget: null | EventTarget,
eventSystemFlags: EventSystemFlags,
targetContainer: EventTarget,
) {
// 调用插件的 extractEvents 函数,用来提取事件函数
SimpleEventPlugin.extractEvents(
dispatchQueue,
domEventName,
targetInst,
nativeEvent,
nativeEventTarget,
eventSystemFlags,
targetContainer,
);
// ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
// \react-dom-bindings\src\events\plugins\SimpleEventPlugin.js
function extractEvents(
dispatchQueue: DispatchQueue,
domEventName: DOMEventName,
targetInst: null | Fiber,
nativeEvent: AnyNativeEvent,
nativeEventTarget: null | EventTarget,
eventSystemFlags: EventSystemFlags,
targetContainer: EventTarget,
): void {
// 1.根据原生事件名拿到 react 事件名
const reactName = topLevelEventsToReactNames.get(domEventName);
if (reactName === undefined) {
return;
}
// 2.定义一个合成事件
let SyntheticEventCtor = SyntheticEvent;
let reactEventType: string = domEventName;
// 3.根据原生事件名调用不同的合成事件
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).
// TODO: Fixed in https://bugzilla.mozilla.org/show_bug.cgi?id=968056. Can
// probably remove.
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.
// TODO: Fixed in https://phabricator.services.mozilla.com/D26793. Can
// probably remove.
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':
case 'scrollend':
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.
// TODO:理想情况下,我们最终会将nonDelegatedEvents列表中的所有事件添加到DOMPluginEventSystem中。
// 然后我们可以移除这个特殊列表。这是一个破坏性的变更,可以等到React 18再处理。
(domEventName === 'scroll' || domEventName === 'scrollend');

// 4.收集从事件源到事件绑定的地方所有的事件函数
const listeners = accumulateSinglePhaseListeners(
targetInst,
reactName,
nativeEvent.type,
inCapturePhase,
accumulateTargetOnly,
nativeEvent,
);
// 5.将事件函数保存到事件函数队列中
if (listeners.length > 0) {
// Intentionally create event lazily.
const event: ReactSyntheticEvent = new SyntheticEventCtor(
reactName,
reactEventType,
null,
nativeEvent,
nativeEventTarget,
);
dispatchQueue.push({event, listeners});
}
}

processDispatchQueue

processDispatchQueue 函数主要功能是执行事件函数队列中的事件函数

dispatchQueue 队列的数据结构如下:

1
2
3
4
5
6
[
{
event, // 合成事件对象
listeners: [事件源对应的事件函数, 事件源父节点对应的事件函数, ..., div#root 对应的事件函数], // 事件函数队列
},
];
1
2
3
4
5
6
7
8
9
10
11
12
13
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);
// event system doesn't use pooling.
}
// This would be a good time to rethrow if any of the event handlers threw.
rethrowCaughtError();
}

具体执行是由 processDispatchQueueItemsInOrder 执行

  1. 判断是不是捕获阶段
    1. 如果是捕获阶段,就从后往前执行
    2. 如果是冒泡阶段,就从前往后执行
    3. 在执行事件函数之前,如果事件被阻止了,则不在继续执行事件函数
  2. 事件函数执行由 executeDispatch 函数完成
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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;
}
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

1
2
3
4
5
6
7
8
9
10
function executeDispatch(
event: ReactSyntheticEvent,
listener: Function,
currentTarget: EventTarget,
): void {
const type = event.type || 'unknown-event';
event.currentTarget = currentTarget;
invokeGuardedCallbackAndCatchFirstError(type, listener, undefined, event);
event.currentTarget = null;
}

invokeGuardedCallbackAndCatchFirstError

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
export function invokeGuardedCallbackAndCatchFirstError<
A,
B,
C,
D,
E,
F,
Context,
>(
this: mixed,
name: string | null,
func: (a: A, b: B, c: C, d: D, e: E, f: F) => void,
context: Context,
a: A,
b: B,
c: C,
d: D,
e: E,
f: F,
): void {
invokeGuardedCallback.apply(this, arguments);
if (hasError) {
const error = clearCaughtError();
if (!hasRethrowError) {
hasRethrowError = true;
rethrowError = error;
}
}
}

export function invokeGuardedCallback<A, B, C, D, E, F, Context>(
name: string | null,
func: (a: A, b: B, c: C, d: D, e: E, f: F) => mixed,
context: Context,
a: A,
b: B,
c: C,
d: D,
e: E,
f: F,
): void {
hasError = false;
caughtError = null;
invokeGuardedCallbackImpl.apply(reporter, arguments);
}

export default function invokeGuardedCallbackImpl<Args: Array<mixed>, Context>(
this: {onError: (error: mixed) => void},
name: string | null,
func: (...Args) => mixed,
context: Context,
): void {
const funcArgs = Array.prototype.slice.call(arguments, 3);
try {
func.apply(context, funcArgs);
} catch (error) {
this.onError(error);
}
}

总结

事件系统核心原理比较简单,主要的作用是抹平各浏览器之间的差异。

通过 react 合成事件的学习,学习 react 对于事件系统的分层设计,它将不同类型的事件做成了插件,每个插件提供注册事件和提取事件两个接口,插件自身去实现事件的注册和事件提取。

这样做的好处是,当我们需要扩展新的事件时,只需要实现这两个接口就可以了,不需要修改原有的代码。

问题1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 2 3 4 5 7 6 1(当前调度未超出时间切片)
// 2 3 4 5 6 1 7(当前调度超出时间切片, 在后面加sleep也可以看出来,effect的调度放在了之后的宏任务中执行)
import { useEffect } from "react";
import { createRoot } from "react-dom/client";

export const App = ({ name }) => {
console.log("log:", 5);

Promise.resolve().then(() => {
console.log("log:", 6);
});

useEffect(() => {
console.log("log:", 7);
});

return (
<div>
<h1>Hello {name}!</h1>
<p>Start editing to see some magic happen :)</p>
</div>
);
};

const root = createRoot(document.getElementById("root"));

console.log("log:", 2);
root.render(<App name="StackBlitz" />);
console.log("log:", 3);

Promise.resolve().then(() => {
console.log("log:", 4);
});

setTimeout(() => {
console.log("log:", 1);
}, 0);

问题2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// 把setTimeout放在render之前的执行顺序
// 2 3 4 1 5 7 6
// 2 3 4 1 5 6 7
import { useEffect } from "react";
import { createRoot } from "react-dom/client";

export const App = ({ name }) => {
console.log("log:", 5);

Promise.resolve().then(() => {
console.log("log:", 6);
});

useEffect(() => {
console.log("log:", 7);
});

return (
<div>
<h1>Hello {name}!</h1>
<p>Start editing to see some magic happen :)</p>
</div>
);
};

const root = createRoot(document.getElementById("root"));

console.log("log:", 2);
setTimeout(() => {
console.log("log:", 1);
}, 0);

root.render(<App name="StackBlitz" />);
console.log("log:", 3);

Promise.resolve().then(() => {
console.log("log:", 4);
});

问题3

从源码来看,effectcreate 函数是在一个被调度的 callback 里面去执行的,为什么它还是会比 Promise.then()callback 函数先执行?

useEffect 是在 commit 阶段被 scheduler 加入到 taskQueue 中的,一个时间切片(5ms)内,如果时间允许,可能会执行很多个 task

taskQueue 中的任务又是在宏任务中被执行的,先于微任务。

总之,当前时间切片时间允许就是 effectPromise.then()前,如果时间不够 effectPromise.then() 后。

问题4

如果中间加了个sleep呢?如果不加呢?

useEffect 是在 commit 阶段被 scheduler 加入到 taskQueue 中的。

  • 加sleep,导致超出时间切片5ms,taskQueue中的调度就要等到下一个宏任务再去调度
  • 不加sleep,可能在5ms之内,useEffect的调度就被执行了,也有可能有各种高优先级的任务在 useEffect 前导致以后得调度再执行 useEffect
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53

// 加:2 3 4 5 6 1 7
// 不加:2 3 4 5 7 6 1 或者 2 3 4 5 6 1 7

import { useEffect } from "react";
import { createRoot } from "react-dom/client";

function sleep() {
const start = performance.now();
while (performance.now() - start < 10) {
// block
}
}

const Child = () => {
useEffect(() => {
console.log("log:", 7);
});

return null;
};

export const App = ({ name }) => {
console.log("log:", 5);

Promise.resolve().then(() => {
console.log("log:", 6);
});

// sleep();

return (
<div>
<h1>Hello {name}!</h1>
<p>Start editing to see some magic happen :)</p>
<Child />
</div>
);
};

const root = createRoot(document.getElementById("root"));

console.log("log:", 2);
root.render(<App name="StackBlitz" />);
console.log("log:", 3);

Promise.resolve().then(() => {
console.log("log:", 4);
});

setTimeout(() => {
console.log("log:", 1);
}, 0);

问题5

5.1 刷新页面,console.log打印了几次?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { useState, useEffect } from 'react'

export default function UseABC() {
const [count, setCount] = useState(-1)
const [count1, setCount1] = useState(-100)

useEffect(() => {
setCount1(0)
})

console.log('x') //sy-log

return (
<>
<h3>UseEffectPage {count1}</h3>
</>
)
}

答案:3次。

  • 第一次:函数组件初次渲染,执行log函数,并且此次由于useEffect函数执行,在fiber节点上记录了包含create函数的effect对象;

  • 第二次:源于组件初次渲染完成之后,延迟useEffect的create函数,此时create函数中执行的是setState事件。setState导致函数组件更新,那么再次执行函数组件这个函数,log再次打印。并且此次又记录了create函数的effect对象。

  • 第三次:基本同第二次,不同的地方在于第三次的时候,前后两次状态值相同,函数组件检测到没有更新发生,bailout了。

  • 详细过程:

    • 初始化执行UseABC组件函数,flush -> workloop -> performConcurrentWorkOnRoot,进到render阶段,在当前fiber的updateQueue上记录了要更新的effect,打印一次log
    • 初始化到了commit阶段,通过 scheduleCallback 向 taskQueue 加入一个优先级为 NormalPriority 的 flushPassiveEffects 任务
      • /* commit的最后还是会ensureRootIsScheduled调度一次微任务scheduleImmediateTask(processRootScheduleInMicrotask);或者performConcurrentWorkOnRoot的最后调度scheduleTaskForRootDuringMicrotask是否要加入高优先级任务,可能涉及到其它高优先级任务插队执行的问题,在flushWork结束的finally里判断如果有任务再次发起宏任务调度 */
    • 不考虑高优先级任务插队或者其他各种情况的话,正常执行完毕后回到 workloop 中,会取出task 或者在下次切片中取出task执行,所以 flushPassiveEffects 任务得以执行,在从root开始遍历,执行各个fiber上的updateQueue的effect
    • 执行到setCount1时候,等于执行dispatchSetState.bind(null,currentlyRenderingFiber,queue),相当于增加了一个调度更新scheduleUpdateOnFiber,cheduleUpdateOnFiber执行时会标记root上的更新也会把对应的update添加到对应fiber上,最后通过 ensureRootIsScheduled 发起微任务调度,但是commit已经发起过了,就不重复发起了,最终是通过微任务把setCount1加入到了taskQueue中
    • 不管中间发生了什么,当再次执行与setCount1任务相同优先级的调度时候,从root开始遍历,再次执行UseABC组件函数,count1变成了0,且再打印一次log(执行UseABC组件函数的过程中又遇到了useEffect执行setCount1,又添加了调度更新)
    • 当再次执行与此次任务相同优先级的调度时候,那就再次同上执行UseABC组件函数,再打印一次log,但是不同的是,现在count1的值没变化,bailout了

5.2 如果useEffect依赖为[],console.log打印了几次?

答案:2次

  • 第一次:函数组件初次渲染,执行log函数,并且此次由于useEffect函数执行,在fiber节点上记录了包含create函数的effect对象;
  • 第二次:源于组件初次渲染完成之后,延迟useEffect的create函数,此时create函数中执行的是setState事件。setState导致函数组件更新,那么再次执行函数组件这个函数,log再次打印。但是此次useEffect没有变化,故不再后续执行。

useEffect

初始化

先来解读下几个参数:

  • fiberFlags:有副作用的更新标记,用来标记hook所在的fiber;
  • hookFlags:副作用标记;
  • create:使用者传入的回调函数;
  • deps:使用者传入的数组依赖;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function mountEffectImpl(
fiberFlags: Flags,
hookFlags: HookFlags,
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
const hook = mountWorkInProgressHook();
// 判断是否有传入deps,如果有会作为下次更新的deps
const nextDeps = deps === undefined ? null : deps;
// 给hook所在的fiber打上有副作用的更新的标记
currentlyRenderingFiber.flags |= fiberFlags;
// 将副作用操作存放到fiber.memoizedState.hook.memoizedState中
hook.memoizedState = pushEffect(
HookHasEffect | hookFlags,
create,
createEffectInstance(),
nextDeps,
);
}

function createEffectInstance(): EffectInstance {
return {destroy: undefined};
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
function pushEffect(
tag: HookFlags,
create: () => (() => void) | void,
inst: EffectInstance,
deps: Array<mixed> | null,
): Effect {
// 初始化副作用结构
const effect: Effect = {
tag,
create,
inst,
deps,
// Circular
next: (null: any),
};
// 向fiber的updateQueue上添加effect,并形成环形链表
let componentUpdateQueue: null | FunctionComponentUpdateQueue =
(currentlyRenderingFiber.updateQueue: any);
if (componentUpdateQueue === null) {
componentUpdateQueue = createFunctionComponentUpdateQueue();
currentlyRenderingFiber.updateQueue = (componentUpdateQueue: any);
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
const lastEffect = componentUpdateQueue.lastEffect;
if (lastEffect === null) {
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
const firstEffect = lastEffect.next;
lastEffect.next = effect;
effect.next = firstEffect;
componentUpdateQueue.lastEffect = effect;
}
}

return effect;
}

上面这段代码除了初始化副作用的结构代码外,还有就是操作闭环链表,向链表末尾添加新的 effect,该 effect.next 指向 fisrtEffect,并且链表当前的指针指向最新添加的 effect

简单总结一下:给 hook 所在的 fiber 打上副作用更新标记,并且 fiber.memoizedState.hook.memoizedStatefiber.updateQueue 存储了相关的副作用,这些副作用通过闭环链表的结构存储。

alt text

更新

主要功能就是创建一个带有回调函数的 newHook 去覆盖之前的 hook

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
function updateEffectImpl(
fiberFlags: Flags,
hookFlags: HookFlags,
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
const effect: Effect = hook.memoizedState;
const inst = effect.inst;

// currentHook is null on initial mount when rerendering after a render phase
// state update or for strict mode.
if (currentHook !== null) {
if (nextDeps !== null) {
const prevEffect: Effect = currentHook.memoizedState;
const prevDeps = prevEffect.deps;
// 比较两次依赖数组中的值是否有变化
if (areHookInputsEqual(nextDeps, prevDeps)) {
hook.memoizedState = pushEffect(hookFlags, create, inst, nextDeps);
return;
}
}
}

currentlyRenderingFiber.flags |= fiberFlags;

hook.memoizedState = pushEffect(
HookHasEffect | hookFlags,
create,
inst,
nextDeps,
);
}

alt text

create 和 destroy 函数是怎样被调用?

在弄清楚怎样被调用之前,我们先来看 destroy 函数的存在性问题。即:在 hookmount 阶段,我们创建 effect 对象的时候,destroy 函数是不存在的。因为,destroy 函数本来就是 create 函数的返回值。而此时 create 函数还没有被调用。

而我们知道,组件 mount 阶段过后, create 函数是一定会被调用的。所以,我们也可以推理得出,在组件的 update 阶段,effect 对象的 create 函数和 destroy 函数肯定是存在的(现在我们假设用户一定定义 destroy 函数 )。

react 会在 commit 阶段去调用我们的 create 函数和 destroy 函数。commit 阶段又可以分为三个子阶段:

  • beforeMutation
  • mutation
  • layout

回到源码的视角,create 函数和 destroy 函数调用具体是发生在 commit 阶段的入口函数 commitRootImpl 内部,而真正的调用入口函数为 flushPassiveEffects

调用入口

该函数在 commitRootImpl 出现了三次。也就是说 create 函数和 destroy 函数调用存在三个入口:

  1. 处理由于调用 flushSyncUpdateQueue() 所衍生的 effect
  2. beforeMutation 子阶段之前的对执行 effect 进行调度
  3. layout 子阶段完成之后的同步调用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
function commitRootImpl(
root: FiberRoot,
recoverableErrors: null | Array<CapturedValue<mixed>>,
transitions: Array<Transition> | null,
didIncludeRenderPhaseUpdate: boolean,
renderPriorityLevel: EventPriority,
spawnedLane: Lane,
) {
do {
flushPassiveEffects();
} while (rootWithPendingPassiveEffects !== null);

// ...

if (
(finishedWork.subtreeFlags & PassiveMask) !== NoFlags ||
(finishedWork.flags & PassiveMask) !== NoFlags
) {
if (!rootDoesHavePassiveEffects) {
rootDoesHavePassiveEffects = true;
pendingPassiveEffectsRemainingLanes = remainingLanes;
// workInProgressTransitions might be overwritten, so we want
// to store it in pendingPassiveTransitions until they get processed
// We need to pass this through as an argument to commitRoot
// because workInProgressTransitions might have changed between
// the previous render and commit if we throttle the commit
// with setTimeout
pendingPassiveTransitions = transitions;
scheduleCallback(NormalSchedulerPriority, () => {
// ! 1. 异步执行 passive effects
flushPassiveEffects();
// This render triggered passive effects: release the root cache pool
// *after* passive effects fire to avoid freeing a cache pool that may
// be referenced by a node in the tree (HostRoot, Cache boundary etc)
return null;
});
}
}

const subtreeHasEffects =
(finishedWork.subtreeFlags &
(BeforeMutationMask | MutationMask | LayoutMask | PassiveMask)) !==
NoFlags;
const rootHasEffect =
(finishedWork.flags &
(BeforeMutationMask | MutationMask | LayoutMask | PassiveMask)) !==
NoFlags;

if (subtreeHasEffects || rootHasEffect) {

// ! 2. 进入commit阶段
executionContext |= CommitContext;

const shouldFireAfterActiveInstanceBlur = commitBeforeMutationEffects(
root,
finishedWork,
);

// ...

// ! 3. mutation阶段 (包括DOM变更)
commitMutationEffects(root, finishedWork, lanes);

// ...

// ! 4. layout阶段
commitLayoutEffects(finishedWork, root, lanes);
}
// ...
if (includesSyncLane(pendingPassiveEffectsLanes) && root.tag !== LegacyRoot) {
flushPassiveEffects();
}

// ...
}

通过简单的 react 应用来测试得到的结果是:1)在组件的 mount 阶段,react 会走第二个入口;2)在组件的 update 阶段,react 会走第三个入口

不管是从哪个入口进入,create 函数和 destroy 函数调用都是发生了 DOM 更新之后

不管是从哪个入口进入,它们都是走同一个函数 flushPassiveEffects

1
2
3
4
5
6
7
8
9
10
11
function flushPassiveEffectsImpl() {
// ...

const prevExecutionContext = executionContext;
executionContext |= CommitContext;

commitPassiveUnmountEffects(root.current); // cleanup
commitPassiveMountEffects(root, root.current, lanes, transitions); //setup

// ...
}

且有相同的调用栈:

alt text

因为调用 destroy 和 调用 create 函数是分开的。所以,我们需要将两者分开来讨论。但是由于两者的调用逻辑几乎是一样的。所以,在这里,我们以调用 destroy 函数为例即可,create 函数的调用原理跟这个是一样的。
通过源码的阅读和调试,我将「调用 destroy 函数的过程」划分为两个步骤:

  1. 遍历 fiber 树 - 深度优先遍历 fiber 树,找到身上有 effectfiber 节点
  2. 遍历 effect 链表 - 遍历当前的 effect 链表,根据当前 effect 是否满足特定的条件(是否包含特定标签)来确定是否要调用 destroy 函数。

1. 遍历 fiber 树

我们也知道,所有的 useEffect hook 函数只能用 function component 里面使用。

fiber 上可能存在多个 function component 类型的 fiber 节点使用了 hook 函数。所以,这一步中,遍历 fiber 树的目的就是要找到消费了 hook 函数的 fiber 节点。

在这一步之前,react 其实是有做一些前置工作的。那就是:因为遍历 fiber 都是深度优先,在 render 阶段,performUnitOfWork 中的 beginWork 执行到叶子节点后会进入到 completeUnitOfWork 中,会执行 bubbleProperties,也就是 react 会逐步向上追溯当前消费了 hook 函数的 fiber 节点的所有的祖先 fiber 节点,一一给它们的 subtreeFlags 属性值加入一个 Passive 标签。这种机制类似于浏览器的事件冒泡。浏览器的事件起源于特定的 DOM 节点,但是它会在冒泡阶段向上传播到 document 这个根节点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
// packages\react-reconciler\src\ReactFiberCompleteWork.js
function completeWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
const newProps = workInProgress.pendingProps;
popTreeContext(workInProgress);
switch (workInProgress.tag) {
case IndeterminateComponent:
case LazyComponent:
case SimpleMemoComponent:
case FunctionComponent:
case ForwardRef:
case Fragment:
case Mode:
case Profiler:
case ContextConsumer:
case MemoComponent:
bubbleProperties(workInProgress);
return null;
case ClassComponent: {
const Component = workInProgress.type;
if (isLegacyContextProvider(Component)) {
popLegacyContext(workInProgress);
}
bubbleProperties(workInProgress);
return null;
}
// ...
}
}

function bubbleProperties(completedWork: Fiber) {
const didBailout =
completedWork.alternate !== null &&
completedWork.alternate.child === completedWork.child;

let newChildLanes = NoLanes;
let subtreeFlags = NoFlags;

if (!didBailout) {
// ? sy
// Bubble up the earliest expiration time.
// “向上冒泡”最早的过期时间
if (enableProfilerTimer && (completedWork.mode & ProfileMode) !== NoMode) {
// In profiling mode, resetChildExpirationTime is also used to reset
// profiler durations.
let actualDuration = completedWork.actualDuration;
let treeBaseDuration = ((completedWork.selfBaseDuration: any): number);
// ! 遍历completedWork的所有子节点
let child = completedWork.child;
while (child !== null) {
// ! 1. 将他们的lanes和childLanes合并到newChildLanes中
newChildLanes = mergeLanes(
newChildLanes,
mergeLanes(child.lanes, child.childLanes),
);
// ! 2. 将他们的subtreeFlags和flags合并到subtreeFlags中
subtreeFlags |= child.subtreeFlags;
subtreeFlags |= child.flags;

actualDuration += child.actualDuration;

// $FlowFixMe[unsafe-addition] addition with possible null/undefined value
treeBaseDuration += child.treeBaseDuration;
child = child.sibling;
}

completedWork.actualDuration = actualDuration;
completedWork.treeBaseDuration = treeBaseDuration;
} else {
let child = completedWork.child;
while (child !== null) {
newChildLanes = mergeLanes(
newChildLanes,
mergeLanes(child.lanes, child.childLanes),
);

subtreeFlags |= child.subtreeFlags;
subtreeFlags |= child.flags;

// Update the return pointer so the tree is consistent. This is a code
// smell because it assumes the commit phase is never concurrent with
// the render phase. Will address during refactor to alternate model.
child.return = completedWork;

child = child.sibling;
}
}
// ! 子树的flags
completedWork.subtreeFlags |= subtreeFlags;
} else {
// Bubble up the earliest expiration time.
if (enableProfilerTimer && (completedWork.mode & ProfileMode) !== NoMode) {
// In profiling mode, resetChildExpirationTime is also used to reset
// profiler durations.
let treeBaseDuration = ((completedWork.selfBaseDuration: any): number);

let child = completedWork.child;
while (child !== null) {
newChildLanes = mergeLanes(
newChildLanes,
mergeLanes(child.lanes, child.childLanes),
);

// "Static" flags share the lifetime of the fiber/hook they belong to,
// so we should bubble those up even during a bailout. All the other
// flags have a lifetime only of a single render + commit, so we should
// ignore them.
subtreeFlags |= child.subtreeFlags & StaticMask;
subtreeFlags |= child.flags & StaticMask;

// $FlowFixMe[unsafe-addition] addition with possible null/undefined value
treeBaseDuration += child.treeBaseDuration;
child = child.sibling;
}

completedWork.treeBaseDuration = treeBaseDuration;
} else {
let child = completedWork.child;
// ! 遍历completedWork的所有子节点
while (child !== null) {
// ! 1. 将他们的lanes和childLanes合并到newChildLanes中
newChildLanes = mergeLanes(
newChildLanes,
mergeLanes(child.lanes, child.childLanes),
);

// "Static" flags share the lifetime of the fiber/hook they belong to,
// so we should bubble those up even during a bailout. All the other
// flags have a lifetime only of a single render + commit, so we should
// ignore them.
// “静态”标志(Static flags)与它们所属的 Fiber 或 Hook 共享生命周期,因此即使在放弃(bailout)时,也应该将这些flags向上冒泡。
// 而其他所有flags仅在单次render + commit 的生命周期内存在,因此我们应该忽略它们。
// ! 2. 将他们的(subtreeFlags&StaticMask)和flags合并到subtreeFlags中
subtreeFlags |= child.subtreeFlags & StaticMask;
subtreeFlags |= child.flags & StaticMask;

// Update the return pointer so the tree is consistent. This is a code
// smell because it assumes the commit phase is never concurrent with
// the render phase. Will address during refactor to alternate model.
// 更新return pointer以保持树的一致性。这被描述为一种代码异味(code smell),因为它假设commit阶段永远不会与 render 阶段并发。
// 在重构为交替模型(alternate model)时将会解决这个问题。
child.return = completedWork;

child = child.sibling;
}
}

completedWork.subtreeFlags |= subtreeFlags;
}
// ! 后代节点的lanes
completedWork.childLanes = newChildLanes;

return didBailout;
}

回到 useEffect 这个主题。上面所提到的前置工作中,有一点特别需要注意的点是:如果「消费了 hook 函数的那个 fiber 节点」的子树之下没有其他消费了 hook 函数的 fiber 节点,它自己的 subtreeFlags 属性值是不会被贴上一个 Passive 标签的。这也不难理解,因为这恰恰是符合 subtreeFlags 属性名的语义的。

假设只有我们的 <Counter /> 组件里面消费到 useEffect 这个 hook 函数。那么,在 render 阶段,经过上面所提到的前置工作后,我们会得到这样的一颗 fiber 树:

alt text

好了,做完上面的前置工作后,react 会在 commit 阶段发起对 flushPassiveEffects 的调度,将任务加入 taskQueue 中,在适当的时间切片内会执行到此调度。

这个遍历工作是从 fiber 树的根节点 - hostRootFiber 开始的。

源码中的 commitPassiveUnmountOnFiber()recursivelyTraversePassiveUnmountEffects()commitHookPassiveUnmountEffects() 这三个函数共同完成了这个过程。而这三个函数是有分工的:

  • commitPassiveUnmountOnFiber() - 是遍历流程的入口
  • recursivelyTraversePassiveUnmountEffects() - 负责实现了以深度优先的算法的「递」与「归」
  • commitHookPassiveUnmountEffects() - 在「归」之前,尝试去调用当前 fiber 节点 effect 链表上的所有的 destroy 函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function my_commitPassiveUnmountOnFiber(parentFiber){
if (parentFiber.subtreeFlags & PassiveMask) {
const child = parentFiber.child;

while (child !== null) {
my_commitPassiveUnmountOnFiber(child);
child = child.sibling;
}
}

const isfunctionComponentKind = [FunctionComponent, ForwardRef, SimpleMemoComponent].includes(parentFiber.tag);
const hasPassiveEffect = parentFiber.flags & Passive

if(isfunctionComponentKind && hasPassiveEffect){
commitHookPassiveUnmountEffects(
parentFiber,
parentFiber.return,
Passive | HasEffect
);
}
}

其实看完 react 源码,熟悉 react 内部的原理后,这种深优先遍历算法跟 render 阶段的 「work-loop」的遍历算法是一模一样的。唯一的一个区别点有两点:

  1. 它深度优先遍历不一定会遍历到当前子树路径的叶子节点。它会因为不满足「以当前 parentFiber 为根节点的子树上是否有身上贴有 Passive 标签的 fiber 节点?」这个条件提前终止了;
  2. render 阶段的 「work-loop」的遍历算法中,归去之前必定会执行 complete work 不同,当前的遍历算法会先检查是否满足条件,如果不满足,则不执行 “complete work” 。显然,这里的 “complete work” 是指 commitHookPassiveUnmountEffects() 的调用。

2. 遍历 effect 链表

上面,我们已经介绍了 react 是如何了遍历整棵 fiber 树,找到那些所有需要执行 effectfiber 节点。上一步骤末尾,我们也指出了,执行 effectfiber 节点的函数是 commitHookPassiveUnmountEffects()。而 commitHookPassiveUnmountEffects() 会直接调用 commitHookEffectListUnmount()。顾名思义,该函数就是 react 遍历 effect 链表去调用 destroy 函数的所在。

effect 链表并不是只存储 useEffect hook 函数的 effect 对象

那这里所提到的 「effect 类型的」hook 函数有哪些呢?从 react@18.2.0 的源码来看,它包括了5 种类型的 hook 函数:

  • useEffect()
  • useLayoutEffect()
  • useInsertionEffect()
  • useSyncExternalStore()
  • useImperativeHandle()

提出这个结论的依据是: 这些 hook 函数的调用栈最终都是指向了 pushEffect() 函数。如下图:

alt text

pushEffect() 函数就是负责创建新的 effect 对象,把它追加到当前 fiber 节点 effect 链表的尾部。可见,如果一个 function component 都消费了上面这些 hook 函数的话,那么它所对应的 fiber 节点的 effect 链表上并不是单纯包含 useEffect hook 函数所产生的 effect
回到正题。上面我们指出了 commitHookEffectListUnmount 函数就是 react 遍历 effect 链表去调用 destroy 函数的所在,下面来看看它的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
function commitHookEffectListUnmount(
flags,
finishedWork,
nearestMountedAncestor
) {
const updateQueue = finishedWork.updateQueue;
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) {
safelyCallDestroy(finishedWork, nearestMountedAncestor, destroy);
}
}

effect = effect.next;
} while (effect !== firstEffect);
}
}

function safelyCallDestroy(current, nearestMountedAncestor, destroy) {
try {
destroy();
} catch (error) {
captureCommitPhaseError(current, nearestMountedAncestor, error);
}
}

有了上面的铺垫,我们很快就能看明白这段代码的意思。react 通过 do{...}while() 循环来遍历 effect 链表。我们在上面也提到过,effect 链表其实是单向的循环链表。所以,当前即将需要遍历的 effect 对象又指会了第一个 effect 的时候,意味着我们已经遍历完了整条链表,可以退出循环了。

小结

alt text

执行顺序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import {useEffect} from "react";

function Child(){
useEffect(function childEffect(){
console.log('Child effect been called');
});

return null
}

function Sibling(){
useEffect(function siblingEffect(){
console.log('Sibling effect been called');
});

return null;
}

function App(){
useEffect(function appEffect(){
console.log('App effect been called');
});

return (
<div>
<Child />
<Sibling />
</div>
)
}

相信看完上面的分析,也不难得出答案:

1
2
3
Child effect been called
Sibling effect been called
App effect been called

react 采用的递归遍历算法是深度优先算法。示例 react 应用所对应的 fiber 树如下:

alt text

用文字描述整个递归遍历过程是这样的:
<App> 的递;
<div> 的递;
<Child> 的递;
<Child> 的归;
<Sibling> 的递;
的归; ``<App> 的归。

在归时候,react 会在当前 fiber 节点上调用 commitHookEffectListMount() 函数,所以,调用结果是:

<Child> 身上调用 commitHookEffectListMount()
<Sibling> 身上调用 commitHookEffectListMount()
<App> 身上调用 commitHookEffectListMount()

useLayoutEffect

useLayoutEffect 函数跟 useEffect hook 函数 的不同。两者的不同之处在于两点:

  1. 它们给「自己所创建的 effect 对象」所贴的 effect flag 是不同的;
  2. 它们给「自己所关联的 fiber 节点」所追加的 fiber flag 的不同的。

所贴的 hook flag 不同

  • 对于 useLayoutEffect() hook 函数而言,它给自己所创建的 effect 对象所贴的 effect flagLayout
  • useEffect() hook 函数给自己所创建的 effect 对象所贴的 effect flagPassive

所追加的 fiber flag 不同

对于 useEffect() hook 函数而言,它给自己所关联的 fiber 节点所追加的 fiber flag 是:

  • mount 阶段是 Passive | PassiveStatic
  • update 阶段是 Passive

useLayoutEffect() hook 函数给自己所关联的 fiber 节点所追加的 fiber flag 是:

  • mount 阶段是 Update | LayoutStatic
  • update 阶段是 Update

useLayoutEffect - mutation 子阶段调用 destroy 函数

它的 destroy 函数的调用是发生在 mutation 子阶段。具体来讲,就是发生在 commitMutationEffectsOnFiber() 这个函数里面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
function commitMutationEffectsOnFiber(finishedWork, root, lanes) {
const current = finishedWork.alternate;
const flags = finishedWork.flags;

switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case MemoComponent:
case SimpleMemoComponent: {
recursivelyTraverseMutationEffects(root, finishedWork);
commitReconciliationEffects(finishedWork);

if (flags & Update) {
// ......
{
try {
commitHookEffectListUnmount(
Layout | HasEffect,
finishedWork,
finishedWork.return
);
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
}
}

return;
}
// ......
}
}

useLayoutEffect - layout 子阶段调用 create 函数

  • hook 类型 - 这里就是指 useLayoutEffect() hook 的 create 函数;
  • class 类型 - 这里是指 class component 的两个生命周期函数:
    • componentDidMount()
    • componentDidUpdate()

useLayoutEffect() hookcreate 函数的真正调用入口为 commitHookLayoutEffects()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
function commitLayoutEffectOnFiber(
finishedRoot,
current,
finishedWork,
committedLanes
) {
const flags = finishedWork.flags;

switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case SimpleMemoComponent: {
recursivelyTraverseLayoutEffects(
finishedRoot,
finishedWork,
committedLanes
);

if (flags & Update) {
commitHookLayoutEffects(finishedWork, Layout | HasEffect);
}

break;
}

// ......
}
}

应用

关于这个问题,react 官方文档给出了一个使用用例: 在浏览器重绘之前测量 DOM 的布局信息

「界面更新时候的闪烁问题」。为什么使用 useLayoutEffect() 能解决这类的问题呢?

首先,我们要知道为什么会造成闪烁。造成闪烁的原因是 react 在短时间更新了多帧显示内容不一样的界面。而 useLayoutEffect() hookcreate 函数是在 layout 子阶段以同步的,批量的方式去执行的。也就是说,create 函数里面的所发起的多次状态更新请求只会产生一次的实质性的界面重绘。 通过抹除代表着中间状态的过渡帧,将多帧压缩为一帧来更新界面,这就是 useLayoutEffect() 能解决「界面更新时候的闪烁问题」原因之所在。

上面所提到的 「界面更新时候的闪烁问题」只是 useLayoutEffect() 能解决问题中的一个垂类。useLayoutEffect() 应该还能解决更多不同业务场景下的问题,我们得通过现象看本质。

useLayoutEffect() 的本质能力是什么?

  • 访问更新后的 DOM 树 - 因为 useLayoutEffect() hookcreate 函数是在 mutation 子阶段之后的 layout 子阶段执行的。所以,这就意味着我们可以去访问更新后的 DOM 树和做一些布局信息的测量。
  • 同步/批量执行状态更新,阻塞浏览器重绘 - react 将会以「同步阻塞,批量更新」的方式去对待 create 函数体里面所发起的过个状态更新请求。也就说,在 create 函数体里面所发起的过个状态更新请求只会产生一次界面重绘。

总结

从源码的角度来看, 这两者其实都是在同一个架构里面,拥有很多相同点:

  • 每一个 effect 类型 hook 都会关联一个 hook 对象 和 effect 对象;
  • 同一个 function component 内,所有的 hook 对象都共用同一条 hook 链表;
  • 同一个 function component 内,所有的 effect 对象都共用同一条 effect 链表;
  • 同一个调用机制 - 在 render 阶段创建 hook 对象和 effect 对象,在 commit 阶段去调用 hookcreatedestroy 函数。

这两者的不同点在于:

  • useEffect()destroycreate 函数是在同一个子阶段(layout 子阶段后的 passive 子阶段)调用的(调用入口要么是调度后的 flushPassiveEffectImpl(),要么是同步的flushPassiveEffectImpl());
  • useLayoutEffect() 的这两个函数却是在不同的子阶段执行。destroy 是在 mutation 子阶段执行的,而 create 是在 layout 子阶段。

beginWork

单节点diff - reconcileSingleElement

单节点diff的为什么做成一个循环:

  • 因为新的内容只有一个节点,不代表旧的内容只有一个节点,这里可以分为两种情况:
    • 新旧都只有一个节点,只执行一次循环的比较
    • 旧的有多个节点,新的只有一个节点,就会循环旧的节点,依次和这个新节点进行key的比较,如果key相等,再比较type类型,比如是否都是div类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
function reconcileSingleElement(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
element: ReactElement,
lanes: Lanes,
debugInfo: ReactDebugInfo | null,
): Fiber {
// 取出最新的react元素对象的key
const key = element.key;
let child = currentFirstChild; // 旧的节点

// 检查老的fiber单链表中是否有可以复用的节点
while (child !== null) {
// 1.key相等的情况下
if (child.key === key) {
// 取出最新的节点type:比如组件的type或者DOM节点的type
const elementType = element.type;
// 2.组件type为Fragment
if (elementType === REACT_FRAGMENT_TYPE) {
// 新老都是Fragment
if (child.tag === Fragment) {
// 在相等的情况下,给剩下的旧节点打上删除标记
deleteRemainingChildren(returnFiber, child.sibling);
// 复用当前旧的节点,生成新的fiber节点
const existing = useFiber(child, element.props.children);
// 设置父级节点
existing.return = returnFiber;
return existing;
}
} else {
// 3,组件type也相等的情况下
if (
child.elementType === elementType ||
// Keep this check inline so it only runs on the false path:
(__DEV__
? isCompatibleFamilyForHotReloading(child, element)
: false) ||
// Lazy types should reconcile their resolved type.
// We need to do this after the Hot Reloading check above,
// because hot reloading has different semantics than prod because
// it doesn't resuspend. So we can't let the call below suspend.
(typeof elementType === 'object' &&
elementType !== null &&
elementType.$$typeof === REACT_LAZY_TYPE &&
resolveLazy(elementType) === child.type)
) {
// 在相等的情况下,给剩下的旧节点打上删除标记
deleteRemainingChildren(returnFiber, child.sibling);
// 复用当前旧的节点,生成新的fiber节点(生成的existing的pendingProps就是新的了)
const existing = useFiber(child, element.props);
// 设置初始的ref
coerceRef(returnFiber, child, existing, element);
// 设置父级节点
existing.return = returnFiber;
return existing;
}
}
// key相等但是type不等的情况下,给所有旧节点打上删除标记【比如组件由div变成span了】
deleteRemainingChildren(returnFiber, child);
break;
} else {
// 4,key不相等的情况下,给当前旧的节点打上删除标记
deleteChild(returnFiber, child);
}
// 取出旧的节点的兄弟节点,继续与新的节点进行比较【旧节点可能存在多个】
child = child.sibling;
}

// 1. 初次挂载 2. 没有找到可以服用的老节点
if (element.type === REACT_FRAGMENT_TYPE) {
const created = createFiberFromFragment(
element.props.children,
returnFiber.mode,
lanes,
element.key,
);
created.return = returnFiber;
return created;
} else {
const created = createFiberFromElement(element, returnFiber.mode, lanes);
coerceRef(returnFiber, currentFirstChild, created, element);
created.return = returnFiber;
return created;
}
}

多节点diff - reconcileChildrenArray

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
function reconcileChildrenArray(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChildren: Array<any>,
lanes: Lanes,
debugInfo: ReactDebugInfo | null,
): Fiber | null {
let resultingFirstChild: Fiber | null = null; // 存储新生成的child
let previousNewFiber: Fiber | null = null;

let oldFiber = currentFirstChild;
let lastPlacedIndex = 0;
let newIdx = 0;
let nextOldFiber = null;
// ! 1. 从左边往右遍历,比较新老节点,如果节点可以复用,继续往右,否则就停止
for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
if (oldFiber.index > newIdx) {
nextOldFiber = oldFiber;
oldFiber = null;
} else {
nextOldFiber = oldFiber.sibling;
}
const newFiber = updateSlot(
returnFiber,
oldFiber,
newChildren[newIdx],
lanes,
debugInfo,
);
if (newFiber === null) {
// TODO: This breaks on empty slots like null children. That's
// unfortunate because it triggers the slow path all the time. We need
// a better way to communicate whether this was a miss or null,
// boolean, undefined, etc.
if (oldFiber === null) {
oldFiber = nextOldFiber;
}
break;
}
if (shouldTrackSideEffects) {
if (oldFiber && newFiber.alternate === null) {
// We matched the slot, but we didn't reuse the existing fiber, so we
// need to delete the existing child.
deleteChild(returnFiber, oldFiber);
}
}
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
// TODO: Move out of the loop. This only happens for the first run.
resultingFirstChild = newFiber;
} else {
// TODO: Defer siblings if we're not at the right index for this slot.
// I.e. if we had null values before, then we want to defer this
// for each null value. However, we also don't want to call updateSlot
// with the previous one.
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
oldFiber = nextOldFiber;
}

// !2.1 新节点没了,(老节点还有)。则删除剩余的老节点即可
// 0 1 2 3 4
// 0 1 2 3
if (newIdx === newChildren.length) {
// We've reached the end of the new children. We can delete the rest.
deleteRemainingChildren(returnFiber, oldFiber);
if (getIsHydrating()) {
const numberOfForks = newIdx;
pushTreeFork(returnFiber, numberOfForks);
}
return resultingFirstChild;
}
// ! 2.2 (新节点还有),老节点没了
// 0 1 2 3 4
// 0 1 2 3 4 5
if (oldFiber === null) {
// If we don't have any more existing children we can choose a fast path
// since the rest will all be insertions.
for (; newIdx < newChildren.length; newIdx++) {
const newFiber = createChild(
returnFiber,
newChildren[newIdx],
lanes,
debugInfo,
);
if (newFiber === null) {
continue;
}
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
// TODO: Move out of the loop. This only happens for the first run.
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
}
if (getIsHydrating()) {
const numberOfForks = newIdx;
pushTreeFork(returnFiber, numberOfForks);
}
return resultingFirstChild;
}


// !2.3 新老节点都还有节点,但是因为老fiber是链表,不方便快速get与delete,
// ! 因此把老fiber链表中的节点放入Map中,后续操作这个Map的get与delete
// 0 1| 4 5
// 0 1| 7 8 2 3
// Add all children to a key map for quick lookups.
const existingChildren = mapRemainingChildren(returnFiber, oldFiber);

// Keep scanning and use the map to restore deleted items as moves.
for (; newIdx < newChildren.length; newIdx++) {
const newFiber = updateFromMap(
existingChildren,
returnFiber,
newIdx,
newChildren[newIdx],
lanes,
debugInfo,
);
if (newFiber !== null) {
if (shouldTrackSideEffects) {
if (newFiber.alternate !== null) {
// The new fiber is a work in progress, but if there exists a
// current, that means that we reused the fiber. We need to delete
// it from the child list so that we don't add it to the deletion
// list.
existingChildren.delete(
newFiber.key === null ? newIdx : newFiber.key,
);
}
}
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
}
}

// !3. 如果是组件更新阶段,此时新节点已经遍历完了,能复用的老节点都用完了,
// ! 则最后查找Map里是否还有元素,如果有,则证明是新节点里不能复用的,也就是要被删除的元素,此时删除这些元素就可以了
if (shouldTrackSideEffects) {
// Any existing children that weren't consumed above were deleted. We need
// to add them to the deletion list.
existingChildren.forEach(child => deleteChild(returnFiber, child));
}

if (getIsHydrating()) {
const numberOfForks = newIdx;
pushTreeFork(returnFiber, numberOfForks);
}
return resultingFirstChild;
}

工具函数

updateSlot

updateSlot 函数的主要作用是:调用 updateTextNode 或者 updateElement 函数,如果 Fiber 能够复用就复用,不能够复用就创建一个新的 Fiber

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function updateSlot(returnFiber, oldFiber, newChild) {
const key = oldFiber !== null ? oldFiber.key : null;
if ((typeof newChild === "string" && newChild !== "") || typeof newChild === "number") {
if (key !== null) return null;
return updateTextNode(returnFiber, oldFiber, "" + newChild);
}

if (newChild !== null && typeof newChild === "object") {
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE: {
if (newChild.key === key) {
return updateElement(returnFiber, oldFiber, newChild);
}
}
// ...
default:
return null;
}
}
return null;
}

placeChild

placeChild 函数主要作用是:判断当前的 newFiber 是否是复用的节点,如果是复用的节点,就返回这个节点的 index 作为 lastPlacedIndex

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 每个 fiber 都有一个 index 属性,表示当前 fiber 在父节点中的位置
// 如果是个复用的 fiber,current 存在,且 current.index > lastPlacedIndex,否则表示这个节点需要新创建,也可能是移动(移动也是创建)
function placeChild(newFiber, lastPlacedIndex, newIdx) {
newFiber.index = newIdx;
if (!shouldTrackSideEffects) {
return lastPlacedIndex;
}
const current = newFiber.alternate;
if (current !== null) {
const oldIndex = current.index;
if (oldIndex < lastPlacedIndex) {
newFiber.flags |= Placement;
return lastPlacedIndex;
} else {
// 复用节点,返回复用节点的 index
return oldIndex;
}
} else {
newFiber.flags |= Placement;
return lastPlacedIndex;
}
}

updateFromMap

updateFromMap 函数的主要作用是:和 updateSlot 函数功能差不多,从 existingChildren 中找到相应的 Fiber,然后调用 updateTextNode 或者 updateElement 函数,如果 Fiber 能够复用就复用,不能够复用就创建一个新的 Fiber

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function updateFromMap(existingChildren, returnFiber, newIdx, newChild) {
// 文本 Fiber
if ((typeof newChild === "string" && newChild !== "") || typeof newChild === "number") {
const matchedFiber = existingChildren.get(newIdx) || null;
return updateTextNode(returnFiber, matchedFiber, `${newChild}`);
}
// div/span Fiber
if (typeof newChild === "object" && newChild !== null) {
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE: {
const matchedFiber = existingChildren.get(newChild.key === null ? newIdx : newChild.key) || null;
return updateElement(returnFiber, matchedFiber, newChild);
}
// ...
}
}
return null;
}

updateTextNode

updateTextNode 函数的主要作用是:如果 current 存在,就复用 Fiber,否则就创建一个新的 Fiber

1
2
3
4
5
6
7
8
9
10
11
function updateTextNode(returnFiber, current, textContent) {
if (current === null || current.tag !== HostText) {
const created = createFiberFromText(textContent);
created.return = returnFiber;
return created;
} else {
const existing = useFiber(current, textContent);
existing.return = returnFiber;
return existing;
}
}

updateElement

updateElement 函数的主要作用是:如果 current 存在,且 current.type === element.type,就复用 Fiber,否则就创建一个新的 Fiber

1
2
3
4
5
6
7
8
9
10
11
12
13
function updateElement(returnFiber, current, element) {
const elementType = element.type;
if (current !== null) {
if (current.type === elementType) {
const existing = useFiber(current, element.props);
existing.return = returnFiber;
return existing;
}
}
const created = createFiberFromElement(element);
created.return = returnFiber;
return created;
}

总结

  1. 主要功能是对不同 fiber.tag 进行不同的处理。HostRoot容器节点document.getElementById(‘root’),HostComponent常规节点div/span,HostText文本节点
  2. updateHostComponent
  3. reconcileChildren 函数负责协调子节点,接收三个参数:current:构建完成的 fiber 树;workInProgress:构建中的 fiber 树;nextChildren:要处理的子节点。
    1. 标注子节点是否要移动,如果没有old子节点。直接标注 Placement(新增或者移动位置),如果有old子节点,如果oldIndex < lastPlacedIndex(上一次新增或者移动的点的index),证明可以移动,否则就保持原位置
    2. 01234 -> 2134 移动的是1
    3. 2134 -> 01234 移动的是2
    4. 1 3 2 5 -> 0 1 2 3 4 移动的是3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function placeChild(
newFiber: Fiber,
lastPlacedIndex: number,
newIndex: number,
): number {
newFiber.index = newIndex;
const current = newFiber.alternate;
if (current !== null) {
const oldIndex = current.index;
if (oldIndex < lastPlacedIndex) {
// This is a move.
newFiber.flags |= Placement | PlacementDEV;
return lastPlacedIndex;
} else {
// This item can stay in place.
return oldIndex;
}
} else {
// This is an insertion.
newFiber.flags |= Placement | PlacementDEV;
return lastPlacedIndex;
}
}

completeWork-总结

  1. 没有老节点
    1. 创建真实的 DOM 节点
    2. 将当前子节点下子节点挂载到当前节点上
  2. 有老节点
    1. updateHostComponent,对比workInProgress.pendingProps 和 current.memoizedProps,如果不同,就markUpdate标记更新
  3. bubbleProperties - 收集当前节点下子节点的 flags 和 subtreeFlags

commitWork

commitWork 在处理单节点时落脚点也是在 HostComponent 中,主要分为三种处理方式:

  • DOM 节点是新增,也就是 Placement,协调是标记哪些节点要移动(包括新增的节点和复用需要移动的节点)下面代码分析了一下要插入到哪个节点前面
  • DOM 节点中的文本内容变成空,也就是 ContentReset, ContentReset会调用 resetTextContnt 将文本节点设置为空
  • DOM 节点是更新,也就是 Update,会调用 commitUpdate 更新 DOM 节点
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
if (flags & Placement) {
try {
commitPlacement(finishedWork);
}
finishedWork.flags &= ~Placement;
}

function commitPlacement(finishedWork: Fiber): void {
if (!supportsMutation) {
return;
}

const parentFiber = getHostParentFiber(finishedWork);

switch (parentFiber.tag) {
case HostSingleton: {
if (supportsSingletons) {
const parent: Instance = parentFiber.stateNode;
const before = getHostSibling(finishedWork);
// We only have the top Fiber that was inserted but we need to recurse down its
// children to find all the terminal nodes.
insertOrAppendPlacementNode(finishedWork, before, parent);
break;
}
// Fall through
}
case HostComponent: {
const parent: Instance = parentFiber.stateNode;
if (parentFiber.flags & ContentReset) {

console.log('%c [ ]-1832', 'font-size:13px; background:pink; color:#bf2c9f;', )
// Reset the text content of the parent before doing any insertions
resetTextContent(parent);
// Clear ContentReset from the effect tag
parentFiber.flags &= ~ContentReset;
}

const before = getHostSibling(finishedWork);
// We only have the top Fiber that was inserted but we need to recurse down its
// children to find all the terminal nodes.
insertOrAppendPlacementNode(finishedWork, before, parent);
break;
}
case HostRoot:
case HostPortal: {
const parent: Container = parentFiber.stateNode.containerInfo;
const before = getHostSibling(finishedWork);
insertOrAppendPlacementNodeIntoContainer(finishedWork, before, parent);
break;
}
default:
throw new Error(
'Invalid host parent fiber. This error is likely caused by a bug ' +
'in React. Please file an issue.',
);
}
}

function getHostSibling(fiber: Fiber): ?Instance {
// We're going to search forward into the tree until we find a sibling host
// node. Unfortunately, if multiple insertions are done in a row we have to
// search past them. This leads to exponential search for the next sibling.
// TODO: Find a more efficient way to do this.
let node: Fiber = fiber;
siblings: while (true) {
// If we didn't find anything, let's try the next sibling.
while (node.sibling === null) {
if (node.return === null || isHostParent(node.return)) {
// If we pop out of the root or hit the parent the fiber we are the
// last sibling.
return null;
}
node = node.return;
}
node.sibling.return = node.return;
node = node.sibling; // 找到下一个兄弟节点
while (
node.tag !== HostComponent &&
node.tag !== HostText &&
(!supportsSingletons ? true : node.tag !== HostSingleton) &&
node.tag !== DehydratedFragment
) {
// If it is not host node and, we might have a host node inside it.
// Try to search down until we find one.
if (node.flags & Placement) {
// If we don't have a child, try the siblings instead.
continue siblings;
}
// If we don't have a child, try the siblings instead.
// We also skip portals because they are not part of this host tree.
if (node.child === null || node.tag === HostPortal) {
continue siblings;
} else {
node.child.return = node;
node = node.child;
}
}
// Check if this host node is stable or about to be placed.
// 判断节点是稳定的,即不需要移动或是新增的
if (!(node.flags & Placement)) {
// Found it!
// 就找到了这个节点了
return node.stateNode;
}
}
}

useCallbackuseMemo 是一样的东西,只是入参有所不同。

useCallback 缓存的是回调函数,如果依赖项没有更新,就会使用缓存的回调函数

useMemo 缓存的是回调函数的 return,如果依赖项没有更新,就会使用缓存的 return

官网有这样一段描述,换言之 useCallback(fn, dependencies) 相当于 useMemo(() => fn, dependencies)

1
2
3
4
// Simplified implementation (inside React)
function useCallback(fn, dependencies) {
return useMemo(() => fn, dependencies);
}

useCallback

useCallback 允许你在组件渲染之间保存 函数定义。

1
const cachedFn = useCallback(fn, dependencies)

参数

  1. fn :想要缓存的函数。此函数可以接受任何参数并且返回任何值。
    1. React 将会在初次渲染⽽⾮调⽤时返回该函数。
    2. 当进⾏下⼀次渲染时,如果 dependencies 相⽐于上⼀次渲染时没有改变,那么 React 将会返回相同的函数。
    3. 否则,React 将返回在最新⼀次渲染中传⼊的函数,并且将其缓存以便之后使⽤。
    4. React 不会调⽤此函数,⽽是返回此函数。你可以⾃⼰决定何时调⽤以及是否调⽤。
  2. dependencies :有关是否更新 fn 的所有响应式值的⼀个列表。
    1. 响应式值包括 propsstate,和所有在你组件内部直接声明的变量和函数。
    2. 依赖列表必须具有确切数量的项,并且必须像 [dep1, dep2, dep3] 这样编写。
    3. React 使⽤ Object.is ⽐较每⼀个依赖和它的之前的值。

返回值

在初次渲染时, useCallback 返回你已经传⼊的 fn 函数

在之后的渲染中, 如果依赖没有改变, useCallback 返回上⼀次渲染中缓存的 fn 函数;否则返回这⼀次渲染传⼊的 fn

源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function mountCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
hook.memoizedState = [callback, nextDeps];
return callback;
}

function updateCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
const prevState = hook.memoizedState;
if (nextDeps !== null) {
const prevDeps: Array<mixed> | null = prevState[1];
if (areHookInputsEqual(nextDeps, prevDeps)) {
return prevState[0];
}
}
hook.memoizedState = [callback, nextDeps];
return callback;
}

updateCallback 代码意图也很简单,如果依赖数组 deps 没有变化,或者 deps=[] 的情况下,会返回之前缓存的回调函数,否则就更新对应 fiber.memoizedState.hook.memoizedState 并返回新的回调函数。

useMemo

useMemo 是每次重新渲染的时候能够缓存计算结果的Hook。

1
const cachedValue = useMemo(calculateValue, dependencies)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
function mountMemo<T>(
nextCreate: () => T,
deps: Array<mixed> | void | null,
): T {
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
const nextValue = nextCreate();

hook.memoizedState = [nextValue, nextDeps];
return nextValue;
}

function updateMemo<T>(
nextCreate: () => T,
deps: Array<mixed> | void | null,
): T {
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
const prevState = hook.memoizedState;
// Assume these are defined. If they're not, areHookInputsEqual will warn.
if (nextDeps !== null) {
const prevDeps: Array<mixed> | null = prevState[1];
if (areHookInputsEqual(nextDeps, prevDeps)) {
return prevState[0];
}
}
const nextValue = nextCreate();

hook.memoizedState = [nextValue, nextDeps];
return nextValue;
}

使用场景总结

  1. useCallBack 不要每个函数都包一下,否则就会变成反向优化。
    1. useCallBack 本身就是需要一定性能的
    2. 其次 useCallBack 并不能阻止函数重新创建, 由于使用 useCallback 时,函数会作为实参传给 useCallback,所以无论怎样 useCallback 包裹的函数都是会重新创建的,只是当 useCallback 的依赖没有改变时返回的是缓存中的函数而已
  2. useMemo 的关键是权衡。其目的是避免不必要的计算,但也要注意不要滥用,因为维持这些缓存值也是有开销的。最佳的做法是先写出清晰和可读的代码,然后在性能瓶颈出现时,再考虑优化。
    1. 如果函数组件中某个值需要大量的计算才能得出,或者渲染某个组件时有昂贵的计算,都可以考虑使用 useMemo 进行包裹
    2. 当值作为别的 hooks 的依赖时,可以考虑使用
  3. 如果某个函数或者值是传递给子组件的 props,可以考虑使用 useCallback 或者 useMemo 进行包裹(配合 React.memo 使用)

时间切片是 Concurrent Mode 的核心机制之一。它的主要目的是将长任务分解为小的任务单元,每个单元只执行一小段时间,然后让出控制权,避免长时间阻塞主线程。

react 的并发模式在 render 阶段 每处理一个 fiber 就会根据以下两点判断一次是否应该打断并把控制权交换给主线程:

  1. 是否超过分片时间(5ms)
  2. 根据 isInputPending 判断是否有用户输入

但是并不会根据优先级来打断,优先级只会影响任务队列的任务排序,所以按照按优先级排序好的任务顺序来执行,也就能让高优先级任务得到及时处理。

当一个任务开始执行时, Scheduler 会为其分配一个时间片 (通常为 5ms)。在这个时间片内,任务可以连续执行。

  • 如果任务在时间片内完成,则直接结束
  • 如果任务执行时间超过了时间片,则 Scheduler 会中断任务的执行,保存当前的进度,并让出控制权给浏览器,以便响应用户交互或执行其他高优先级的任务
  • 如果任务执行中,遇到用户输入等需要浏览器及时反馈的操作,会通过判断 isInputPending,会中断当前任务,并让出控制权给浏览器,以便响应用户交互或执行其他高优先级的任务
1
2
3
4
5
6
7
function workLoopConcurrent() {
// Perform work until Scheduler asks us to yield
while (workInProgress !== null && !shouldYield()) {
// $FlowFixMe[incompatible-call] found when upgrading Flow
performUnitOfWork(workInProgress);
}
}

通过 shouldYield 判断,具体方法就详细说明了,可以去看《react调度器scheduler源码分析》

scheduleTaskForRootDuringMicrotask

scheduleTaskForRootDuringMicrotask 每次执行都会判断上一次中断任务优先级和root下的最高优先级是否一样。

  • 如果一样,说明没有更高优先级的更新产生,可以继续上次未完成的协调;
  • 如果不一样,说明有更高优先级的更新进来,要清空之前已开始的协调过程,从根节点开始重新协调。等高优先级更新处理完成以后,再次从根节点开始处理低优先级更新。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
// packages/react-reconciler/src/ReactFiberRootScheduler.js
function scheduleTaskForRootDuringMicrotask(
root: FiberRoot,
currentTime: number,
): Lane {
// 这个函数总是在microtask中被调用,或者在渲染任务的最后,在我们将控制权交还给主线程之前被调用。它绝对不应该被同步调用。

// 检查是否有任何lanes被其他work饿死。如果是,将它们标记为过期,这样我们就知道下一个要处理的是它们。
markStarvedLanesAsExpired(root, currentTime);

// Determine the next lanes to work on, and their priority.
const workInProgressRoot = getWorkInProgressRoot();
const workInProgressRootRenderLanes = getWorkInProgressRootRenderLanes();

// 获取当前所有优先级中最高的优先级
const nextLanes = getNextLanes(
root,
root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,
);

const existingCallbackNode = root.callbackNode;
if (
// Check if there's nothing to work on
nextLanes === NoLanes ||
// If this root is currently suspended and waiting for data to resolve, don't
// schedule a task to render it. We'll either wait for a ping, or wait to
// receive an update.
//
// Suspended render phase
(root === workInProgressRoot && isWorkLoopSuspendedOnData()) ||
// Suspended commit phase
root.cancelPendingCommit !== null
) {
// Fast path: There's nothing to work on.
if (existingCallbackNode !== null) {
cancelCallback(existingCallbackNode);
}
root.callbackNode = null;
root.callbackPriority = NoLane;
return NoLane;
}

// Schedule a new callback in the host environment.
if (includesSyncLane(nextLanes)) {
// sy- setState
// sy-no 初次渲染
// 同步工作始终在微任务结束时刷新,因此我们不需要安排额外的任务。
if (existingCallbackNode !== null) {
cancelCallback(existingCallbackNode);
}
root.callbackPriority = SyncLane;
root.callbackNode = null;
return SyncLane;
} else {

// We use the highest priority lane to represent the priority of the callback.
const existingCallbackPriority = root.callbackPriority;
const newCallbackPriority = getHighestPriorityLane(nextLanes);
if (
newCallbackPriority === existingCallbackPriority &&
// Special case related to `act`. If the currently scheduled task is a
// Scheduler task, rather than an `act` task, cancel it and re-schedule
// on the `act` queue.
!(
__DEV__ &&
ReactCurrentActQueue.current !== null &&
existingCallbackNode !== fakeActCallbackNode
)
) {
// The priority hasn't changed. We can reuse the existing task.
return newCallbackPriority;
} else {
// Cancel the existing callback. We'll schedule a new one below.
cancelCallback(existingCallbackNode);
}

let schedulerPriorityLevel;
switch (lanesToEventPriority(nextLanes)) {
case DiscreteEventPriority:
schedulerPriorityLevel = ImmediateSchedulerPriority;
break;
case ContinuousEventPriority:
schedulerPriorityLevel = UserBlockingSchedulerPriority;
break;
case DefaultEventPriority: // 32
// ? sy 页面初次渲染、transition(128)
schedulerPriorityLevel = NormalSchedulerPriority; // 3
break;
case IdleEventPriority:
schedulerPriorityLevel = IdleSchedulerPriority;
break;
default:
schedulerPriorityLevel = NormalSchedulerPriority;
break;
}

// ? sy 页面初次渲染
const newCallbackNode = scheduleCallback(
schedulerPriorityLevel,
performConcurrentWorkOnRoot.bind(null, root),
);

root.callbackPriority = newCallbackPriority;
root.callbackNode = newCallbackNode;
return newCallbackPriority;
}
}

1. 高优先级打断低优先级

cancelCallback

1
2
3
4
5
function unstable_cancelCallback(task: Task) {
// ...

task.callback = null;
}

cancelCallback(existingCallbackNode)cancelCallback 函数就是将 root.callbackNode 赋值为 null

performConcurrentWorkOnRoot

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
export function performConcurrentWorkOnRoot(
root: FiberRoot,
didTimeout: boolean,
): RenderTaskFn | null {
// ...

// Flush any pending passive effects before deciding which lanes to work on,
// in case they schedule additional work.
const originalCallbackNode = root.callbackNode;
// ...

// 在某些情况下,我们会禁用时间片切片:如果work过长时间做计算(为了防止饥饿而将其视为“过期”的work),或者我们处于默认启用同步更新模式。
const shouldTimeSlice =
!includesBlockingLane(root, lanes) &&
!includesExpiredLane(root, lanes) && // 参考这个例子,UseDeferredValuePage
(disableSchedulerTimeoutInWorkLoop || !didTimeout);

// ! 1. render
let exitStatus = shouldTimeSlice
? renderRootConcurrent(root, lanes) // 参考这个例子,UseDeferredValuePage
: renderRootSync(root, lanes); // ? sy, 不用时间切片

if (exitStatus !== RootInProgress) {
let renderWasConcurrent = shouldTimeSlice;
do {
if (exitStatus === RootDidNotComplete) {
// The render unwound without completing the tree. This happens in special
// cases where need to exit the current render without producing a
// consistent tree or committing.
markRootSuspended(root, lanes, NoLane);
} else {
// ! 2. render结束,做一些检查

// ...

// !3. commit
// 我们现在有了一个一致的树。下一步要么是commit,要么是,如果有什么被暂停了,就等待一段时间后再commit。
root.finishedWork = finishedWork;
root.finishedLanes = lanes;
finishConcurrentRender(root, exitStatus, finishedWork, lanes);
}
break;
} while (true);
}
ensureRootIsScheduled(root);
return getContinuationForRoot(root, originalCallbackNode);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
export function getContinuationForRoot(
root: FiberRoot,
originalCallbackNode: mixed,
): RenderTaskFn | null {
// This is called at the end of `performConcurrentWorkOnRoot` to determine
// if we need to schedule a continuation task.
//
// Usually `scheduleTaskForRootDuringMicrotask` only runs inside a microtask;
// however, since most of the logic for determining if we need a continuation
// versus a new task is the same, we cheat a bit and call it here. This is
// only safe to do because we know we're at the end of the browser task.
// So although it's not an actual microtask, it might as well be.
scheduleTaskForRootDuringMicrotask(root, now());
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);
}
return null;
}

performConcurrentWorkOnRoot 函数是每个并发任务的入口,下面简要分析以下它的运行流程:

  1. 会先把 root.callbackNode 缓存起来,存在 originalCallbackNode 变量中
  2. 并发模式下会使用 renderRootConcurrent 执行
    1. render 阶段中也有可能在代码执行中,触发更高优先级事件,例如点击事件,高优先级事件又触发了 setState 就是相对高优先级的
    2. setState 执行 scheduleUpdateOnFiberprepareFreshStackmarkRootUpdated 函数,就已经把 update 添加到 fiber 上并且将更新优先级标记到 rootpendingLanes 上了
  3. 如果render阶段结束,做一些检查,commit阶段,如果render阶段中断,会直接跳过上述操作,走到函数的末尾
  4. 在函数末尾会调用 getContinuationForRoot 函数,先执行 scheduleTaskForRootDuringMicrotask 就是把 render 中目前最高优先级事件和当前任务优先级作对比,进行 cancelCallback 或者 scheduleCallback,同时 root.callbackNode 也可能被改掉了
  5. 再判断 root.callbackNode 和开始缓存起来的值 originalCallbackNode 是否一样,如果不一样,就代表 root.callbackNode 被赋值为 null 或者其他的task 值,代表有更高优先级任务进来。
  6. 此时 performConcurrentWorkOnRoot 返回值为 null

pop(taskQueue)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
const callback = currentTask.callback;
if (typeof callback === 'function') {
// ...
const continuationCallback = callback(didUserCallbackTimeout);
// 返回值continuationCallback是函数,也就是performConcurrentWorkOnRoot执行后的返回值performConcurrentWorkOnRoot.bind(null, root)或null,如果是函数就继续,不是的话,如果判断如果currentTask === peek(taskQueue),currentTask 出队
if (typeof continuationCallback === 'function') {
// If a continuation is returned, immediately yield to the main thread
// regardless of how much time is left in the current time slice.
// $FlowFixMe[incompatible-use] found when upgrading Flow
currentTask.callback = continuationCallback;
if (enableProfiling) {
// $FlowFixMe[incompatible-call] found when upgrading Flow
markTaskYield(currentTask, currentTime);
}
advanceTimers(currentTime);
return true;
} else {
if (enableProfiling) {
// $FlowFixMe[incompatible-call] found when upgrading Flow
markTaskCompleted(currentTask, currentTime);
// $FlowFixMe[incompatible-use] found when upgrading Flow
currentTask.isQueued = false;
}
if (currentTask === peek(taskQueue)) {
pop(taskQueue);
}
advanceTimers(currentTime);
}
} else {
pop(taskQueue);
}
currentTask = peek(taskQueue);

上面是 Scheduler 模块里面 workLoop 函数的代码片段,currentTask.callback 就是 scheduleCallback 的第二个参数,也就是performConcurrentWorkOnRoot 函数。

承接上个主题,如果 performConcurrentWorkOnRoot 函数返回了 nullworkLoop 内部就会执行 pop(taskQueue),将当前的任务从 taskQueue中弹出。

低优先级任务重启

上一步中说道一个低优先级任务从 taskQueue 中被弹出。那高优先级任务执行完毕之后,如何重启回之前的低优先级任务呢?

1
2
3
4
5
6
7
8
let remainingLanes = mergeLanes(finishedWork.lanes, finishedWork.childLanes);
markRootFinished(root, remainingLanes, spawnedLane);

// ...

// Always call this before exiting `commitRoot`, to ensure that any
// additional work on this root is scheduled.
ensureRootIsScheduled(root, now());

markRootFinished 函数刚刚上面说了是释放已完成任务的 Lane,那也就是说未完成任务依然会存在 lanes 中,所以我们可以重新调用 ensureRootIsScheduled 发起一次新的调度,去重启低优先级任务的执行。我们可以看下重启部分的判断:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const workInProgressRoot = getWorkInProgressRoot();
const workInProgressRootRenderLanes = getWorkInProgressRootRenderLanes();

const nextLanes = getNextLanes(
root,
root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,
);
// 如果 nextLanes 为 NoLanes,就证明所有任务都执行完毕了
if (nextLanes === NoLanes) {
// ...
root.callbackNode = null;
root.callbackPriority = NoLane;
// 只要 nextLanes 为 NoLanes,就可以结束调度了
return;
}
// 如果 nextLanes 不为 NoLanes,就代表还有任务未执行完,也就是那些被打断的低优先级任务
// ...

commit 的最后还是会 ensureRootIsScheduled,高优先级插队后低优先级任务能重启的原因:taskQueue中被打断的低优先级的任务已经 pop了,但是 root 上还有 pendingLanes,通过 ensureRootIsScheduled 重新把低优先级的任务加入 taskQueue 中。

流程总结

  1. 任务通过 unstable_scheduleCallback 进行调度,将任务添加到 taskQueue 中,如果是首次加载通过 requestHostCallback 调度宏任务
  2. Scheduler 里执行 flush 后执行 workLoop 方法,在 workLoop 方法中取出第一个任务,判断是否超出切片时间或者有更需要及时反馈的用户操作,如果没有,那当执行任务的 callback 时,也就是 performConcurrentWorkOnRoot 方法,执行流程可以看上面。所以我们知道 performConcurrentWorkOnRoot 方法可能会返回null,也可能会返回 performConcurrentWorkOnRoot
  3. 当返回 null 时候,Scheduler 会执行 pop(taskQueue),将当前的任务从 taskQueue中弹出
  4. 低优先级任务重启,commitRootImpl 最后会再次调用 ensureRootIsScheduled(root, now()),判断如果 nextLanesNoLanes,就证明所有任务都执行完毕了,如果 nextLanes 不为 NoLanes,就代表还有任务未执行完,也就是那些被打断的低优先级任务,会重新进行调度

任务插队情况具体总结分析

结合 eventLoop, 用户 click 是一个宏任务,会把回调 push 到宏任务队列,等待下一次执行。react 的并发,也是分成了 5ms 的宏任务来执行。

情况1 - 用户手动触发高优先级任务

例如正在执行一个并发任务的时候,用户点击了按钮准备触发 setState(可能是一个 lane2 的同步任务,也可能是比现在优先级高的其它任务),会把这个回调加入到宏任务队列,等到并发任务执行结束,( flushWork 的返回值如果是 true,也会 schedulePerformWorkUntilDeadline 再次 push 一个任务到宏任务队列,click 的回调在这个任务的前面)

下次进入宏任务队列,取出一个任务也就是 click 的回调,执行 dispatchSetStateisRenderPhaseUpdatefalse(还没进入到 render 阶段),scheduleUpdateOnFiber 也会向 root 上标记待执行任务的优先级,ensureRootIsScheduled 调度了微任务scheduleImmediateTask,宏任务执行完毕

进入微任务执行 processRootScheduleInMicrotask,执行 scheduleTaskForRootDuringMicrotask

  1. 这里会进行高优先级打断低优先级的 cancelCallback 操作、通过 scheduleCallback 将任务加入到 taskQueue 中(taskQueue 是最小堆,也会根据 lane 排序)
  2. 上面如果是同步任务,就不会执行 scheduleCallback,会在 microtask 结束时,flushSyncWorkOnAllRoots();( flush 任何pending 的同步 work。这必须放在最后,因为它执行实际的可能会抛出异常的渲染工作。)
  3. 最后执行完微任务,等到下次执行宏任务的时候,高优先级的就会先执行,以后再执行到低优先级时,其实低优先级没有 callback 了,就会跳过,后面commit 最后再重新发起调度低优先级任务

情况2 - 代码里触发了高优先级任务

还有另一种情况是在某个并发任务的 performConcurrentWorkOnRoot 里的 render 过程中加入了可能是一个 lane2 的同步任务,也可能是比现在优先级高的其它任务,会在此次任务切片(5ms)结束后,来进行判断(切片结束是通过 workLoopConcurrent 里面判断)

1
2
3
4
5
6
7
8
9
10
export function performConcurrentWorkOnRoot(
root: FiberRoot,
didTimeout: boolean,
): RenderTaskFn | null {

// ...

ensureRootIsScheduled(root);
return getContinuationForRoot(root, originalCallbackNode);
}
  1. 如果是比现在优先级高的任务但又不是同步任务,getContinuationForRootscheduleTaskForRootDuringMicrotask 会进行打断低优先级任务并对新的且不是同步任务的发起 scheduleCallback 调度加入到 taskQueue 中,最终 performConcurrentWorkOnRoot 的返回值 return null,在 workLoopcontinuationCallbacknull, 会把当前的低优先级任务移除。取出 taskQueue 中的目前最高优先级的任务进行新的调度。
  2. 如果是添加同步任务是在 dispatchSetState 里的 ensureRootIsScheduled 发起的微任务调度,或者在 performConcurrentWorkOnRoot 的最后 ensureRootIsScheduled 发起的调度,执行完本次任务切片的宏任务接着执行微任务的时候同步任务就会被执行了

所以所有同步任务也都是在 microtask 结束时执行的,已保证最高的优先级且尽可能早的执行

2. 饥饿任务问题

上面说到,在高优先级任务执行完毕之后,低优先级任务就会被重启,但假设如果持续有高优先级任务持续进来,那低优先级任务岂不是没有重启之日?

所以 react 为了处理解决饥饿任务问题,在 ensureRootIsScheduled 函数开始的时候做了以下处理:(参考markStarvedLanesAsExpired函数)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
// 在 调度更新的过程中会被调用
// 检查是有lanes挨饿,如果有,则标记他们过期,即提升优先级(以便下次执行)。
export function markStarvedLanesAsExpired(
root: FiberRoot,
currentTime: number,
): void {
// TODO: This gets called every time we yield. We can optimize by storing
// the earliest expiration time on the root. Then use that to quickly bail out
// of this function.

const pendingLanes = root.pendingLanes;
const suspendedLanes = root.suspendedLanes;
const pingedLanes = root.pingedLanes;
const expirationTimes = root.expirationTimes; // 数组,初始化的时候,值都是-1

// Iterate through the pending lanes and check if we've reached their
// expiration time. If so, we'll assume the update is being starved and mark
// it as expired to force it to finish.
// 遍历pending lanes,并检查是否已经达到它们的过期时间。
// 如果是,我们就认为这个update挨饿了,并将其标记为已过期,以强制其完成。
// TODO: We should be able to replace this with upgradePendingLanesToSync
//
// We exclude retry lanes because those must always be time sliced, in order
// to unwrap uncached promises.
// TODO: Write a test for this
let lanes = enableRetryLaneExpiration
? pendingLanes // ? sy
: pendingLanes & ~RetryLanes;

while (lanes > 0) {
// 下面两行代码的作用是找到lanes中最低位的1,即优先级最
const index = pickArbitraryLaneIndex(lanes);
// 把1左移index位,即得到一个只有第index位为1的子掩码
const lane = 1 << index;

const expirationTime = expirationTimes[index];
if (expirationTime === NoTimestamp) {
// sy- console.log('%c [ ]-1469', 'font-size:13px; background:pink; color:#bf2c9f;', lane)
// Found a pending lane with no expiration time. If it's not suspended, or
// if it's pinged, assume it's CPU-bound. Compute a new expiration time
// using the current time.
// 如果这个 pending lane 没有过期时间
// 如果它没有被挂起且需要更新,我们就认为它是CPU密集型操作。
// 用当前时间计算出一个新的过期时间给它。
// CPU bound / IO Bound
if (
(lane & suspendedLanes) === NoLanes ||
(lane & pingedLanes) !== NoLanes
) {
// sy- console.log('%c [ 饿死 ]-482', 'font-size:13px; background:pink; color:#bf2c9f;', lane)
// Assumes timestamps are monotonically increasing.
// 假设timestamps(时间戳)是单调递增的
expirationTimes[index] = computeExpirationTime(lane, currentTime);
}
} else if (expirationTime <= currentTime) {
// 这个 pending lane 已经过期了
// This lane expired
root.expiredLanes |= lane;
}
// 把lane从lanes中移除,计算下一个lane
lanes &= ~lane;
}
}

可以参考 render 阶段执行的函数 performConcurrentWorkOnRoot 中的代码片段

1
2
3
4
// ! 1. render
let exitStatus = shouldTimeSlice
? renderRootConcurrent(root, lanes)
: renderRootSync(root, lanes); // 不用时间切片

可以看到只要 shouldTimeSlice 只要返回 false,就会执行 renderRootSync,也就是以同步优先级进入 render 阶段。而 shouldTimeSlice 的逻辑也就是刚刚的 expiredLanes 属性相关

1
2
3
4
const shouldTimeSlice =
!includesBlockingLane(root, lanes) &&
!includesExpiredLane(root, lanes) &&
(disableSchedulerTimeoutInWorkLoop || !didTimeout);
1
2
3
4
5
6
// 检查root是否包含过期的lane
export function includesExpiredLane(root: FiberRoot, lanes: Lanes): boolean {
// This is a separate check from includesBlockingLane because a lane can
// expire after a render has already started.
return (lanes & root.expiredLanes) !== NoLanes;
}

并发和并行

  • 并发指的是:多个任务,在同一时间段内同时发生,多个任务会有互相抢占资源的情况。
  • 并行指的是:多个任务,在同一时间点上同时发生,多个任务之间不会互相抢占资源。

在这里,我们千万不要理解 Concurrent Mode 是在同时执行多个任务,相信很多人包括我在内都理解混淆,在看源码后,才恍然大悟。

它是同一段时间内,可以执行多个任务:

  • 因为高优先级任务可以插队并打断低优先级任务
  • 或当时间切片的某一条任务执行超时后,将执行权交给主线程,当主线程执行完成,又继续执行之前的切片任务

为什么需要并发模式

我们知道在同步模式下,从 setState虚拟DOM遍历,再到 真实DOM 更新,整个过程都是同步执行且无法被中断的,这样可能就会出现一个问题 —— 用户事件触发的更新被阻塞。

因为我们期望一些不重要的更新不会影响一些重要的更新,比如长列表渲染不会阻塞用户 input 输入,从而提升用户体验。

并发模式特点

可中断

对于 React 来说,任务可能很多,如果不区分优先级,那就是先来后到的顺序。如果高优先级任务来了,但是低优先级任务还没有处理完毕,就会造成高优先级任务等待的局面。

由此可见,对于复杂项目来说,任务可中断这件事情很重要。那么问题来了,React 是如何做到的呢,其实基础还是 fiberfiber 本身链表结构,就是指针,想指向别的地方加个属性值就行了。

可遗弃

Concurrent 模式下,有些update可能会被遗弃掉。

比如我想在淘宝搜索“老人与海”,那么我在输入框输入“老人与海”的过程中,“老人”会有对应的模糊查询结果,但是不一定是我想要的结果,所以这个时候的模糊查询框的update就是低优先级,“老人”对应UI的update相对input的update,优先级就会低一些。在现在React18中,这个模糊查询相关的UI可以被当做transition。

状态复用

Concurrent 模式 下,还支持状态的复用。某些情况下,比如用户走了,又回来,那么上一次的页面状态应当被保存下来,而不是完全从头再来。当然实际情况下不能缓存所有的页面,不然内存不得爆炸,所以还得做成可选的。

目前, React 正在用 Offscreen 组件来实现这个功能。

另外,使用 OffScreen,除了可以复用原先的状态,我们也可以使用它来当做新UI的缓存准备,就是虽然新UI还没登场,但是可以先在后台准备着,这样一旦轮到它,就可以立马快速地渲染出来。

如何实现并发模式(Concurrent Mode)

Concurrent Mode 这种中断渲染的行为,带来了几个关键问题:

  • 怎样做到中断渲染?
  • 怎样定义任务的重要程度和执行顺序?
  • 何时中断任务,怎样划分时间片?

个人总结主要通过三个方面完成:

  • 颗粒化更新节点来解决递归不可中断问题;
  • 任务增加优先级来解决任务执行顺序;
  • 创建任务调度机制来解决时间分片和任务中断,任务恢复;

对应到React的实现就是:Fiber架构lane模型scheduler任务调度

Fiber架构

在重构 Fiber 架构之前,React 是没办法解决这些问题的。因为在此之前,React 的渲染更新主要是通过对比更新前后的 虚拟DOM,找出不同进行更新,而对比的过程因为 虚拟DOM 树结构的限制,只能采用递归更新,我们知道递归一旦开始,中途就无法中断。

Fiber 架构为什么能解决这个问题呢?

  • 每个 Fiber 节点对应一个 React Element,保存有该组件的所有基本状态信息;
  • 每个 Fiber 节点保存有该组件的更新信息;

因为 Fiber 节点承载了基本状态和更新信息,这样 React 就可以将 Fiber 节点视为最小的工作单元,可以实现 Fiber 节点这种粒度的更新,因为粒度的细化也就使得异步可中断更新成为了可能。

Fiber 节点的基本状态保存了它的父节点,子节点,兄弟节点信息,这样可以将之前的递归遍历改变为循环遍历,使渲染中断成为可能。

Lane模型

lane模型 主要解决的是任务优先级问题。

我们想中断渲染的本质是想让有更高优先级的任务可以中断低优先级任务来插队执行。

那怎么定义任务优先级呢,lane模型 通过31位的位运算符来定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// * lane 值越小,优先级越高
export const TotalLanes = 31;

// 没有优先级
export const NoLanes: Lanes = /* */ 0b0000000000000000000000000000000;
export const NoLane: Lane = /* */ 0b0000000000000000000000000000000;

// 同步优先级,表示同步的任务一次只能执行一个,例如:用户的交互事件产生的更新任务
export const SyncLane: Lane = /* */ 0b0000000000000000000000000000001;

// 连续触发优先级,例如:滚动事件,拖动事件等
export const InputContinuousHydrationLane: Lane = /* */ 0b0000000000000000000000000000010;
export const InputContinuousLane: Lanes = /* */ 0b0000000000000000000000000000100;

// 默认优先级,例如使用setTimeout,请求数据返回等造成的更新
export const DefaultHydrationLane: Lane = /* */ 0b0000000000000000000000000001000;
export const DefaultLane: Lanes = /* */ 0b0000000000000000000000000010000;

// 过渡优先级,例如: Suspense、useTransition、useDeferredValue等拥有的优先级
const TransitionHydrationLane: Lane = /* */ 0b0000000000000000000000000100000;
const TransitionLanes: Lanes = /* */ 0b0000000001111111111111111000000;
const TransitionLane1: Lane = /* */ 0b0000000000000000000000001000000;
const TransitionLane2: Lane = /* */ 0b0000000000000000000000010000000;
const TransitionLane3: Lane = /* */ 0b0000000000000000000000100000000;
const TransitionLane4: Lane = /* */ 0b0000000000000000000010000000000; // 1024
const TransitionLane5: Lane = /* */ 0b0000000000000000000100000000000;
const TransitionLane6: Lane = /* */ 0b0000000000000000001000000000000;
const TransitionLane7: Lane = /* */ 0b0000000000000000010000000000000;
const TransitionLane8: Lane = /* */ 0b0000000000000000100000000000000;
const TransitionLane9: Lane = /* */ 0b0000000000000001000000000000000;
const TransitionLane10: Lane = /* */ 0b0000000000000010000000000000000;
const TransitionLane11: Lane = /* */ 0b0000000000000100000000000000000;
const TransitionLane12: Lane = /* */ 0b0000000000001000000000000000000;
const TransitionLane13: Lane = /* */ 0b0000000000010000000000000000000;
const TransitionLane14: Lane = /* */ 0b0000000000100000000000000000000;
const TransitionLane15: Lane = /* */ 0b0000000001000000000000000000000;

const RetryLanes: Lanes = /* */ 0b0000011110000000000000000000000;
const RetryLane1: Lane = /* */ 0b0000000010000000000000000000000;
const RetryLane2: Lane = /* */ 0b0000000100000000000000000000000;
const RetryLane3: Lane = /* */ 0b0000001000000000000000000000000;
const RetryLane4: Lane = /* */ 0b0000010000000000000000000000000;

export const SelectiveHydrationLane: Lane = /* */ 0b0000100000000000000000000000000;

export const IdleHydrationLane: Lane = /* */ 0b0001000000000000000000000000000;
export const IdleLane: Lane = /* */ 0b0010000000000000000000000000000;

export const OffscreenLane: Lane = /* */ 0b0100000000000000000000000000000;
export const DeferredLane: Lane = /* */ 0b1000000000000000000000000000000;

可以看到 React 定义的优先级:

同步任务 > 连续触发事件任务 > setTimeout,请求更新任务 > 过渡任务(React18新特性)

事件优先级

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
export function getEventPriority(domEventName: DOMEventName): * {
switch (domEventName) {
case 'cancel':
case 'click':
case 'copy':
case 'dragend':
case 'dragstart':
case 'drop':
...
case 'focusin':
case 'focusout':
case 'input':
case 'change':
case 'textInput':
case 'blur':
case 'focus':
case 'select':
// 同步优先级
return DiscreteEventPriority;
case 'drag':
case 'mousemove':
case 'mouseout':
case 'mouseover':
case 'scroll':
...
case 'touchmove':
case 'wheel':
case 'mouseenter':
case 'mouseleave':
// 连续触发优先级
return ContinuousEventPriority;
...
default:
return DefaultEventPriority;
}
}
EventPriority Lane 数值
DiscreteEventPriority 离散事件。click、keydown、focusin等,事件的触发不是连续,可以做到快速响应 SyncLane 1
ContinuousEventPriority 连续事件。drag、scroll、mouseover等,事件的是连续触发的,快速响应可能会阻塞渲染,优先级较离散事件低 InputContinuousLane 4
DefaultEventPriority 默认的事件优先级 DefaultLane 16
IdleEventPriority 空闲的优先级 IdleLane 536870912

调度优先级

SchedulerPriority EventPriority 大于>17.0.2 小于>17.0.2
ImmediatePriority DiscreteEventPriority 1 99
UserblockingPriority Userblocking 2 98
NormalPriority DefaultEventPriority 3 97
LowPriority DefaultEventPriority 4 96
IdlePriority IdleEventPriority 5 95
NoPriority 0 90

优先级间的转换

  • lane优先级 转 event优先级(参考 lanesToEventPriority 函数)

    • 转换规则:以区间的形式根据传入的lane返回对应的 event 优先级。比如传入的优先级不大于 Discrete 优先级,就返回 Discrete 优先级,以此类推
  • event优先级 转 scheduler优先级(参考 ensureRootIsScheduled 函数下的 lanesToEventPriority)

    • 转换规则:可以参考上面调度优先级表
  • event优先级 转 lane优先级(参考 getEventPriority 函数)

    • 转换规则:对于非离散、连续的事件,会根据一定规则作转换,具体课参考上面 event 优先级表

任务饥饿

任务饥饿是讲一个低优先级的任务一直被高优先级的任务插队,导致这个任务已经过了执行期限依然没有得到执行,在这种情况下,React 会将该任务置为同步渲染任务,在下次更新时立即执行。

任务插队

具体可查看这篇文章 《react18.2批处理场景原理并结合优先级进行的分析》

scheduler任务调度

具体可查看这篇文章 《react18.2调度器scheduler源码分析》

事件优先级

1
2
3
4
5
6
7
// packages\react-reconciler\src\ReactEventPriorities.js

// 优先级从上往下,越来越小
export const DiscreteEventPriority: EventPriority = SyncLane; // 2
export const ContinuousEventPriority: EventPriority = InputContinuousLane; // 8
export const DefaultEventPriority: EventPriority = DefaultLane; // 页面初次渲染的lane 32, transition
export const IdleEventPriority: EventPriority = IdleLane;
1
2
3
4
5
6
7
// packages\react-reconciler\src\ReactFiberLane.js

export const NoLane: Lane = /* */ 0b0000000000000000000000000000000; // 0
export const SyncLane: Lane = /* */ 0b0000000000000000000000000000010; // 2
export const InputContinuousLane: Lane = /* */ 0b0000000000000000000000000001000; // 8
export const DefaultLane: Lane = /* */ 0b0000000000000000000000000100000; // 32
export const IdleLane: Lane = /* */ 0b0010000000000000000000000000000;

各种批处理操作场景和结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
export default function App() {
const [count, setCount] = useState(0)
const [page, setPage] = useState(0)

console.log('render')

const onPerform = () => {
// !setCount嵌套场景1 点击:页面变成1000 + render 2次,划入:页面变成1000 + render连续 2次
// setCount((prev) => prev + 1)
// setCount((prev) => {
// document.getElementById('onTest').click() // renderWithHooks时候执行这里,fiber和currentlyRenderingFiber相等,所以isRenderPhaseUpdate为true,这里最终走的renderWithHooksAgain
// return prev + 10
// })
// setCount((prev) => prev + 100)
// !setCount嵌套场景2 点击:页面变成1000 + render 2次,划入:页面变成1000 + render连续 2次
// setCount((prev) => prev + 1)
// setCount((prev) => {
// setCount(1000)
// return prev + 10
// })
// setCount((prev) => prev + 100)
// !setCount嵌套场景3 点击:页面变成1000 + render 2次,划入:页面变成1000 + render连续 2次
// setTimeout(() => {
// setCount((prev) => prev + 1)
// setCount((prev) => {
// document.getElementById('onTest').click()
// return prev + 10
// })
// setCount((prev) => prev + 100)
// })

// !插入一个click场景 点击:页面变成1100 + render 1次,划入:页面变成1100 + render 1次
// setCount((prev) => prev + 1)
// setCount((prev) => {
// return prev + 10
// })
// document.getElementById('onTest').click() // 这里相当于在点击阶段就已经加入到pending中了,而且执行到这里的时候,isRenderPhaseUpdate为false,因为点击的时候没有还没有currentlyRenderingFiber
// setCount((prev) => prev + 100)

// !情况1 点击:页面变成1 + render 1次,划入:页面变成1 + render 1次
// setCount(count + 1)
// setCount(count + 1)
// setCount(count + 1) // 1
// !情况2 点击:页面变成3 + render 1次,划入:页面变成3 + render 1次
// setCount((prev) => prev + 1)
// setCount((prev) => prev + 1)
// setCount((prev) => prev + 1) // 3
// !情况3 两个不同的state 点击:页面count变成1,page变成-1 + render 1次,划入:页面变成count变成1,page变成-1 + render 1次
// setCount(count + 1)
// setPage(page - 1)
// !情况4 点击:页面变成1 + render 1次,划入:页面变成1 + render 1次
// setTimeout(() => {
// setCount(count + 1)
// setCount(count + 1)
// })
// !情况5 点击:页面变成1 + render 2次,划入:页面变成1 + render 2次
// setCount(count + 1)
// setCount(count + 1)
// setCount(count + 1)
// setTimeout(() => {
// setCount(count + 1)
// setCount(count + 1)
// })
// !情况6 点击:页面变成2 + render 1次,划入:页面变成2 + render 1次
// setCount((prev) => {
// console.log('hook update 1')
// return prev + 1
// })
// setCount((prev) => {
// console.log('hook update 2')
// return prev + 1
// }) // 2
// !情况7 点击:页面变成1 + render 2次,划入:页面变成1 + render 1次
// setCount((prev) => {
// console.log('hook update 1') // hook update 1
// return prev + 1
// })
// setCount((prev) => {
// console.log('hook update 2') // hook update 2
// return prev + 1
// })
// setTimeout(() => {
// setCount(count + 1)
// setCount(count + 1)
// })
// !情况8 点击:页面变成4 + render 2次,划入:页面变成4 + render 1次
// setCount((prev) => {
// console.log('hook update 1') // hook update 1
// return prev + 1
// })
// setCount((prev) => {
// console.log('hook update 2') // hook update 2
// return prev + 1
// })
// setTimeout(() => {
// setCount((prev) => {
// console.log('hook update 3') // hook update 3
// return prev + 1
// })
// setCount((prev) => {
// console.log('hook update 4') // hook update 4
// return prev + 1
// })
// })
// !情况9 点击:页面变成2 + render 2次,划入:页面变成2 + render 2次
// flushSync(() => {
// setCount(count + 1)
// })
// setCount(count + 2)
// !情况10 点击:页面变成2 + render 2次,划入:页面变成2 + render 2次
// flushSync(() => {
// setCount(count + 1)
// })
// sleep(1000)
// setCount(count + 2)
// !情况11 点击:页面变成2 + render 2次,划入:页面变成2 + render 1次
// setCount(count + 1)
// Promise.resolve().then(() => {
// setCount(count + 2)
// })
}
// !情况12 页面变成1 + render 1次
// const changeValue = () => {
// // setCount((prevState, props) => ({
// // count: prevState.count + 1,
// // }));
// // setCount((prevState, props) => ({
// // count: prevState.count + 1,
// // }));
// // setCount((prevState, props) => ({
// // count: prevState.count + 1,
// // }));
// setCount(count + 1)
// setCount(count + 1)
// setCount(count + 1)
// console.log(count) // react18 输出的是更新前的值 --> 0
// }
// document.body.addEventListener('click', changeValue, false)

function sleep(milliseconds) {
const now = Date.now()
while (Date.now() - now < milliseconds) {}
}

return (
<>
<h1 onClick={onPerform} onMouseEnter={onPerform}>
Hello CodeSandbox {count}
</h1>
</>
)
}

执行流程和原理

dispatchSetState 的源码在 setState文章中 展示过一次。这里只是重复什么是批-更新的说法,通过点击事件或者鼠标划入事件触发setState,其实就是调用 dispatchSetState 方法。dispatchSetState 所做的就是:

  1. hook 更新加入更新队列
  2. 尝试调度一次 react更新
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
function dispatchSetState<S, A>(
fiber: Fiber,
queue: UpdateQueue<S, A>,
action: A,
): void {

const lane = requestUpdateLane(fiber);
// ! 1. 创建update
const update: Update<S, A> = {
lane,
revertLane: NoLane,
action,
hasEagerState: false,
eagerState: null,
next: (null: any),
};

if (isRenderPhaseUpdate(fiber)) {
// ...
} else {

// ...

// ! 2. 把update暂存到concurrentQueues数组中
const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);
if (root !== null) {
// ! 3. 调度更新
scheduleUpdateOnFiber(root, fiber, lane);
entangleTransitionUpdate(root, queue, lane);
}
}

// ...
}

如果不纠结 fiberqueue 的细节的话,就批处理而言,这就是 setState的全部了。

步骤1:获取本次更新的优先级 - requestUpdateLane

dispatchSetState 方法在 创建update 前,会通过 requestUpdateLane 方法取到 update 的优先级。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
export function requestUpdateLane(fiber: Fiber): Lane {
// Special cases
const mode = fiber.mode;
// 1. 非ConcurrentMode模式 2. 目前不支持
if ((mode & ConcurrentMode) === NoMode) {
return (SyncLane: Lane);
} else if (
(executionContext & RenderContext) !== NoContext &&
workInProgressRootRenderLanes !== NoLanes
) {
// This is a render phase update. These are not officially supported. The
// old behavior is to give this the same "thread" (lanes) as
// whatever is currently rendering. So if you call `setState` on a component
// that happens later in the same render, it will flush. Ideally, we want to
// remove the special case and treat them as if they came from an
// interleaved event. Regardless, this pattern is not officially supported.
// This behavior is only a fallback. The flag only exists until we can roll
// out the setState warning, since existing code might accidentally rely on
// the current behavior.
return pickArbitraryLane(workInProgressRootRenderLanes);
}

// 普通更新与非紧急更新(18)
const transition = requestCurrentTransition();
// 如果有transition
if (transition !== null) {
const actionScopeLane = peekEntangledActionLane();
return actionScopeLane !== NoLane
? // We're inside an async action scope. Reuse the same lane.
actionScopeLane
: // We may or may not be inside an async action scope. If we are, this
// is the first update in that scope. Either way, we need to get a
// fresh transition lane.
requestTransitionLane(transition);
}

// React内部的一些update,比如flushSync,会通过上下文变量来跟踪其优先级
const updateLane: Lane = (getCurrentUpdatePriority(): any);
if (updateLane !== NoLane) {
// ? sy setState click 2
return updateLane;
}

// React外部的update,根据事件类型,向当前环境获取对应的优先级。
const eventLane: Lane = (getCurrentEventPriority(): any);
return eventLane;
}

getCurrentEventPriority

1
2
3
4
5
6
7
8
9
export function getCurrentEventPriority(): EventPriority {
const currentEvent = window.event;
if (currentEvent === undefined) {
// ? sy 页面初次渲染
return DefaultEventPriority;
}

return getEventPriority(currentEvent.type);
}

getEventPriority

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
export function getEventPriority(domEventName: DOMEventName): EventPriority {
switch (domEventName) {
// Used by SimpleEventPlugin:
case 'cancel':
case 'click':
case 'close':
case 'contextmenu':
case 'copy':
case 'cut':
case 'auxclick':
case 'dblclick':
case 'dragend':
case 'dragstart':
case 'drop':
case 'focusin':
case 'focusout':
case 'input':
case 'invalid':
case 'keydown':
case 'keypress':
case 'keyup':
case 'mousedown':
case 'mouseup':
case 'paste':
case 'pause':
case 'play':
case 'pointercancel':
case 'pointerdown':
case 'pointerup':
case 'ratechange':
case 'reset':
case 'resize':
case 'seeked':
case 'submit':
case 'touchcancel':
case 'touchend':
case 'touchstart':
case 'volumechange':
// Used by polyfills: (fall through)
case 'change':
case 'selectionchange':
case 'textInput':
case 'compositionstart':
case 'compositionend':
case 'compositionupdate':
// Only enableCreateEventHandleAPI: (fall through)
case 'beforeblur':
case 'afterblur':
// Not used by React but could be by user code: (fall through)
case 'beforeinput':
case 'blur':
case 'fullscreenchange':
case 'focus':
case 'hashchange':
case 'popstate':
case 'select':
case 'selectstart':
return DiscreteEventPriority;
case 'drag':
case 'dragenter':
case 'dragexit':
case 'dragleave':
case 'dragover':
case 'mousemove':
case 'mouseout':
case 'mouseover':
case 'pointermove':
case 'pointerout':
case 'pointerover':
case 'scroll':
case 'toggle':
case 'touchmove':
case 'wheel':
// Not used by React but could be by user code: (fall through)
case 'mouseenter':
case 'mouseleave':
case 'pointerenter':
case 'pointerleave':
return ContinuousEventPriority;
case 'message': {
// We might be in the Scheduler callback.
// Eventually this mechanism will be replaced by a check
// of the current priority on the native scheduler.
const schedulerPriority = getCurrentSchedulerPriorityLevel();
switch (schedulerPriority) {
case ImmediateSchedulerPriority:
return DiscreteEventPriority;
case UserBlockingSchedulerPriority:
return ContinuousEventPriority;
case NormalSchedulerPriority:
case LowSchedulerPriority:
// TODO: Handle LowSchedulerPriority, somehow. Maybe the same lane as hydration.
return DefaultEventPriority;
case IdleSchedulerPriority:
return IdleEventPriority;
default:
return DefaultEventPriority;
}
}
default:
return DefaultEventPriority;
}
}

可以看到,通过一系列操作:

  • click 事件对应的优先级是 DiscreteEventPriority,优先级值也就是 2
  • mouseenter 对应的优先级是 ContinuousEventPriority,优先级值也就是 8
  • setTimeout 对应的优先级的值是默认优先级 32

一般在合成事件创建之初,就会通过 createEventListenerWrapperWithPriority 各个事件的设置优先级,所以一般 clickmouseenter 等react内部的事件,都会通过 getCurrentUpdatePriority 拿到 updateLane
类似 setTimeout,会走到 getCurrentEventPriority 返回 DefaultEventPriority
如果是通过 document.body.addEventListener('click', changeValue, false) 触发的事件,也会走到 getCurrentEventPriority,通过getEventPriority 拿到 window.event 所对应的优先级

总结

requestUpdateLane 中获取优先级:

  1. 先通过requestCurrentTransition请求并判断是否有非紧急更新
  2. 再通过getCurrentUpdatePriority
  3. 最后通过getCurrentEventPriority根据事件类型,项当前环境获取对应的优先级。

步骤2:创建 update 对象

1
2
3
4
5
6
7
8
9
10
11
// ...
// ! 1. 创建update
const update: Update<S, A> = {
lane,
revertLane: NoLane,
action,
hasEagerState: false,
eagerState: null,
next: (null: any),
};
// ...

步骤3:scheduleUpdateOnFiber

忽略一些琐碎的细节后,你可以发现这个函数的核心逻辑甚至更简单:

  • 标记一次具有某一优先级的更新(markRootUpdated)
  • 调用ensureRootIsScheduled
1
2
3
4
5
6
7
8
9
10
11
12
export function scheduleUpdateOnFiber(
root: FiberRoot,
fiber: Fiber,
lane: Lane,
eventTime: number,
) {
// ...
markRootUpdated(root, lane, eventTime);
// ...
ensureRootIsScheduled(root, eventTime);
// ...
}

ensureRootIsScheduled

1
2
3
4
5
6
7
8
9
10
export function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
// ...
// 这里是多次调用不再调度微任务触发processRootScheduleInMicrotask
if (!didScheduleMicrotask) {
// ? sy
didScheduleMicrotask = true;
scheduleImmediateTask(processRootScheduleInMicrotask);
}
// ...
}

processRootScheduleInMicrotask

1
2
3
4
5
6
7
8
9
while (root !== null) {
// ...
const nextLanes = scheduleTaskForRootDuringMicrotask(root, currentTime);
// ...
}

// 在microtask结束时,flush任何pending的同步work。这必须放在最后,因为它执行实际的可能会抛出异常的渲染工作。
// onClick count
flushSyncWorkOnAllRoots();

scheduleTaskForRootDuringMicrotask

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 获取当前所有优先级中最高的优先级
const nextLanes = getNextLanes(
root,
root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,
);

// ...

if (includesSyncLane(nextLanes)) {
// 同步工作始终在微任务结束时刷新,因此我们不需要安排额外的任务。
if (existingCallbackNode !== null) {
cancelCallback(existingCallbackNode);
}
root.callbackPriority = SyncLane;
root.callbackNode = null;
return SyncLane;
} else {
// 本次要调度的优先级
const newCallbackPriority = getHighestPriorityLane(nextLanes);
// 已经存在的调度的优先级
const existingCallbackPriority = root.callbackPriority;

if (existingCallbackPriority === newCallbackPriority) {
// 这里就是同等优先级做批处理
// ...
return;
}
// ... 高优先级打断低优先级

// 调度更新流程
newCallbackNode = scheduleCallback(schedulerPriorityLevel, performConcurrentWorkOnRoot.bind(null, root));

// ...实际的调度,最后会给root.callbackPriority赋值
root.callbackPriority = newCallbackPriority;
root.callbackNode = newCallbackNode;

}

执行流程小结

通过微任务调度 processRootScheduleInMicrotask 方法,此方法核心就是调用 scheduleTaskForRootDuringMicrotask,而在 microtask 结束时,调用 flushSyncWorkOnAllRoots 方法 flush 任何 pending 的同步 work。这必须放在最后,因为它执行实际的可能会抛出异常的渲染工作。

scheduleTaskForRootDuringMicrotask 中:

  1. 先是检查是否有饿死的任务,如果是,将它们标记为过期,这样我们就知道下一个要处理的是它们
  2. 获取当前所有优先级中最高的优先级 nextLanes
  3. 先判断是否包含同步优先级,如果包含,取消其它任务并返回,不包含就继续执行
  4. 再判断现存的任务优先级和新任务的优先级,如果相等,就合并执行且 return
  5. 如果不相等,就取消现存任务
  6. 继续根据 nextLanes,通过 lanesToEventPriority 方法,获取 schedulerPriorityLevel 调度的优先级,最后通过 scheduleCallback 进行调度

调用 scheduleTaskForRootDuringMicrotask 中的两大问题

请看《react18.2高优先级任务插队和饥饿任务问题分析》

updateReducerImpl

我们以函数式组件的处理 setState 时的实际执行函数 updateReducerImpl 为例,来看一下相关处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
function updateReducerImpl<S, A>(
hook: Hook,
current: Hook,
reducer: (S, A) => S,
): [S, Dispatch<A>] {

const queue = hook.queue;

if (queue === null) {
throw new Error(
'Should have a queue. This is likely a bug in React. Please file an issue.',
);
}

// 记录reducer
queue.lastRenderedReducer = reducer;

// The last rebase update that is NOT part of the base state.
let baseQueue = hook.baseQueue; // ! 单向循环链表

// The last pending update that hasn't been processed yet.
const pendingQueue = queue.pending;
if (pendingQueue !== null) {
// 如果有 pendingQueue,说明有未处理的更新
// 把pendingQueue添加到baseQueue中
if (baseQueue !== null) {
// pending queue合并到base queue中。
const baseFirst = baseQueue.next;
const pendingFirst = pendingQueue.next;
baseQueue.next = pendingFirst;
pendingQueue.next = baseFirst;
}
// baseQueue 被设置为 pendingQueue,也就是待更新的 update 对象队列
current.baseQueue = baseQueue = pendingQueue;
// 需要清空 pending 队列,这样本次执行 hook 的 pending 队列不会影响到下一次
queue.pending = null;
}

const baseState = hook.baseState;
// 如果基础更新队列中有 update 对象,说明需要执行更新,就按照优先级依次处理每一个 update 对象(下面会进行批量更新)
if (baseQueue === null) {
// 如果没有pendingQueue,那么memoizedState应该等于baseState
hook.memoizedState = baseState;
} else {
// 有待处理的queue,接下来遍历即可
const first = baseQueue.next;
let newState = baseState;

let newBaseState = null;
let newBaseQueueFirst = null;
let newBaseQueueLast: Update<S, A> | null = null; // 跳过的这些更新(低优先级任务)会被保存在这个循环链表中
let update = first;
let didReadFromEntangledAsyncAction = false;
do {
// An extra OffscreenLane bit is added to updates that were made to
// a hidden tree, so that we can distinguish them from updates that were
// already there when the tree was hidden.
const updateLane = removeLanes(update.lane, OffscreenLane);
const isHiddenUpdate = updateLane !== update.lane;

// Check if this update was made while the tree was hidden. If so, then
// it's not a "base" update and we should disregard the extra base lanes
// that were added to renderLanes when we entered the Offscreen tree.
const shouldSkipUpdate = isHiddenUpdate
? !isSubsetOfLanes(getWorkInProgressRootRenderLanes(), updateLane)
: !isSubsetOfLanes(renderLanes, updateLane);

// 当前任务的优先级不够,也就是说当前的 renderLanes 比该 update 对象的优先级高。需要先跳过,之后再处理。
if (shouldSkipUpdate) {
// Priority is insufficient. Skip this update. If this is the first
// skipped update, the previous update/state is the new base
// update/state.
const clone: Update<S, A> = {
lane: updateLane,
revertLane: update.revertLane,
action: update.action,
hasEagerState: update.hasEagerState,
eagerState: update.eagerState,
next: (null: any),
};
// 如果在当前被跳过的 update 对象之前没有其他的 update 被跳过,该对象就是作为新的基础更新对象。并把最初跳过任务时候的 baseState 存储起来
if (newBaseQueueLast === null) {
newBaseQueueFirst = newBaseQueueLast = clone;
newBaseState = newState;
} else {
// 如果之前有更新被跳过,那么将这个更新对象添加到队列最后
newBaseQueueLast = newBaseQueueLast.next = clone;
}
// 更新当前正在工作的 Fiber 节点(workInProgress)的优先级,标记这个更新对象的优先级由于不匹配当前的 renderLane,因此已经被跳过。
// 在同文件的 renderWithHook() 方法中可以知道 currentlyRenderingFiber 对应了 workInProgress,表示当前正在工作的 fiber 树
currentlyRenderingFiber.lanes = mergeLanes(
currentlyRenderingFiber.lanes,
updateLane,
);
markSkippedUpdateLanes(updateLane);
} else { // 优先级足够
// 优先级足够的时候理论上可以考虑对哪些 update 对象进行收集更新,但是此时还需要考虑一个 revertLane(还原的优先级),以保证组件状态的正确。
// This update does have sufficient priority.

// Check if this is an optimistic update.
const revertLane = update.revertLane;
if (!enableAsyncActions || revertLane === NoLane) {
// ? sy
// This is not an optimistic update, and we're going to apply it now.
// But, if there were earlier updates that were skipped, we need to
// leave this update in the queue so it can be rebased later.
// 如果 newBaseQueueLast 不为 null,证明有跳过的更新,要把当前的update也加入newBaseQueueLast只是到其 lane 会赋值为 0,后面进行低优先级的更新时,这些已经被处理过的更新也仍旧会被处理,保证最后计算的数据是正确的
if (newBaseQueueLast !== null) {
const clone: Update<S, A> = {
// This update is going to be committed so we never want uncommit
// it. Using NoLane works because 0 is a subset of all bitmasks, so
// this will never be skipped by the check above.
lane: NoLane,
revertLane: NoLane,
action: update.action,
hasEagerState: update.hasEagerState,
eagerState: update.eagerState,
next: (null: any),
};
newBaseQueueLast = newBaseQueueLast.next = clone;
}

// Check if this update is part of a pending async action. If so,
// we'll need to suspend until the action has finished, so that it's
// batched together with future updates in the same action.
if (updateLane === peekEntangledActionLane()) {
// ? sy-no
didReadFromEntangledAsyncAction = true;
}
} else {
// This is an optimistic update. If the "revert" priority is
// sufficient, don't apply the update. Otherwise, apply the update,
// but leave it in the queue so it can be either reverted or
// rebased in a subsequent render.
if (isSubsetOfLanes(renderLanes, revertLane)) {
// The transition that this optimistic update is associated with
// has finished. Pretend the update doesn't exist by skipping
// over it.
update = update.next;

// Check if this update is part of a pending async action. If so,
// we'll need to suspend until the action has finished, so that it's
// batched together with future updates in the same action.
if (revertLane === peekEntangledActionLane()) {
didReadFromEntangledAsyncAction = true;
}
continue;
} else {
const clone: Update<S, A> = {
// Once we commit an optimistic update, we shouldn't uncommit it
// until the transition it is associated with has finished
// (represented by revertLane). Using NoLane here works because 0
// is a subset of all bitmasks, so this will never be skipped by
// the check above.
lane: NoLane,
// Reuse the same revertLane so we know when the transition
// has finished.
revertLane: update.revertLane,
action: update.action,
hasEagerState: update.hasEagerState,
eagerState: update.eagerState,
next: (null: any),
};
if (newBaseQueueLast === null) {
newBaseQueueFirst = newBaseQueueLast = clone;
newBaseState = newState;
} else {
newBaseQueueLast = newBaseQueueLast.next = clone;
}
// Update the remaining priority in the queue.
// TODO: Don't need to accumulate this. Instead, we can remove
// renderLanes from the original lanes.
currentlyRenderingFiber.lanes = mergeLanes(
currentlyRenderingFiber.lanes,
revertLane,
);
markSkippedUpdateLanes(revertLane);
}
}

// 该 update 对象的优先级足够,因此开始处理它的 action,收集新的 state 状态
const action = update.action;
if (shouldDoubleInvokeUserFnsInHooksDEV) {
reducer(newState, action);
}
if (update.hasEagerState) {
// If this update is a state update (not a reducer) and was processed eagerly,
// we can use the eagerly computed state
newState = ((update.eagerState: any): S);
} else {
// ! 计算useReducer的新的state
newState = reducer(newState, action);
}
}
// 循环 baseQueue(实际上就是 pendingQueue),处理该队列中的每个 update 对象,
// 并把对应的 action 操作得到的结果更新到 newState 中(即收集新状态)
update = update.next;
} while (update !== null && update !== first);

if (newBaseQueueLast === null) {
newBaseState = newState; // 如果没有跳过的任务,那就把最终计算后的结果存到newBaseState,最终赋值给baseState,否则baseState就还是第一个被跳过的任务时可以拿到的baseState,也是保证最后执行低优先级任务计算的数据是正确的
} else {
newBaseQueueLast.next = (newBaseQueueFirst: any);
}

// Mark that the fiber performed work, but only if the new state is
// different from the current state.
if (!is(newState, hook.memoizedState)) {
markWorkInProgressReceivedUpdate();

if (didReadFromEntangledAsyncAction) {
const entangledActionThenable = peekEntangledActionThenable();
if (entangledActionThenable !== null) {
throw entangledActionThenable;
}
}
}
// 更新到hook上
hook.memoizedState = newState;

hook.baseState = newBaseState;
// 如果一直没有跳过的,newBaseQueueLast就为null,如果有跳过的就不为null也就赋值给了baseQueue,之后低优先级任务重启后可以执行baseQueue上的任务,当执行最后一批低优先级任务时newBaseQueueLast为null,baseQueue也就为null了,不会影响以后再次触发任务
hook.baseQueue = newBaseQueueLast;
queue.lastRenderedState = newState;
}

if (baseQueue === null) {
// `queue.lanes` is used for entangling transitions. We can set it back to
// zero once the queue is empty.
queue.lanes = NoLanes;
}

const dispatch: Dispatch<A> = (queue.dispatch: any);
return [hook.memoizedState, dispatch];
}

从上面的源码可以看到,updateReducerImpl 方法核心内容分为四个部分:

  • 获取 Hook 对象中的更新队列、基础更新队列、基础状态、reduce 函数等信息。
  • 如果更新队列中有待处理的更新对象,就将其加入到基础更新队列中。
  • 按照优先级高低区分是否跳过该更新或者依次处理基础更新队列中的更新对象,计算新的状态。
  • 判断新的状态和旧的状态是否相同,如果相同就不做重新渲染,反之需要重新渲染。
  • 返回更新后的状态和 dispatch 方法。

针对setState嵌套场景的分析

当执行嵌套的 setState 时候,会执行 dispatchSetState

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function dispatchSetState<S, A>(
fiber: Fiber,
queue: UpdateQueue<S, A>,
action: A,
): void {

const lane = requestUpdateLane(fiber);
// ! 1. 创建update
const update: Update<S, A> = {
lane,
revertLane: NoLane,
action,
hasEagerState: false,
eagerState: null,
next: (null: any),
};
// setState嵌套setState的情况,是同一个fiber
if (isRenderPhaseUpdate(fiber)) {
enqueueRenderPhaseUpdate(queue, update);
} else {
// ...
}
}
1
2
3
4
5
6
7
8
// 判断是否是同一个fiber,如果发生嵌套的情况,是一个fiber,会返回true
function isRenderPhaseUpdate(fiber: Fiber): boolean {
const alternate = fiber.alternate;
return (
fiber === currentlyRenderingFiber ||
(alternate !== null && alternate === currentlyRenderingFiber)
);
}

update 加入到队列中,并将 didScheduleRenderPhaseUpdateDuringThisPass 置为 true

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function enqueueRenderPhaseUpdate<S, A>(
queue: UpdateQueue<S, A>,
update: Update<S, A>,
): void {
// This is a render phase update. Stash it in a lazily-created map of
// queue -> linked list of updates. After this render pass, we'll restart
// and apply the stashed updates on top of the work-in-progress hook.
didScheduleRenderPhaseUpdateDuringThisPass = didScheduleRenderPhaseUpdate =
true;
const pending = queue.pending;
if (pending === null) {
// This is the first update. Create a circular list.
update.next = update;
} else {
update.next = pending.next;
pending.next = update;
}
queue.pending = update;
}

当有 setState 嵌套发生,是发生在 render 阶段的 renderWithHooks 中。执行完函数组件 Component() 会进行判断, 当didScheduleRenderPhaseUpdateDuringThisPasstrue 会执行 renderWithHooksAgain

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
export function renderWithHooks<Props, SecondArg>(
current: Fiber | null,
workInProgress: Fiber,
Component: (p: Props, arg: SecondArg) => any,
props: Props,
secondArg: SecondArg,
nextRenderLanes: Lanes,
): any {
// ...

let children = Component(props, secondArg);

// Check if there was a render phase update
// enqueueRenderPhaseUpdate会给didScheduleRenderPhaseUpdateDuringThisPass设置为true,所以下面还会render一次,这也就是setState嵌套setState的情况下render连续 2次
if (didScheduleRenderPhaseUpdateDuringThisPass) {
// Keep rendering until the component stabilizes (there are no more render
// phase updates).
children = renderWithHooksAgain(
workInProgress,
Component,
props,
secondArg,
);
}

// ...
}

总结

所以通过对执行流程和原理的分析,对于以上提到的这么多批处理的场景,也就能通过源码并结合eventLoop的知识解释的通了。

关于批处理的逻辑,主要有三点:

  • 通过 getNextLanesgetHighestPriorityLane 拿到本次应该(不一定是setState时的那个)更新的优先级 newCallbackPriority
  • 对比上次等待的更新和本次更新的优先级,即 newCallbackPriority === existingCallbackPriority,如果相等,则提前 return
  • 在更新阶段,以 setState 为例,会根据优先级来判断是否要同一批次执行

scheduleUpdateOnFiber 中已经对 setState 对应的优先级做了标记,所以那个优先级在这里是可以被读取到的。如果两次更新的优先级相同,批处理就会起作用。

批处理的发生当然意味着代码进入上述的 newCallbackPriority === existingCallbackPriority 分支内。但是即使是其他情形也有可能进入这个分支。
例如,当连续的两次 setState 被调用,前者优先级高于后者,那么当第二次 setState 被调用,从而进入 ensureRootIsScheduled 时,existingCallbackPriority与newCallbackPriority 都是第一次调用时的优先级(每次所取的都是最高优先级),导致函数提前返回。这并不意味着低优先级的更新被忽略,在高优先级的更新即将完成时,ensureRootIsScheduled 会被再次调用,确保所有更新会被执行。

react16 - 半自动批处理

结论

  • 执行顺序

    • 合成事件和钩子函数中:异步
    • 原生事件和setTimeout中:同步
  • 批量处理

    • 合成事件和钩子函数中的this.setState或者setState:会批量处理
    • 合成事件和钩子函数中的this.setState或者setState里面写成函数:会批量处理
    • 原生事件和setTimeout中任何情况:不会批量处理

源码分析

1. setState

1
2
3
Component.prototype.setState = function(partialState, callback) {
this.updater.enqueueSetState(this, partialState, callback, 'setState');
};

2. this.updater

this.updater 是在哪个地方进行赋值暂时不用关心,只需要知道他被赋值为 classComponentUpdater

classComponentUpdaterrender 流程里面的 ReactDOM.renderscheduleRootUpdate 非常的相似,其实他们就是同一个更新原理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
const classComponentUpdater = {
// ......
enqueueSetState(inst, payload, callback) {
// inst 就是我们调用this.setState的this,也就是classComponent实例
// 获取到当前实例上的fiber
const fiber = ReactInstanceMap.get(inst);
const currentTime = requestCurrentTime();
// 计算当前fiber的到期时间(优先级)
const expirationTime = computeExpirationForFiber(currentTime, fiber);

// 创建更新一个更新update
const update = createUpdate(expirationTime);

//payload是setState传进来的要更新的对象
update.payload = payload;

//callback就是setState({},()=>{})的回调函数
if (callback !== undefined && callback !== null) {
update.callback = callback;
}

// 把更新放到队列UpdateQueue
enqueueUpdate(fiber, update);

// 开始进入React异步渲染的核心:React Scheduler
scheduleWork(fiber, expirationTime);
},
// ......
}

状态更新都会创建一个保存更新状态相关内容的对象 Update。在 render 阶段的 beginWork 中会根据 Update 计算新的 state

这里讲这个 Update 通过 enqueueUpdate 放到队列 UpdateQueue

3. requestWork

scheduleWork 里会执行 requestWork 方法。

requestWork 中可以看到有多个 if 判断,这里就是 setState 在不同的场景使用会出现同步和异步的根本原因

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function requestWork(root, expirationTime) {
// 将根节点添加到调度任务中
addRootToSchedule(root, expirationTime)

// isRendering是全局变量,在后面生命周期函数我们会具体分析到
if (isRendering) {
return;
}

// isBatchingUpdates、isUnbatchingUpdates是全局变量
// react事件时有对他们进行重新赋值
if (isBatchingUpdates) {
if (isUnbatchingUpdates) {
....
performWorkOnRoot(root, Sync, false);
}
return;
}

if (expirationTime === Sync) {
performSyncWork();
} else {
scheduleCallbackWithExpirationTime(root, expirationTime);
}
}

场景1 - 合成事件

React有着一套自己的合成事件机制,在一个事件调用的时候会经过一些处理,这里不详细描述,最重要的一个函数就是 interactiveUpdates$1,在执行一个事件的时候会先调用这个函数。

1
2
3
4
5
6
7
8
9
10
handleClick(){
this.setState({
name: '吴彦祖'
})
console.log(this.state.name) // 狗蛋
this.setState({
age: '18'
})
console.log(this.state.age) // 40
}

interactiveUpdates$1方法

  • isBatchingUpdates = true;让setState不马上更新
  • try finally 语句;先执行一个事件里的代码最后才更新
  • isBatchingUpdates = previousIsBatchingUpdates;合成事件里setTimeout能马上更新的原因
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function interactiveUpdates$1(fn, a, b) {
if (isBatchingInteractiveUpdates) {
return fn(a, b);
}

if (!isBatchingUpdates && !isRendering && lowestPendingInteractiveExpirationTime !== NoWork) {
// Synchronously flush pending interactive updates.
performWork(lowestPendingInteractiveExpirationTime, false, null);
lowestPendingInteractiveExpirationTime = NoWork;
}
var previousIsBatchingInteractiveUpdates = isBatchingInteractiveUpdates;
var previousIsBatchingUpdates = isBatchingUpdates;
isBatchingInteractiveUpdates = true;
isBatchingUpdates = true; // 把requestWork中的isBatchingUpdates标识改为true
try {
return fn(a, b);
} finally {
isBatchingInteractiveUpdates = previousIsBatchingInteractiveUpdates;
isBatchingUpdates = previousIsBatchingUpdates;
if (!isBatchingUpdates && !isRendering) {
performSyncWork();
}
}
}

isBatchingUpdates变量

interactiveUpdates$1 这个方法中把 isBatchingUpdates 设为了 true, 导致在 requestWork 方法中, isBatchingUpdatestrue ,但是 isUnbatchingUpdatesfalse ,而被直接 return 了。

1
2
3
4
5
6
7
8
//requestWork
if (isBatchingUpdates) {
if (isUnbatchingUpdates) {
// ....
performWorkOnRoot(root, Sync, false);
}
return;
}

这就导致了 requestWork 根本没有执行到任何更新的函数,比如 performSyncWork,但在最开始的 enqueueSetState 这个方法里还是已经将每一次更新都存到了一个 update 队列里。

所以合成事件里的 setState 不会马上更新,而是存入了一个更新队列里(enqueueUpdate)

try finally

interactiveUpdates$1 最后执行了一个 try finally 语法,会先执行 try 代码块中的语句,然后再执行 finally 中的代码,而 fn(a, b) 是在 try 代码块中执行相关的事件回调,而在 finally 里才有 performSyncWork();

也就是说我们写的事件监听函数在 try 中执行,但更新在 finally 里,这就导致了所谓的”异步”,state 并没有马上更新并渲染到UI上,而是等到事件执行完之后才更新的。

1
2
3
4
5
6
7
8
9
try {
return fn(a, b);
} finally {
isBatchingInteractiveUpdates = previousIsBatchingInteractiveUpdates;
isBatchingUpdates = previousIsBatchingUpdates;
if (!isBatchingUpdates && !isRendering) {
performSyncWork();
}
}

场景2 - setTimeout

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class App extends Component {

state = { val: 0 }

componentDidMount() {
setTimeout(_ => {
this.setState({ val: this.state.val + 1 })
console.log(this.state.val) // 输出更新后的值 --> 1
}, 0)
}

render() {
return (
<div>
{`Counter is: ${this.state.val}`}
</div>
)
}
}

try 代码块执行到 setTimeout 的时候,这是一个宏任务,把它丢到列队里,并没有去执行,而是先执行的 finally 代码块。

finally 执行的时候会执行 isBatchingUpdates = previousIsBatchingUpdates; 将 isBatchingUpdates 重置为了 false

导致最后下次事件循环的时候去执行队列里的 setState 时候, requestWork 走的是和原生事件一样的 expirationTime === Sync if分支, 可以同步拿到最新的 state 值。

场景3 - 生命周期函数中的setState

三个全局变量:isRendering、isWorking、isCommitting

  • isRendering:开始react更新就为true
  • isWorking:进入reconciler阶段就为true、进入commit阶段就为true
  • isCommitting:进入commit阶段就为true

render前生命周期属于reconciler阶段:isRendering = true、isWorking = true Fiber Reconciler 的执行阶段:

  • 阶段一是生成 Fiber 树的渐进阶段,可以被打断。
  • 阶段二是批量更新节点的阶段,不可被打断。

alt text

现在回过头来看 requestWork 里的第一个if判断:

1
2
3
4
5
6
7
function requestWork(){
// ...
if (isRendering) {
return
}
// ...
}

和合成事件一样,当 componentDidmount 执行的时候,isRenderingtrue,react内部并没有更新就先 return 了,执行完 componentDidmount 后才去 commitUpdateQueue 更新。这就导致你在 componentDidmountsetState 完去 console.log 拿的结果还是更新前的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class App extends Component {
state = { val: 0 }

componentDidMount() {
this.setState({ val: this.state.val + 1 })
console.log(this.state.val) // 输出的还是更新前的值 --> 0
}
render() {
return (
<div>
{`Counter is: ${this.state.val}`}
</div>
)
}
}

场景4 - 原生事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class App extends Component {

state = { val: 0 }

changeValue = () => {
this.setState({ val: this.state.val + 1 })
console.log(this.state.val) // 输出的是更新后的值 --> 1
}

componentDidMount() {
document.body.addEventListener('click', this.changeValue, false)
}

render() {
return (
<div>
{`Counter is: ${this.state.val}`}
</div>
)
}
}

原生事件的调用栈就比较简单了,因为没有走合成事件的那一大堆,直接触发 click 事件,到 requestWork ,在 requestWork 里由于 expirationTime === Sync 的原因,直接走了 performSyncWork 去更新,并不像合成事件或钩子函数中被 return,所以当你在原生事件中 setState后,能同步拿到更新后的 state 值。

场景5 - setState批量更新的情况

简单分析源码

React 加入 fiber 架构后,调度之前通过 enqueueUpdate 函数维护的 UpdateQueue 就是挂载在组件对应的 fiber 节点上,我们更新的通过调度最后会进入到 updateClassComponent 方法,里面最终会调用一个getStateFromUpdate 来获取最终的 state 状态。

getStateFromUpdate 函数外面是对 UpdateQueue 队列的一个 while 循环,比如我们连续 setState 三次,那每次都会创建一个 update 实例通过 enqueueUpdate 放入 fiberUpdateQueue 中,这里就是把这三次的state 合并计算出一个最终的值以提高性能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
while (update !== null) {
// ...

/**
* resultState作为参数prevState传入getStateFromUpdate,然后getStateFromUpdate会合并生成
* 新的状态再次赋值给resultState。完成整个循环遍历,resultState即为最终要更新的state。
*/
resultState = getStateFromUpdate(
workInProgress,
queue,
update,
resultState,
props,
instance,
);
// ...

// 遍历下一个update对象
update = update.next;
}

getStateFromUpdate 函数主要功能是将存储在更新对象 update 上的 partialState 与上一次的 prevState 进行对象合并,生成一个全新的状态 state

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
function getStateFromUpdate<State>(
workInProgress: Fiber,
queue: UpdateQueue<State>,
update: Update<State>,
prevState: State,
nextProps: any,
instance: any,
): any {
switch (update.tag) {

// ...

// 调用setState会创建update对象,其属性tag当时被标记为UpdateState
case UpdateState: {
// payload 存放的是要更新的状态state
const payload = update.payload;
let partialState;

// 获取要更新的状态
if (typeof payload === 'function') {
partialState = payload.call(instance, prevState, nextProps);
} else {
partialState = payload;
}

// partialState 为null 或者 undefined,则视为未操作,返回上次状态
if (partialState === null || partialState === undefined) {
return prevState;
}

// 注意:此处通过Object.assign生成一个全新的状态state, state的引用地址发生了变化。
return Object.assign({}, prevState, partialState);
}

// ...
}

return prevState;
}
setState传入对象会合并对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class IncrementByObject extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
this.increment = this.increment.bind(this);
}
increment() {
// 会批量更新,只会render一次,结果是 1
this.setState({
count: this.state.count + 1
});

this.setState({
count: this.state.count + 1
});

this.setState({
count: this.state.count + 1
});
}
render() {
return (
<div>
<button onClick={this.increment}>IncrementByObject</button>
<span>{this.state.count}</span>
</div>
);
}
}

如果是一个 Object ,直接看最后的 Object.assign({}, prevState, partialState);

Object.assign 的作用: 主要是将所有可枚举属性的值从一个或多个源对象复制到目标对象,同时返回目标对象。如果目标对象中的属性具有相同的键,则属性将被源对象中的属性覆盖。后来的源对象的属性将类似地覆盖早先的属性。

之前提过在合成事件中或者在生命周期了 state 是不会马上刷新的,是在事件执行完后也就是 try finallyfinally 里才真正刷新,这就导致了每次 Object.assignpartialState 都是 this.state.count + 1,而 statecount 在三次 setState 的时候都不会改变都是0,所以计算过程可以简化如下:

1
2
3
Object.assign({}, {count:0}, {count:1});
Object.assign({}, {count:0}, {count:1});
Object.assign({}, {count:0}, {count:1});

很明显最终的 statecount 只会增加 1。

setState传入函数
1
2
3
4
5
6
7
8
9
10
11
12
increment() {
// 采用传入函数的方式来更新 state,会批量,只会render一次,更新但结果是 3
this.setState((prevState, props) => ({
count: prevState.count + 1
}));
this.setState((prevState, props) => ({
count: prevState.count + 1
}));
this.setState((prevState, props) => ({
count: prevState.count + 1
}));
}

如果是一个回调函数function 可以发现 if (typeof payload === 'function') 这里对传入的是否是方法做了判断,如果是方法,就执行

1
partialState = payload.call(instance, prevState, nextProps);

instance 对于类组件来说,这里保存类组件的实例在外层的 updateClassInstance函数中 const instance = workInProgress.stateNode; 赋值的。

这里其实只看 payloadprevState 就行了,payload 是我们通过 setState 传入的回调函数,返回最新的 statewhile 循环调用 getStateFromUpdate 每次传入的是 resultState,也就是说接受的 state 都是上一轮计算之后的新值,因此循环计算的过程可以简化如下:

1
2
3
Object.assign({}, {count:0}, {count:1});
Object.assign({}, {count:0}, {count:2});
Object.assign({}, {count:0}, {count:3});

可以看到最终的 statecount 为增加 3。

react18 - 自动批处理

结论

  • 执行顺序

    • 合成事件和钩子函数中:异步
    • 原生事件和setTimeout中:异步
  • 批量处理

    • 合成事件和钩子函数中:会批量处理
    • setState里面写成函数:会批量处理
    • 原生事件和setTimeout中:会批量处理

场景

具体解析可以看 《react18.2优先级和批处理场景解析》

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
export default function App() {
const [count, setCount] = useState(0)

console.log('render')

const onClick = () => {
// !情况1 页面变成1,render 1次
// setCount(count + 1)
// setCount(count + 1)
// setCount(count + 1) // 1
// !情况2 页面变成3,render 1次
// setCount((prev) => prev + 1)
// setCount((prev) => prev + 1)
// setCount((prev) => prev + 1) // 3
// !情况3 页面变成1,render 1次
// setTimeout(() => {
// setCount(count + 1)
// setCount(count + 1)
// })
// !情况4 页面变成1,render 2次
// setCount(count + 1)
// setCount(count + 1)
// setCount(count + 1)
// setTimeout(() => {
// setCount(count + 1)
// setCount(count + 1)
// })
// !情况5 页面变成2,render 1次
// setCount((prev) => {
// console.log('hook update 1')
// return prev + 1
// })
// setCount((prev) => {
// console.log('hook update 2')
// return prev + 1
// }) // 2
// !情况6 页面变成1,render 2次
// setCount((prev) => {
// console.log('hook update 1') // hook update 1
// return prev + 1
// })
// setCount((prev) => {
// console.log('hook update 2') // hook update 2
// return prev + 1
// }) // 2
// setTimeout(() => {
// setCount(count + 1)
// setCount(count + 1)
// })
// !情况7 页面变成2,render 2次
// flushSync(() => {
// setCount(count + 1)
// })
// setCount(count + 2)
// !情况8 页面变成2,render 2次
// setCount(count + 1)
// Promise.resolve().then(() => {
// setCount(count + 2)
// })
// !情况9 页面变成2,render 2次
// flushSync(() => {
// setCount(count + 1)
// })
// sleep(1000)
// setCount(count + 2)
}
return (
<>
<h1 onClick={onClick}>{count}</h1>
</>
)
}

概念

批处理:React 会尝试将同一上下文中触发的更新合并为一个更新。

就 React18 而言当说到批处理的时候,需同时具备以下三者:

  • 包括了多个react更新
  • 每个更新具有相同的优先级
  • 每个更新都是待执行

这样做的好处也显而易见:

  • 合并不必要的更新,减少更新流程调用次数
  • 状态按顺序保存下来,更新时不会出现「竞争问题」
  • 最终触发的更新是异步流程,减少浏览器掉帧可能性

1. 更新

对于 hook 有更新队列,对于 react 也有相应的更新(通常伴随着组件render),当然对浏览器还存在页面视图的更新。

当我们调用dispatch或者setState时,上述三种更新都是有涉及的。但是要特别指出的是,批处理中的更新就是指 react 的更新,包含了render,commit阶段等。在后续的批处理部分你将看到三者的差异。

如果我们看 dispatchSetState 的源码,会发现它们主要做了两件事:

  • 记录一次 hook 更新(enqueueConcurrentHookUpdate
  • 调度一次 react 更新(scheduleUpdateOnFiber

批中的更新就是指调度一次 react 更新 scheduleUpdateOnFiber

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 以下是 dispatchReducerAction 中同样包含的逻辑
// 这个函数中 fiber 和 queue 都是通过 dispatchSetState.bind 提前绑定好的,我们调用 setState 时传入的参数是 action
function dispatchSetState<S, A>(
fiber: Fiber,
queue: UpdateQueue<S, A>,
action: A,
): void {
// ...
const lane = requestUpdateLane(fiber);
const update: Update<S, A> = {
lane,
action,
hasEagerState: false,
eagerState: null,
next: (null: any),
};

if (isRenderPhaseUpdate(fiber)) {
// ...异常情形
} else {
// ...
const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);
// 首次渲染后root !== null
if (root !== null) {
const eventTime = requestEventTime();
scheduleUpdateOnFiber(root, fiber, lane, eventTime);
// ...
}
}
// ...
}

2. 优先级

在更新部分的相关源码示例中,可以看到 lane 字段,它表示的就是这次更新的优先级。只有优先级相同的多个更新才在一个批中,与之相应的就是这些更新被批处理,反之则不然。

一般而言,如果优先级没有被手动改变,那么相同场景下多次调用 setState 或者 dispatch 对应的更新优先级是相同的。

例外的情况是具有一整个序列而非单一的优先级,像 TransitionLanesRetryLanes。以 TransitionLanes 为例,它们包含了许多个优先级并不相同并且依次排列的 lane,但是在 render 场景下,这些 lane 是一起被处理的。

像下面这样的示例中的更新是不会被视为同一批的,startTransition 改变了第二个更新的优先级:

1
2
setCount(count + 1)
startTransition(() => setCount(count + 2)) // startTransition引自react

3. 待执行

待执行指的是已经调度但还未被执行。通常执行相对于调度而言是异步的。假如两个更新具有相同的优先级,那么:

  • 只要一个已执行,另一个未执行,无法批处理
  • 只要都未执行,就能批处理(一些异步场景可能带来迷惑性)

对于第一点,当我们手动调用同步执行更新的api时,后续的更新就无法与同步的更新成批,在下面的示例中,你会发现点击将带来两次render。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
export default function App() {
const [count, setCount] = useState(0)

console.log('render!')
function plus() {
flushSync(() => {
setCount(count + 1)
})
setCount(count + 2)

// 然而,这样做是可以批处理的
// flushSync(() => {
// setCount(count + 1)
// setCount(count + 2)
// })
}

return (
<div style={{ textAlign: 'center', fontSize: '42px', marginTop: '100px' }}>
<p>点击数字</p>
<span onClick={plus} onMouseEnter={plus}>
{count}
</span>
</div>
)
}

flushSync 可以使更新同步地被执行,这样一来,第二个 setCount 带来的更新与第一个 setCount 的更新无法被批处理,因为 setCount(count + 2) 调用时,第一个更新已经执行完了。

对于第二点,考虑到js事件循环带来的复杂异步特性,在一些让人意想不到的场景也能批处理,下面是一个有趣的示例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
export default function App() {
const [count, setCount] = useState(0)

console.log('render!')
function plus() {
setCount(count + 1)
Promise.resolve().then(() => {
setCount(count + 2)
})
}

return (
<div style={{ textAlign: 'center', fontSize: '42px', marginTop: '100px' }}>
<p>划入render一次,点击render两次</p>
<span onClick={plus} onMouseEnter={plus}>
{count}
</span>
</div>
)
}

click事件对应的更新优先级是被调度在微任务中的,而mouseEnter事件则是另一类。

源码分析

1. setState

setState 所做的就是:

  1. hook 更新加入更新队列
  2. 尝试调度一次 react 更新
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function dispatchSetState<S, A>(
fiber: Fiber,
queue: UpdateQueue<S, A>,
action: A,
): void {
// ...
const lane = requestUpdateLane(fiber);
const update: Update<S, A> = {
lane,
action,
hasEagerState: false,
eagerState: null,
next: (null: any),
};

if (isRenderPhaseUpdate(fiber)) {
// ...
} else {
// ...
const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);
// 首次渲染后root !== null
if (root !== null) {
const eventTime = requestEventTime();
scheduleUpdateOnFiber(root, fiber, lane, eventTime);
// ...
}
}
// ...
}

2. scheduleUpdateOnFiber

忽略一些琐碎的细节后,可以发现这个函数的核心逻辑甚至更简单:

  1. 标记一次具有某一优先级的更新(markRootUpdated)
  2. 调用ensureRootIsScheduled
1
2
3
4
5
6
7
8
9
10
11
12
export function scheduleUpdateOnFiber(
root: FiberRoot,
fiber: Fiber,
lane: Lane,
eventTime: number,
) {
// ...
markRootUpdated(root, lane, eventTime);
// ...
ensureRootIsScheduled(root, eventTime);
// ...
}

3. ensureRootIsScheduled

1
2
3
4
5
6
7
8
9
10
export function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
// ...
// 这里是多次调用不再调度微任务触发processRootScheduleInMicrotask
if (!didScheduleMicrotask) {
// ? sy
didScheduleMicrotask = true;
scheduleImmediateTask(processRootScheduleInMicrotask);
}
// ...
}

4. processRootScheduleInMicrotask

1
2
3
4
5
6
7
8
9
while (root !== null) {
// ...
const nextLanes = scheduleTaskForRootDuringMicrotask(root, currentTime);
// ...
}

// 在microtask结束时,flush任何pending的同步work。这必须放在最后,因为它执行实际的可能会抛出异常的渲染工作。
// onClick count
flushSyncWorkOnAllRoots();

5. scheduleTaskForRootDuringMicrotask

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 获取当前所有优先级中最高的优先级
const nextLanes = getNextLanes(
root,
root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,
);

// ...

if (includesSyncLane(nextLanes)) {
// 同步工作始终在微任务结束时刷新,因此我们不需要安排额外的任务。
if (existingCallbackNode !== null) {
cancelCallback(existingCallbackNode);
}
root.callbackPriority = SyncLane;
root.callbackNode = null;
return SyncLane;
} else {
// 本次要调度的优先级
const newCallbackPriority = getHighestPriorityLane(nextLanes);
// 已经存在的调度的优先级
const existingCallbackPriority = root.callbackPriority;

if (existingCallbackPriority === newCallbackPriority) {
// 这里就是同等优先级做批处理
// ...
return;
}
// ... 高优先级打断低优先级

// 调度更新流程
newCallbackNode = scheduleCallback(schedulerPriorityLevel, performConcurrentWorkOnRoot.bind(null, root));

// ...实际的调度,最后会给root.callbackPriority赋值
root.callbackPriority = newCallbackPriority;
root.callbackNode = newCallbackNode;

}

关于批处理有三点:

  1. 多次调用 setState 时候,在 ensureRootIsScheduled 中通过 didScheduleMicrotask 标记,第一次进入标记为 true,再次进入便不再调度微任务触发 processRootScheduleInMicrotask
  2. 当触发后续微任务触发 processRootScheduleInMicrotask 方法,通过 getNextLanesgetHighestPriorityLane 拿到本次应该(不一定是setState时的那个)更新的优先级 newCallbackPriority
  3. 如果是同步优先级,直接return,因为processRootScheduleInMicrotask 方法最后会调用 flushSyncWorkOnAllRoots 执行一遍同步任务, 否则对比上次等待的更新和本次更新的优先级,即 existingCallbackPriority === newCallbackPriority,如果相等,则提前return,否则通过 scheduleCallback 调度更新流程

1. 加载过程

执行函数组件 Fiber 节点的 beginWork 工作,根据 tag 类型,进入 IndeterminateComponent 待定组件的逻辑处理【case IndeterminateComponent】:

每个函数组件的首次加载都是走的 IndeterminateComponent 分支逻辑,这是因为在创建函数组件 Fiber 的时候,react 没有更新它的 tag 值,所以它的首次 beginWork 工作就会进入 IndeterminateComponent 分支,在mountIndeterminateComponent 方法中才会更新它的 tag,使函数组件的Fiber在更新阶段执行 beginWork 时,能够进入正确的 FunctionComponent 分支。

1.1 mountIndeterminateComponent

packages\react-reconciler\src\ReactFiberBeginWork.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
function mountIndeterminateComponent(
_current,
workInProgress,
Component,
renderLanes,
) {

// 取出函数组件的props
const props = workInProgress.pendingProps;

// 存储FirstChild内容
let value;
let hasId;

# 调用函数组件
value = renderWithHooks(
null,
workInProgress,
Component,
props,
context,
renderLanes,
);

// 针对类组件和函数组件进行不同的处理
if (
!disableModulePatternComponents &&
typeof value === 'object' &&
value !== null &&
typeof value.render === 'function' &&
value.$$typeof === undefined
) {
# 类组件的处理逻辑 【只是类组件现在已经不走这里了】

} else {

# 函数组件处理
// 更新tag为函数组件类型的值,下个逻辑就可以直接进入函数组件的处理【节点更新的时候】
workInProgress.tag = FunctionComponent;

# 创建子节点
reconcileChildren(null, workInProgress, value, renderLanes);

return workInProgress.child;
}
}

首先取出当前函数组件FIber节点上的最新的props,方便函数组件加载的使用,然后调用renderWithHooks方法,这个方法会执行我们定义的函数组件,返回值就是函数中return的内容,也就是jsx内容【处理过后的react-element元素对象】。

1.1.1 renderWithHooks

packages\react-reconciler\src\ReactFiberHooks.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
export function renderWithHooks<Props, SecondArg>(
current: Fiber | null,
workInProgress: Fiber,
Component: (p: Props, arg: SecondArg) => any,
props: Props,
secondArg: SecondArg,
nextRenderLanes: Lanes,
): any {
renderLanes = nextRenderLanes;
# 设置为当前渲染中的Fiber
currentlyRenderingFiber = workInProgress;

# 重置函数组件节点的数据
workInProgress.memoizedState = null;
workInProgress.updateQueue = null;
workInProgress.lanes = NoLanes;

// 设置首次加载的dispatcher【重点】
ReactCurrentDispatcher.current = current === null || current.memoizedState === null
? HooksDispatcherOnMount
: HooksDispatcherOnUpdate;

// Component为workInProgress.type 如果是函数组件:就是自身函数
# 调用这个函数,即调用组件,循环生成Element对象,
// 将return返回的Jsx内容转换为reactElement对象,最后返回这个对象
let children = Component(props, secondArg);
// 重置一些信息
renderLanes = NoLanes;
currentlyRenderingFiber = (null: any);

currentHook = null;
workInProgressHook = null;
didScheduleRenderPhaseUpdate = false;

# 返回函数组件的内容【reactElement对象】
return children;
}
1.1.1.1 ReactCurrentDispatcher

ReactCurrentDispatcher 对象是一个全局变量,它是在react源码中的react包定义的:

packages\react\src\ReactCurrentDispatcher.js

1
2
3
4
5
const ReactCurrentDispatcher = {
current: (null: null | Dispatcher),
};

export default ReactCurrentDispatcher;

然后将它包装在一个新的对象中:

packages\react\src\ReactSharedInternalsClient.js

1
2
3
4
5
6
7
8
const ReactSharedInternals = {
ReactCurrentDispatcher,
ReactCurrentCache,
ReactCurrentBatchConfig,
ReactCurrentOwner,
};

export default ReactSharedInternals;

而shared包【通用工具包】会引入这个对象,然后暴露给全局:

packages\shared\ReactSharedInternals.js

1
2
3
import ReactSharedInternals from '../react/src/ReactSharedInternalsClient'

export default ReactSharedInternals;

其他资源包就可以通过shared工具包来拿到这个对象,所以我们在函数组件加载时才能使用这个对象:

packages\react-reconciler\src\ReactFiberHooks.js

1
2
3
import ReactSharedInternals from 'shared/ReactSharedInternals';
// 拿到ReactCurrentDispatcher对象
const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals;

知道了 ReactCurrentDispatcher 对象的由来,我们才能更好地理解它的作用,因为函数组件的每个 hook 实际就是在调用这个对象中的同名方法,比如 useState

packages\react\src\ReactHooks.js

1
2
3
4
export function useState(initialState){
const dispatcher = resolveDispatcher();
return dispatcher.useState(initialState);
}

查看 resolveDispatcher:

1
2
3
4
5
function resolveDispatcher() {
const dispatcher = ReactCurrentDispatcher.current;
// 返回获取到的dispatcher
return dispatcher;
}

其他的 hook 也是一样的原理。

1.1.1.2 函数组件调用

函数组件的加载核心就是执行一次函数的内容,理解起来也很简单。最后触发 return 关键字,这里的 jsx 内容会在 react 内部通过 jsxRuntime.jsx 方法进行处理,生成 react-element 对象,最后返回值就是创建的 react 元素对象。

最后返回生成的 react-element 对象,renderWithHooks 方法执行完成。

函数组件初始化执行完成后,就会更新函数组件Fiber节点的tag值为正确的类型FunctionComponent【后续逻辑函数组件节点便可以进入Function分支了】。

然后根据新建的value【react元素对象】创建子Fiber节点,最后返回子节点,函数组件的加载过程就基本完成了。

1.2 hooks的加载

根据上文得知,我们查看先前的 ReactCurrentDispatcher 对象

1
ReactCurrentDispatcher.current = HooksDispatcherOnMount

packages\react-reconciler\src\ReactFiberHooks.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const HooksDispatcherOnMount: Dispatcher = {
readContext,

useCallback: mountCallback,
useContext: readContext,
useEffect: mountEffect,
useImperativeHandle: mountImperativeHandle,
useLayoutEffect: mountLayoutEffect,
useInsertionEffect: mountInsertionEffect,
useMemo: mountMemo,
useReducer: mountReducer,
useRef: mountRef,
useState: mountState, // 加载state
useDebugValue: mountDebugValue,
useDeferredValue: mountDeferredValue,
useTransition: mountTransition,
useMutableSource: mountMutableSource,
useSyncExternalStore: mountSyncExternalStore,
useId: mountId,

unstable_isNewReconciler: enableNewReconciler,
};

例如我们在函数中依次使用useState、useEffect、useLayoutEffect,我们来看一下执行过程:

1.2.1 mountState

packages\react-reconciler\src\ReactFiberHooks.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function mountState(initialState) {
# hook加载工作
const hook = mountWorkInProgressHook();
if (typeof initialState === 'function') {
initialState = initialState();
}
hook.memoizedState = hook.baseState = initialState;
const queue = {
pending: null, // 等待处理的update链表
lanes: NoLanes,
dispatch: null, // dispatchSetState方法
lastRenderedReducer: basicStateReducer, // 一个函数,通过action和lastRenderedState计算最新的state
lastRenderedState: initialState, // 上一次的state
};
hook.queue = queue;
const dispatch = queue.dispatch = dispatchSetState.bind(null, currentlyRenderingFiber, queue)
# 返回值
return [hook.memoizedState, dispatch];
}
1.2.1.1 mountWorkInProgressHook
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function mountWorkInProgressHook(): Hook {
# hook对象
const hook: Hook = {
memoizedState: null,
baseState: null,
baseQueue: null,
queue: null,
next: null,
};

if (workInProgressHook === null) {
// 第一个hook
currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
} else {
# 后面的hook对象添加到第一个hook的next属性上,形成一个单向链表
workInProgressHook = workInProgressHook.next = hook;
}
return workInProgressHook;
}

首先创建一个 hook 对象,workInProgressHook 默认为 null,它代表当前正在处理中的 hook 对象。

当前 useState 为函数组件中的第一个调用的 hook ,所以这时 workInProgressHook 肯定为 null

将新建 hook 对象赋值给 workInProgressHook,表示为正在处理中的 hook 对象。

同时也将第一个 hook 对象赋值给当前函数组件 Fiber 节点的 memoizedState 属性。

此时函数组件 Fiber 节点的 memoizedState 属性指向为:

alt text

继续回到 mountState 方法中:

hook 新建完成之后,判断传入的参数 initialState 是否为函数,如果为函数则调用此函数,将结果赋值为新的 initialState

然后设置 hook 对象的 memoizedStatebaseState 属性为初始的数据 initialState

接下来创建一个queue对象,这里要注意两个属性:

  1. lastRenderedReducer:它是一个函数,作用是根据action和lastRenderedState计算最新的state。
1
2
3
4
5
function basicStateReducer(state, action) {
// action就是setCount传入的参数,如果为一个函数,则将state传入进行计算,返回新的state
// 如果不是函数,则action就是最新的state
return typeof action === 'function' ? action(state) : action;
}
  1. lastRenderedState:代表上一次渲染的state

然后更新 hook 对象的 queue 属性,同时设置 queue 对象的 dispatch 属性为一个修改函数 dispatchSetState

最后返回一个数组,这就是 useState hook 的返回值:一个初始state和一个修改函数。

1.2.2 mountEffect

packages\react-reconciler\src\ReactFiberHooks.js

1
2
3
4
5
6
7
8
9
10
11
12
function mountEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
// 进入effect加载
return mountEffectImpl(
PassiveEffect | PassiveStaticEffect, # Passive标记对应的是useEffect
HookPassive,
create,
deps,
);
}
1.2.2.1 mountEffectImpl

packages\react-reconciler\src\ReactFiberHooks.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function mountEffectImpl(fiberFlags, hookFlags, create, deps): void {
# 创建的新的hook对象
const hook = mountWorkInProgressHook();
// 确定当前hook的deps依赖
const nextDeps = deps === undefined ? null : deps;
// 当前渲染中的Fiber节点,即函数组件对应的,打上effect钩子的flags
currentlyRenderingFiber.flags |= fiberFlags;
// 设置hook的memoizedState属性
hook.memoizedState = pushEffect(
HookHasEffect | hookFlags,
create,
undefined,
nextDeps,
);
}

依然是先调用 mountWorkInProgressHook 创建一个 hook 对象,所以这里是先将第一个 hook 对象的 next 属性指向新建的 hook,然后再更新 workInProgressHook 的值为当前的 hook 对象。

此时函数组件Fiber节点的memoizedState属性指向为:

alt text

hook 创建完成之后,确定当前 hook 对象的 deps 依赖,因为我们传递的依赖为[],所以此时 deps 为一个空数组。然后更新当前 Fiber 节点的 flags 标记,最后设置 hook 对象的 memoizedState 属性内容,这里属性的结果为pushEffect 方法调用的返回值

1.2.2.2 pushEffect
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
function pushEffect(tag, create, destroy, deps) {
# 创建副作用对象
const effect = {
tag,
create, // 回调函数
destroy, // 销毁函数
deps,
// Circular
next: null,
};
// 取出当前函数组件的UpdateQueue
let componentUpdateQueue = currentlyRenderingFiber.updateQueue;
if (componentUpdateQueue === null) {
// 为null时: 创建当前函数组件的UpdateQueue
componentUpdateQueue = createFunctionComponentUpdateQueue();
currentlyRenderingFiber.updateQueue = componentUpdateQueue;
// 第一个effect对象: 它的next属性会执行自己,形成一个单向环状链表
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
// 第二次加载其他的effect时: 将
const lastEffect = componentUpdateQueue.lastEffect;
if (lastEffect === null) {
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
const firstEffect = lastEffect.next;
# 上一个effect的next属性指向新建的effect
lastEffect.next = effect;
# 新建的next属性指向第一个effect
effect.next = firstEffect;
componentUpdateQueue.lastEffect = effect;
}
}
return effect;
}

首先创建了一个effect对象,查看它的内容:

alt text

  1. create 属性即为我们传入的回调函数。
  2. deps 属性是当前 useEffect hook的依赖。
  3. destory 属性为 undefined,它存储的是 useEffect hook返回的clean清理函数或者说销毁函数,但是它不是在这里赋值的,并且当前我们也没有返回这个函数。

然后取出当前函数组件 Fiber 节点的 updateQueue 属性内容赋值给变量 componentUpdateQueue

然后判断 componentUpdateQueue 是否为 null

null 时,然后调用 createFunctionComponentUpdateQueue 方法更新它的值。

1
2
3
4
5
6
function createFunctionComponentUpdateQueue(): FunctionComponentUpdateQueue {
return {
lastEffect: null,
stores: null,
};
}

最后将当前创建的 effect 对象的 next 属性指向了自身,且同时更新 updateQueue.lastEffect 属性为当前 effect 对象,由此形成一个单向环状链表。

所以此时函数组件 Fiber 节点的 updateQueue 属性更新为:

alt text

pushEffect 方法最后,返回当前创建的 effect 对象。

再回到 mountEffectImpl 中:

1
hook.memoizedState = pushEffect()

所以 hook 对象的 memoizedState 属性值为一个 effect 对象。

从这里我们可以发现,虽然每个hook对象都是相同的属性,但是不同的 hook 类型它存储的内容却完全不同。

  • useState 创建的 hook 对象,它的 memoizedState 属性存储的为数据 state
  • useEffect 创建的 hook 对象,它的 memoizedState 属性存储的为一个 effect 对象。

1.2.3 mountLayoutEffect

packages\react-reconciler\src\ReactFiberHooks.js

1
2
3
4
5
6
7
8
9
10
function mountLayoutEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
let fiberFlags: Flags = UpdateEffect;
if (enableSuspenseLayoutEffectSemantics) {
fiberFlags |= LayoutStaticEffect;
}
return mountEffectImpl(fiberFlags, HookLayout, create, deps);
}

可以发现useEffect和useLayoutEffect共用了同一个加载方法 mountEffectImpl,所以它们会执行同样的逻辑处理。

hook 对象创建和处理,此时函数组件Fiber节点的memoizedState属性指向更新为:

alt text

effect 对象创建和处理,依然是 pushEffect 方法的调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
if (componentUpdateQueue === null) {
...
} else {
# 第二次加载其他的effect时:
const lastEffect = componentUpdateQueue.lastEffect;
if (lastEffect === null) {
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
const firstEffect = lastEffect.next;
// 上一个effect的next属性指向新建的effect
lastEffect.next = effect;
// 新建的next属性指向第一个effect
effect.next = firstEffect;
componentUpdateQueue.lastEffect = effect;
}
}

当前为第二个effect相关的 hook 处理,所以此时 Fiber.updateQueue【即componentUpdateQueue】是有值的,进入else分支处理。

更新Fiber.updateQueue.lastEffect属性指向为当前新建的 effect2,将 effect2next 属性指向为之前的 effect 对象。

此时函数组件 Fiber 节点的 updateQueue 属性指向更新为:

alt text

到此,函数组件加载阶段的 hooks 就处理完成。

1.3 commit阶段

前面全部的加载逻辑都是在 Fiber Reconciler 协调流程中执行的,即函数组件大部分的加载逻辑都是在 reconciler 协调流程中完成的【更新阶段同理】,还有剩下的一部分逻辑在 commit 阶段之中处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function commitRootImpl() {
// 发起调度处理useEffect回调
scheduleCallback(NormalSchedulerPriority, () => {
// 这个回调就是处理useEffect的
flushPassiveEffects();
});

// 1,BeforeMutation阶段
commitBeforeMutationEffects()
// 2,Mutation阶段,渲染真实DOM加载到页面
commitMutationEffects()
// 3,Layout阶段
commitLayoutEffects()
}

commit 阶段的内容都是同步执行,在进入具体的执行之前,都会先调用 scheduleCallback 方法发起一个新的调度,即创建一个新的任务 task,最后会生成一个新的宏任务来异步处理副作用【即执行useEffect的回调钩子】。

上面是 useEffect 的回调处理,我们再查看 useLayoutEffect 的回调处理。

1.4 Layout阶段

1
2
3
4
5
6
7
8
9
10
11
12
13
function commitLayoutEffectOnFiber() {
if ((finishedWork.flags & LayoutMask) !== NoFlags) {
// 根据组件类型
switch (finishedWork.tag) {
// 函数组件的处理
case FunctionComponent: {
// 传入的是layout相关的flag标记
commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork);
}
// ...
}
}
}

1.4.1 commitHookEffectListMount

packages/react-reconciler/src/ReactFiberCommitWork.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function commitHookEffectListMount(flags: HookFlags, finishedWork: Fiber) {
# 当前函数组件的updateQueue属性,存储的是副作用链表
const updateQueue = finishedWork.updateQueue;
// 取出最后一个effect对象
const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
if (lastEffect !== null) {
// 获取第一个effect对象
const firstEffect = lastEffect.next;
let effect = firstEffect;
// 开始循环处理
do {
if ((effect.tag & flags) === flags) {
// Mount
const create = effect.create;
# 执行回调函数
effect.destroy = create();
}
effect = effect.next;
} while (effect !== firstEffect);
}
}

首先从当前函数组件 Fiber 节点取出它的 updateQueue 属性内容,在前面我们已经知道了 Fiber.updateQueue 存储的是副作用相关的链表,定义一个 lastEffect 变量存储 updateQueue.lastEffect 的内容,即最后一个 effect 对象。

判断 lastEffect 是否为 null ,如果 lastEffectnull,代表当前函数组件没有使用过 effect 相关的 hook

当前肯定是有值的,继续向下执行。从 lastEffect.next 中取出第一个 effect 对象,开始按顺序循环处理副作用。

1
2
3
4
5
6
7
8
9
do {
if ((effect.tag & flags) === flags) {
// Mount
const create = effect.create;
// 执行回调函数
effect.destroy = create();
}
effect = effect.next;
} while (effect !== firstEffect);

注意在执行之前有一个条件判断,只有存在 effect 相关的 flags 标记才会执行对应副作用回调。

而在之前 hook 加载是有进行设置的:

1
2
3
4
hook.memoizedState = pushEffect(
HookHasEffect | hookFlags, // HookHasEffect标记就是表示有需要执行副作用
// ...
)

在函数组件加载阶段时,每个 useEffectuseLayoutEffect 都有打上 HookHasEffect 的标记,表示在加载阶段都会默认执行一次。

需要注意的是:之前 commitHookEffectListMount 传入的是与 Layout 相关的 flags 标记。

1
commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork); // Layout

所以这里只有 layout hook 的回调才能执行,第一个 effect 对象对应的是 useEffect,不满足判断条件

从当前 effect 对象的 next 属性取出下一个 effect 对象,开始第二次循环。

第二个 effect 对象对应的是 useLayoutEffect,满足判断条件,执行它的回调函数。

1
2
3
const create = effect.create;
// 执行回调函数
effect.destroy = create();

到此 hook 相关的回调处理完成,函数组件加载逻辑全部执行完成。

2. 更新过程

2.1 dispatchSetState

当操作更新触发 setState 时, 就是触发之前 useState 加载返回的 dispatch 方法:

1
2
3
const dispatch = queue.dispatch = dispatchSetState.bind(null, currentlyRenderingFiber, queue)
# 返回值
return [hook.memoizedState, dispatch];

packages\react-reconciler\src\ReactFiberHooks.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
function dispatchSetState<S, A>(
fiber: Fiber,
queue: UpdateQueue<S, A>,
action: A, // state 1
) {

// 请求更新优先级
const lane = requestUpdateLane(fiber);
// 1. 创建update更新对象
const update: Update<S, A> = {
lane,
action, // state 1
hasEagerState: false,
eagerState: null,
next: (null: any),
};

if (isRenderPhaseUpdate(fiber)) {
enqueueRenderPhaseUpdate(queue, update);
} else {
# 调度之前的一个优化策略校验: eagerState
// 快速计算出本次最新的state,与原来的进行对比,如果没有发生变化,则跳过后续的更新逻辑
const alternate = fiber.alternate;
if (fiber.lanes === NoLanes && (alternate === null || alternate.lanes === NoLanes)) {
const lastRenderedReducer = queue.lastRenderedReducer;
if (lastRenderedReducer !== null) {
let prevDispatcher;
try {
// 当前的state,即旧的state
const currentState: S = (queue.lastRenderedState: any);
// 快速计算最新的state
const eagerState = lastRenderedReducer(currentState, action);
update.hasEagerState = true;
update.eagerState = eagerState;
if (is(eagerState, currentState)) {
# 如果state没变,组件不做更新。此处和useReducer对比下,useReducer还是会让函数组件更新
enqueueConcurrentHookUpdateAndEagerlyBailout(fiber, queue, update);
return;
}
} catch (error) {
// Suppress the error. It will throw again in the render phase.
} finally {
// nothing
}
}
}

// 2. 将更新对象入队
const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);
if (root !== null) {
const eventTime = requestEventTime();
// 3. 开启一个新的调度更新任务
scheduleUpdateOnFiber(root, fiber, lane, eventTime);
entangleTransitionUpdate(root, queue, lane);
}
}
}

2.1.1 eagerState

首先看 dispatchSetState 方法的整个结构和类组件的更新方法 enqueueSetState 基本相同,还有 react 应用的初始加载 updateContainer,其实一个react应用的更新场景就只有这三种,而它们的更新逻辑就是以下几个步骤:

  1. 获取更新优先级 lane
  2. 创建 update 更新对象 。
  3. update 更新对象添加到目标Fiber对象的更新队列中。
  4. 开启一个新的调度更新任务。

它们的区别主要在于函数组件这里在调度之前有一个eagerState优化策略校验:

1
2
3
4
5
6
7
8
9
10
11
// 当前的state,即旧的state
const currentState: S = (queue.lastRenderedState: any);
// 快速计算最新的state
const eagerState = lastRenderedReducer(currentState, action);
update.hasEagerState = true;
update.eagerState = eagerState;
// 比较新旧state
if (is(eagerState, currentState)) {
enqueueConcurrentHookUpdateAndEagerlyBailout(fiber, queue, update);
return;
}

这个优化策略的作用是:调用 queue.lastRenderedReducer 方法,通过原来的 state 和当前传入的 action 参数,快速的计算出本次最新的 state 【即eagerState】,通过比较新旧 state 来判断数据是否变化,如果没有变化则可以跳过后续的更新逻辑,即不会开启新的调度更新任务。当前我们的 state 是有变化的,所以不满足优化策略,将继续向下执行更新。

2.1.2 enqueueConcurrentHookUpdate

1
2
3
4
5
6
7
8
9
10
11
12
13
export function enqueueConcurrentHookUpdate<S, A>(
fiber: Fiber,
queue: HookQueue<S, A>,
update: HookUpdate<S, A>,
lane: Lane,
): FiberRoot | null {
const concurrentQueue: ConcurrentQueue = (queue: any);
const concurrentUpdate: ConcurrentUpdate = (update: any);
// 通用的入队方法
enqueueUpdate(fiber, concurrentQueue, concurrentUpdate, lane);
// 找到根节点并返回
return getRootForUpdatedFiber(fiber);
}

回到 dispatchSetState 方法中,最后还是会调用 scheduleUpdateOnFiber 函数进入更新的调度程序。

2.1.3 flushSyncWorkOnAllRoots

1
2
3
4
function processRootScheduleInMicrotask() {
// ...
flushSyncWorkOnAllRoots();
}

microtask 结束时,flush 任何 pending 的同步 work。这必须放在最后,因为它执行实际的可能会抛出异常的渲染工作。

直接快进到performSyncWorkOnRoot方法中:

1
2
3
4
export function performSyncWorkOnRoot(root: FiberRoot, lanes: Lanes): null {
// ...
let exitStatus = renderRootSync(root, lanes);
}

调用renderRootSync方法,开始FiberTree的创建过程。

在这之前,还有一个处理要注意:把 concurrentQueues 的内容添加到 fiberqueue

1
2
3
4
5
6
7
8
function renderRootSync() {
// ...
prepareFreshStack()
}
function prepareFreshStack() {
// ...
finishQueueingConcurrentUpdates()
}

alt text

2.1.4 updateFunctionComponent

下面进入 beginWork 工作的 FunctionComponent 处理分支,开始函数组件的更新:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function updateFunctionComponent(
current,
workInProgress,
Component,
nextProps: any,
renderLanes,
) {

let nextChildren;
# 调用函数组件
nextChildren = renderWithHooks(
current,
workInProgress,
Component,
nextProps,
context,
renderLanes,
);

# 函数组件默认的bailout策略,满足条件比较苛刻
if (current !== null && !didReceiveUpdate) {
bailoutHooks(current, workInProgress, renderLanes);
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
}

// 创建子节点
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
return workInProgress.child;
}

可以看见 updateFunctionComponent 方法主要有两个处理:

  1. 调用renderWithHooks【函数组件加载也是调用了这个方法】。
  2. 判断是否满足Bailout优化策略,满足则进入优化逻辑,跳过本组件的更新。不满足,则执行正常的组件更新逻辑。

2.1.5 renderWithHooks

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
export function renderWithHooks<Props, SecondArg>(
current: Fiber | null,
workInProgress: Fiber,
Component: (p: Props, arg: SecondArg) => any,
props: Props,
secondArg: SecondArg,
nextRenderLanes: Lanes,
): any {
renderLanes = nextRenderLanes;
// 设置为当前渲染中的Fiber
currentlyRenderingFiber = workInProgress;

// 重置函数组件节点的数据
workInProgress.memoizedState = null;
workInProgress.updateQueue = null;
workInProgress.lanes = NoLanes;

// 设置更新的dispatcher【重点】
ReactCurrentDispatcher.current =current === null || current.memoizedState === null
? HooksDispatcherOnMount
: HooksDispatcherOnUpdate;

// Component为workInProgress.type 如果是函数组件:就是自身函数
// 调用这个函数,即调用组件,循环生成Element对象,
// 将return返回的Jsx内容转换为reactElement对象,最后返回这个对象
let children = Component(props, secondArg);

renderLanes = NoLanes;
currentlyRenderingFiber = (null: any);

currentHook = null;
workInProgressHook = null;
didScheduleRenderPhaseUpdate = false;

# 返回函数组件的内容【reactElement对象】
return children;
}

在更新阶段时:

1
ReactCurrentDispatcher.current = HooksDispatcherOnUpdate

renderWithHooks 方法的重点依然是组件的调用 Component(),这里的逻辑依然只是重新调用一遍我们定义的函数,最后返回最新的jsx内容【即reactElement对象】

2.1.6 hooks的更新

首先查看 useState 的更新:

1
2
3
const HooksDispatcherOnUpdate = {
useState: updateState, // 更新state
}
1
2
3
function updateState(initialState:) {
return updateReducer(basicStateReducer, initialState);
}
1
2
3
4
5
6
function updateReducer(reducer, initialArg, init?){
// 更新hook工作
const hook = updateWorkInProgressHook();

// ...
}

2.1.7 updateWorkInProgressHook

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
function updateWorkInProgressHook(): Hook {

// 即将处理的hook
let nextCurrentHook: null | Hook;
// 第一此进入更新时,currentHook为null
if (currentHook === null) {
// 取出当前正在更新的函数组件Fiber的旧节点
const current = currentlyRenderingFiber.alternate;
// 更新阶段,current都是存在的
if (current !== null) {
// 将旧节点的memoizedState 设置为下一个处理的Hook
// 将组件加载时,初始化的hook链表取出,memoizedState指向的是hook1
nextCurrentHook = current.memoizedState;
} else {
nextCurrentHook = null;
}
} else {
// 从第二个hook更新开始,会走这里
nextCurrentHook = currentHook.next;
}

// 设置下一个工作中的Hook为null
let nextWorkInProgressHook: null | Hook;
// 组件的第一个Hook更新时,workInProgressHook为null
if (workInProgressHook === null) {
// 将当前函数组件Fiber节点的memoizedState 设置为下一个处理的hook【默认是null】
nextWorkInProgressHook = currentlyRenderingFiber.memoizedState;
} else {
// 如果不是第一个Hook,则取next指向的下一个
nextWorkInProgressHook = workInProgressHook.next;
}

// 下一个不为null, 说明当前hook不是最后一个更新的hook,只有最后一个hook更新时,nextWorkInProgressHook才为null
if (nextWorkInProgressHook !== null) {
// There's already a work-in-progress. Reuse it.
workInProgressHook = nextWorkInProgressHook;
nextWorkInProgressHook = workInProgressHook.next;

currentHook = nextCurrentHook;
} else {

if (nextCurrentHook === null) {
throw new Error('Rendered more hooks than during the previous render.');
}

// 更新currentHook 为第一个hook
currentHook = nextCurrentHook;

// 创建一个新的Hook对象,复用原来的内容
const newHook: Hook = {
memoizedState: currentHook.memoizedState,

baseState: currentHook.baseState,
baseQueue: currentHook.baseQueue,
queue: currentHook.queue,

next: null, // 但是清空了next指向
};

// 第一个hook更新时,workInProgressHook为null,会进入这里
if (workInProgressHook === null) {
// This is the first hook in the list.
// 更新当前函数的组件的memoizedState为第一个hook对象,同时设置为当前正在工作中的hook
currentlyRenderingFiber.memoizedState = workInProgressHook = newHook;
} else {
// Append to the end of the list.
// 非第一个Hook,直接添加到上一个hook对象的next属性中
workInProgressHook = workInProgressHook.next = newHook;
}
}
// 返回当前正在工作中的hook
return workInProgressHook;
}

就像函数组件的hook在加载时都会调用一个 mountWorkInProgressHook 方法,生成一个hook链表。

而函数组件的hook在更新时也会调用一个 updateWorkInProgressHook 方法,生成对应的hook链表。

所以 updateWorkInProgressHook 方法的作用是:确定当前函数 Fiber 节点的 memoizedState 属性内容,也就是生成它的 hook 链表。

它的做法就是从 current 节点上取出函数组件加载时生成的 hook 链表,按顺序取出原来的 hook 对象,根据原来的对象信息创建生成新的 newHook 对象,最后按顺序一个一个添加到新的 Fiber 节点的 memoizedState 属性上。

注意:这里是一个重点,如果没有清空 next 属性,那更新当前函数组件 Fiber 节点的 memoizedState 属性,直接拿到第一个 hook 对象,就可以拿到整个 hook 链表,然后后续的 hook 更新就不需要再调用 updateWorkInProgressHook 方法了。

但是函数组件为啥不能如此处理呢?

因为react不能保证开发者是一定按照规范来使用的 hook ,如果开发者将 hook 置于条件语句中,在更新阶段出现了原来 hook 链表中不存在的 hook 对象,则在渲染时就会发生异常,所以react在函数组件更新时需要主动中断 hook 对象的next属性指向,按原来的链表顺序重新一个一个添加,如果出现了不匹配的 hook 对象,就会主动抛出异常,提示用户:

1
2
3
if (nextCurrentHook === null) {
throw new Error('Rendered more hooks than during the previous render.');
}

2.1.8 计算state

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
function updateReducer<S, I, A>(
reducer: (S, A) => S,
initialArg: I,
init?: I => S,
): [S, Dispatch<A>] {
// 返回新的hook对象
const hook = updateWorkInProgressHook();
const queue = hook.queue;

if (queue === null) {
throw new Error(
'Should have a queue. This is likely a bug in React. Please file an issue.',
);
}

queue.lastRenderedReducer = reducer; // 还是basicStateReducer,无变化
const current = currentHook; // 旧的hook对象,加载时useState创建的hook对象
// The last rebase update that is NOT part of the base state.
let baseQueue = current.baseQueue;

// The last pending update that hasn't been processed yet.
// 等待处理的更新链表:默认指向的是最后一个update对象
const pendingQueue = queue.pending;
if (pendingQueue !== null) {
// pendingQueue不为null,代表有需要处理的更新对象,然后需要将它们添加到baseQueue
if (baseQueue !== null) {
// Merge the pending queue and the base queue.
const baseFirst = baseQueue.next;
const pendingFirst = pendingQueue.next;
baseQueue.next = pendingFirst;
pendingQueue.next = baseFirst;
}
current.baseQueue = baseQueue = pendingQueue;
queue.pending = null;
}

if (baseQueue !== null) {
// 我们有一个队列要处理
const first = baseQueue.next;
let newState = current.baseState;

let newBaseState = null;
let newBaseQueueFirst = null;
let newBaseQueueLast = null;
let update = first;

# 循环处理update更新对象
do {
// An extra OffscreenLane bit is added to updates that were made to
// a hidden tree, so that we can distinguish them from updates that were
// already there when the tree was hidden.
const updateLane = removeLanes(update.lane, OffscreenLane);
const isHiddenUpdate = updateLane !== update.lane;

// Check if this update was made while the tree was hidden. If so, then
// it's not a "base" update and we should disregard the extra base lanes
// that were added to renderLanes when we entered the Offscreen tree.
const shouldSkipUpdate = isHiddenUpdate
? !isSubsetOfLanes(getWorkInProgressRootRenderLanes(), updateLane)
: !isSubsetOfLanes(renderLanes, updateLane);

if (shouldSkipUpdate) {
// Priority is insufficient. Skip this update. If this is the first
// skipped update, the previous update/state is the new base
// update/state.
const clone: Update<S, A> = {
lane: updateLane,
action: update.action,
hasEagerState: update.hasEagerState,
eagerState: update.eagerState,
next: (null: any),
};
if (newBaseQueueLast === null) {
newBaseQueueFirst = newBaseQueueLast = clone;
newBaseState = newState;
} else {
newBaseQueueLast = newBaseQueueLast.next = clone;
}
// Update the remaining priority in the queue.
// TODO: Don't need to accumulate this. Instead, we can remove
// renderLanes from the original lanes.
currentlyRenderingFiber.lanes = mergeLanes(
currentlyRenderingFiber.lanes,
updateLane,
);
markSkippedUpdateLanes(updateLane);
} else {
// This update does have sufficient priority.

if (newBaseQueueLast !== null) {
const clone: Update<S, A> = {
// This update is going to be committed so we never want uncommit
// it. Using NoLane works because 0 is a subset of all bitmasks, so
// this will never be skipped by the check above.
lane: NoLane,
action: update.action,
hasEagerState: update.hasEagerState,
eagerState: update.eagerState,
next: (null: any),
};
newBaseQueueLast = newBaseQueueLast.next = clone;

// Process this update.
if (update.hasEagerState) {
// If this update is a state update (not a reducer) and was processed eagerly,
// we can use the eagerly computed state
newState = ((update.eagerState: any): S);
} else {
const action = update.action;
newState = reducer(newState, action);
}
}
update = update.next;
} while (update !== null && update !== first);


if (newBaseQueueLast === null) {
newBaseState = newState;
} else {
newBaseQueueLast.next = (newBaseQueueFirst: any);
}

# 【重点】如果对某一个状态多次修改的最终结果是无变化,则会设置全局变量ReceivedUpdate为false,方便后续进入Bailout策略
if (!is(newState, hook.memoizedState)) {
markWorkInProgressReceivedUpdate();
}

hook.memoizedState = newState;
hook.baseState = newBaseState;
hook.baseQueue = newBaseQueueLast;

queue.lastRenderedState = newState;
}

const dispatch: Dispatch<A> = (queue.dispatch: any);
return [hook.memoizedState, dispatch];
}

updateWorkInProgressHook 方法调用完成之后,返回值就是 useState 对应的hook对象:

取出hook对象的queue队列,如果queue为null,则会抛出错误:

1
2
3
4
5
if (queue === null) {
throw new Error(
'Should have a queue. This is likely a bug in React. Please file an issue.',
);
}

后面的逻辑看似比较多,但其实比较简单,而且和this.setState计算state的逻辑基本一致。

它的核心逻辑:按顺序正向循环 update 更新队列,定义一个变量 newState 来存储最新的 state,然后根据原来 stateupdate 对象里面的信息计算最新的数据更新变量 newState,每循环一次就会从 update 对象的 next 属性取出下一个参与计算的 update,直接到所有的 update 处理完成。

当前 pendingQueue 结构【单向环状链表】:

alt text

在类组件中,会根据pendingQueue的内容重构生成一个新的单向链表,不再是环状,有明确的结束。

alt text

和类组件不同的是,函数组件这里并没有额外处理pendingQueue,而是直接复制给baseQueue,从baseQueue.next取出第一个update对象【即first】开始计算state。

所以函数组件这里的do while循环多了一个结束的判断条件,就是不能等于first,不然就会陷入无限循环:

1
2
3
do {
// ...
} while (update !== null && update !== first)

然后就是函数组件计算state的逻辑:

1
2
3
4
5
6
7
// do while循环中,计算state的核心逻辑
if (update.hasEagerState) {
newState = ((update.eagerState: any): S);
} else {
const action = update.action;
newState = reducer(newState, action);
}

如果eagerState存在,则直接使用eagerState的值为新的state。

如果不存在,则调用reducer【basicStateReducer】,根据最新的newState和当前update对象的action重新计算state。

循环结束,在更新state之前,还有一个校验需要注意:

1
2
3
4
# 【重点】如果对某一个状态多次修改的最终结果是无变化,则会设置全局变量ReceivedUpdate为false,方便后续进入Bailout策略
if (!is(newState, hook.memoizedState)) {
markWorkInProgressReceivedUpdate();
}

在针对一个状态的批量处理之后,有一个状态变化的校验,针对的是函数组件内部的 Bailout 策略。

即如果对某一个状态多次修改的最终结果是无变化,则会设置全局变量 ReceivedUpdatefalse,表示改组件没有更新的内容,这样就可以在 renderWithHooks 方法执行完成后,进入 Bailout 策略。

然后更新 hook 对象的 memoizedState 属性为最新的 newState

1
2
// 存储最新的state
hook.memoizedState = newState;

到此,useState hook 的更新程序执行完成,最后返回结果:

1
2
// 记忆state
return [hook.memoizedState, dispatch];

同时这里我们也可以明白:函数组件 useState hook 能够缓存变量结果的原因,因为它的 state 存储在 hook 对象的属性之中,并且这个属性可以在函数组件重新渲染过程中得到更新。

2.2 updateEffect

1
2
3
4
5
6
function updateEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
return updateEffectImpl(PassiveEffect, HookPassive, create, deps);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
function updateEffectImpl(fiberFlags, hookFlags, create, deps): void {
const hook = updateWorkInProgressHook();
// 取出新的依赖
const nextDeps = deps === undefined ? null : deps;
// 重置销毁方法
let destroy = undefined;

if (currentHook !== null) {
// 原来的pushEffect方法
const prevEffect = currentHook.memoizedState;
// 继承原来的destroy方法
destroy = prevEffect.destroy;
if (nextDeps !== null) {
const prevDeps = prevEffect.deps;
if (areHookInputsEqual(nextDeps, prevDeps)) {
hook.memoizedState = pushEffect(hookFlags, create, destroy, nextDeps);
return;
}
}
}

// 上面校验为true的情况下,这里就不会再执行
currentlyRenderingFiber.flags |= fiberFlags;

hook.memoizedState = pushEffect(
HookHasEffect | hookFlags,
create,
destroy,
nextDeps,
);
}

首先依然是调用一个 updateWorkInProgressHook 方法,前面已经详细讲解了它的作用。所以这里调用此方法后,就会新建一个 newHook 对象,添加到第一个 hook 对象的 next 属性之上,形成一个链表,后续如果还有新的 newHook 对象则继续执行同样的逻辑。

此时函数 Fiber 节点的 memoizedState 属性内容为:

alt text

2.2.1 areHookInputsEqual

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function areHookInputsEqual(
nextDeps: Array<mixed>,
prevDeps: Array<mixed> | null,
) {

// 情况1,无依赖参数,每次渲染都会执行副作用
if (prevDeps === null) {
return false;
}

// 情况2,有至少一项依赖参数,循环判断每个依赖是否相等,任何一个依赖变化则会重新执行副作用
for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
if (is(nextDeps[i], prevDeps[i])) {
continue;
}
return false;
}
// 情况3,即空数组的情况,重新渲染不执行副作用
return true;
}

当前我们依赖为一个空数组,所以满足第三种情况,直接返回 true

  • 在依赖校验为 true 的情况下,即表示没有变化,此时更新 hook.memoizedState 属性,最后触发 return 关键字,updateEffect 方法执行完成。

  • 在依赖变化时,也会重新设置 hook.memoizedState 属性,依赖校验为true的情况下就是第一个参数不同,会打上 HookHasEffect 的标记。

    • 这个值会存储到 effect 对象的tag属性上,表示此effect对象有需要执行的副作用回调。
    • hookFlags 表示副作用的类型标记,比如 HookPassiveHookLayout
    • 所以依赖发生变化的唯一区别就是:打上了 HookHasEffect 标记。最终会 commit 阶段中执行回调时,判断 effect.tag 的值来判断是否执行回调。

到此,函数组件的第二个hook:useEffect更新完成。

2.3 updateLayoutEffect

1
2
3
4
5
6
function updateLayoutEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
return updateEffectImpl(UpdateEffect, HookLayout, create, deps);
}

可以发现 useEffectuseLayoutEffect 共用了同一个更新方法 updateEffectImpl,所以它们会执行同样的逻辑处理。

  • 调用 updateWorkInProgressHook 方法:创建新 hook 对象,此时函数组件 Fiber 节点的 memoizedState 属性指向更新为:

alt text

  • 判断 deps 依赖是否变化,如果变化则为对应的 effect 对象打上 HookHasEffect 的标记。

到此,函数组件更新阶段的 hooks 就处理完成。

2.4 总结

函数组件更新阶段主要有这两个重点逻辑:

  • 根据 updateQueue 更新队列,循环计算 state,最后将最新的 state 数据存储到 Fiber.memoizedState 属性上并返回。
  • 更新 Effecthook 时,判断依赖是否变化打上 HookHasEffect,最后会根据 effect.tag 值来决定本次更新是否执行回调。

useEffectuseLayoutEffect的区别:useLayoutEffectuseEffect 的一个变种,它们都是在 React 组件中处理副作用的方法。两者之间的主要区别在于它们的执行时机。

  • useEffect 的回调函数会在每次渲染结束后异步执行,这意味着它不会阻塞浏览器的渲染过程。换句话说,React 会在处理 useEffect 内部的状态更新之前,让浏览器先绘制屏幕。
  • useLayoutEffect 的回调函数会在每次渲染结束后同步执行。这意味着它会阻塞浏览器的渲染过程,直到其执行完毕。因此,useLayoutEffect 可以在浏览器重新绘制屏幕之前触发。

需要注意的是,由于 useLayoutEffect 会阻塞浏览器的渲染过程,如果过度使用,可能会导致应用程序变慢,甚至引发性能问题。因此,在大多数情况下,应优先考虑使用 useEffect。只有在某些特定情况下,例如需要在 DOM 更新后立即进行某些操作,或者需要在用户看到渲染结果之前进行某些操作,才考虑使用 useLayoutEffect