React v16 源码分析 ⑩ 事件系统

合成事件

先看一个案例

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 class App extends React.Component {
state = {
show: false,
};
ref = React.createRef();
componentDidMount() {
const that = this;
document.addEventListener("click", () => {
that.setState({ show: false });
console.log("document");
});
this.ref.current.addEventListener("click", () => {
console.log("ref");
});
}
render() {
return (
<div
ref={this.ref}
onClick={() => this.setState({ show: true }, console.log("dom"))}
>
点击 {this.state.show ? "show" : "null"}
</div>
);
}
}

当点击事件触发的时候会发现点击事件无效, 先打印出 ref,再打印 dom,后打印出 document,先分析一个打印顺序的问题

如果事件是绑定在原生上的,那么 render 的执行会早于 componentDidMount,所以按理来说打印 dom 的事件应该先执行,在执行打印 ref 的事件,但显然结果不是这样.

其实这就是 React 合成事件(SyntheticEvent),在项目初始化的时候, 在 react v16 版本会把所有的事件绑定在 document 元素上, v17 版本把元素修改为 root 元素,用官方的话讲就是:不需要使用 addEventListener 为已创建的 DOM 元素添加监听器。事实上,你只需要在该元素初始渲染的时候添加监听器即可。

因为这个原因,在冒泡阶段最先执行的是 div 的原生事件,又因为 div 上的合成事件虽然绑定在 document 上,但绑定时机在项目初始化的时候, 而 document 上原生的绑定事件是在 componentDidMount 生命周期中,所以按照原生事件的行为,先绑定的先执行,会先打印 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
import React from "react";

export default class App extends React.Component {
parentRef = React.createRef();
childRef = React.createRef();
componentDidMount() {
this.parentRef.current.addEventListener(
"click",
() => {
console.log("父元素原生事件捕获");
},
true
);
this.parentRef.current.addEventListener("click", () => {
console.log("父元素原生事件冒泡");
});
this.childRef.current.addEventListener(
"click",
() => {
console.log("子元素原生事件捕获");
},
true
);
this.childRef.current.addEventListener("click", () => {
console.log("子元素原生事件冒泡");
});
document.addEventListener(
"click",
() => {
console.log("document 捕获");
},
true
);
document.addEventListener("click", () => {
console.log("document 冒泡");
});
}
parentBubble = () => {
console.log("父组件React事件冒泡");
};
parenteCapture = () => {
console.log("父组件React事件捕获");
};
childBubble = () => {
console.log("子组件React事件冒泡");
};
childeCapture = () => {
console.log("子组件React事件捕获");
};
render() {
return (
<div
ref={this.parentRef}
onClick={this.parentBubble}
onClickCapture={this.parenteCapture}
>
<p
ref={this.childRef}
onClick={this.childBubble}
onClickCapture={this.childeCapture}
>
事件
</p>
</div>
);
}
}

v16 版本

1
2
3
4
5
6
7
8
9
10
document 捕获
父元素原生事件捕获
子元素原生事件捕获
子元素原生事件冒泡
父元素原生事件冒泡
父组件React事件捕获
子组件React事件捕获
子组件React事件冒泡
父组件React事件冒泡
document 冒泡

虽然合成事件绑定在 document 元素上但只能在冒泡阶段触发, 因为合成事件绑定在冒包阶段而不是捕获阶段, 因此会先执行所有原生事件的捕获阶段, 捕获阶段结束之后由于合成事件在 document 仍然不会执行, 紧接着执行原生的冒泡事件.直到冒泡到 document 元素由于合成事件绑定的早,所有一次性的执行了合成事件的捕获和冒泡阶段,最后才是 document 冒泡事件

v17 版本

1
2
3
4
5
6
7
8
9
10
11

document 捕获
父组件React事件捕获
子组件React事件捕获
父元素原生事件捕获
子元素原生事件捕获
子元素原生事件冒泡
父元素原生事件冒泡
子组件React事件冒泡
父组件React事件冒泡
document 冒泡

由于在 v17 版本中事件是绑定在 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
<body>
<div id="root">
<div id="parent">
<p id="child">事件</p>
</div>
</div>
</body>
<script>
let root = document.getElementById("root");
let parent = document.getElementById("parent");
let child = document.getElementById("child");

parent.addEventListener(
"click",
() => {
console.log("父元素原生事件捕获");
},
true
);
parent.addEventListener("click", () => {
console.log("父元素原生事件冒泡");
});
child.addEventListener(
"click",
() => {
console.log("子元素原生事件捕获");
},
true
);
child.addEventListener("click", () => {
console.log("子元素原生事件冒泡");
});

document.addEventListener(
"click",
function () {
console.log("document 捕获");
},
true
);
document.addEventListener("click", function () {
console.log("document 冒泡");
});

parent.onClick = function () {
console.log("父组件React事件冒泡");
};
parent.onClickCapture = () => {
console.log("父组件React事件捕获");
};
child.onClick = function () {
console.log("子组件React事件冒泡");
};
child.onClickCapture = () => {
console.log("子组件React事件捕获");
};
</script>

v16 版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
document.addEventListener("click", dispatchEvent);
function dispatchEvent(event) {
let paths = [];
let current = event.target;
while (current) {
paths.push(current);
current = current.parentNode;
}
for (let i = paths.length - 1; i >= 0; i--) {
let handle = paths[i].onClickCapture;
handle && handle();
}
for (let i = 0; i < paths.length; i++) {
let handle = paths[i].onClick;
handle && handle();
}
}

v17 版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function dispatchEvent(event, useCapture) {
let paths = [];
let current = event.target;
while (current) {
paths.push(current);
current = current.parentNode;
}
if (useCapture) {
for (let i = paths.length - 1; i >= 0; i--) {
let handle = paths[i].onClickCapture;
handle && handle();
}
} else {
for (let i = 0; i < paths.length; i++) {
let handle = paths[i].onClick;
handle && handle();
}
}
}
root.addEventListener("click", (e) => dispatchEvent(e, true), true);
root.addEventListener("click", (e) => dispatchEvent(e, false));

事件注册

这一过程是在全局执行的

1
2
3
4
5
SimpleEventPlugin.registerEvents();
EnterLeaveEventPlugin.registerEvents();
ChangeEventPlugin.registerEvents();
SelectEventPlugin.registerEvents();
BeforeInputEventPlugin.registerEvents();

先把所有的事件分为以下 5 种类型

  • simpleEvents

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    for (var i = 0; i < simpleEventPluginEvents.length; i++) {
    var eventName = simpleEventPluginEvents[i];
    var domEventName = eventName.toLowerCase();
    var capitalizedEvent = eventName[0].toUpperCase() + eventName.slice(1);

    // 把事件转换成 onEvent 的格式
    registerSimpleEvent(domEventName, "on" + capitalizedEvent);
    }

    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");
  • enterLeaveEvents

    1
    2
    3
    4
    registerDirectEvent("onMouseEnter", ["mouseout", "mouseover"]);
    registerDirectEvent("onMouseLeave", ["mouseout", "mouseover"]);
    registerDirectEvent("onPointerEnter", ["pointerout", "pointerover"]);
    registerDirectEvent("onPointerLeave", ["pointerout", "pointerover"]);
  • changeEvents

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    registerTwoPhaseEvent("onChange", [
    "change",
    "click",
    "focusin",
    "focusout",
    "input",
    "keydown",
    "keyup",
    "selectionchange",
    ]);
  • selectEvent

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    registerTwoPhaseEvent("onSelect", [
    "focusout",
    "contextmenu",
    "dragend",
    "focusin",
    "keydown",
    "keyup",
    "mousedown",
    "mouseup",
    "selectionchange",
    ]);
  • beforeInputEvents

    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 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",
    ]);
    }

registerSimpleEvent 是为了保存原生事件和合成事件名称之间的对应关系,并且调用 registerTwoPhaseEvent

1
2
3
4
5
6
var topLevelEventsToReactNames = new Map();
// click onClick
function registerSimpleEvent(domEventName, reactName) {
topLevelEventsToReactNames.set(domEventName, reactName);
registerTwoPhaseEvent(reactName, [domEventName]);
}

registerTwoPhaseEvent 会调用 registerDirectEvent 用于绑定合成事件名称和其对应的真正事件,一个合成事件对应多个原生事件,也就是 JSX 中绑定的事件,可能会触发多个原生事件

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 registerTwoPhaseEvent(registrationName, dependencies) {
registerDirectEvent(registrationName, dependencies);
registerDirectEvent(registrationName + "Capture", dependencies);
}

function registerDirectEvent(registrationName, dependencies) {
// 合成事件和原生事件的对应关系 {onAbort:['abort']}
registrationNameDependencies[registrationName] = dependencies;

{
var lowerCasedName = registrationName.toLowerCase();

// 内部用于验证事件的对象,保存的是驼峰命名和非驼峰命名之间的关系 {onclick:onClick}
possibleRegistrationNames[lowerCasedName] = registrationName;

if (registrationName === "onDoubleClick") {
possibleRegistrationNames.ondblclick = registrationName;
}
}
//所有原生事件名称的 set 集合 set([click,cancel])
for (var i = 0; i < dependencies.length; i++) {
allNativeEvents.add(dependencies[i]);
}
}

注册过程就是通过几个全局变量完整的保存了,原生事件名称(click),原生事件绑定名称(onClick),合成事件名称 中的对应关系

事件绑定

绑定事件发生在 rendercreateRoot 方法中, 在创建了 root 节点之后会调用 listenToAllSupportedEvents, 会对所有的原生事件调用绑定函数

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
function listenToAllSupportedEvents(rootContainerElement) {
allNativeEvents.forEach(function (domEventName) {
// 特殊处理,这个方法只会在 document 上
if (domEventName !== "selectionchange") {
// 排除那些没有冒泡阶段的事件
if (!nonDelegatedEvents.has(domEventName)) {
listenToNativeEvent(domEventName, false, rootContainerElement);
}
listenToNativeEvent(domEventName, true, rootContainerElement);
}
});

ownerDocument[listeningMarker] = true;
listenToNativeEvent("selectionchange", false, ownerDocument);
}

function listenToNativeEvent(domEventName, isCapturePhaseListener, target) {
var eventSystemFlags = 0;

// 添加捕获阶段的标识
if (isCapturePhaseListener) {
eventSystemFlags |= IS_CAPTURE_PHASE;
}

addTrappedEventListener(
target,
domEventName,
eventSystemFlags,
isCapturePhaseListener
);
}

function addTrappedEventListener(
targetContainer,
domEventName,
eventSystemFlags,
isCapturePhaseListener,
isDeferredListenerForLegacyFBSupport
) {
// 创建事件回调函数
var listener = createEventListenerWrapperWithPriority(
targetContainer,
domEventName,
eventSystemFlags
);

var isPassiveListener = undefined;
// 是否支持 passive 参数
// addEventListener 第三个参数可以写为一个对象 { capture: false, once: true,passive:true }
// capture 表示冒泡阶段执行还是捕获阶段执行
// once 标识绑定事件只会执行一次就被移除
// passive 是否执行默认事件,有些默认事件和浏览器行为绑定比如移动端 touchstart 会触发浏览器滚动
// 如果使用 event.preventDefault() 会调用函数后才会停止默认行为, 可以使用 passive:false 直接阻止默认行为
if (passiveBrowserEventsSupported) {
if (
domEventName === "touchstart" ||
domEventName === "touchmove" ||
domEventName === "wheel"
) {
isPassiveListener = true;
}
}

targetContainer = targetContainer;
var unsubscribeListener;

// 利用 addEventListener 绑定事件监听函数

if (isCapturePhaseListener) {
if (isPassiveListener !== undefined) {
unsubscribeListener = addEventCaptureListenerWithPassiveFlag(
targetContainer,
domEventName,
listener,
isPassiveListener
);
} else {
unsubscribeListener = addEventCaptureListener(
targetContainer,
domEventName,
listener
);
}
} else {
if (isPassiveListener !== undefined) {
unsubscribeListener = addEventBubbleListenerWithPassiveFlag(
targetContainer,
domEventName,
listener,
isPassiveListener
);
} else {
unsubscribeListener = addEventBubbleListener(
targetContainer,
domEventName,
listener
);
}
}
}

createEventListenerWrapperWithPriority 会调用 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
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
function getEventPriority(domEventName) {
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:
// eslint-disable-next-line no-fallthrough

case "change":
case "selectionchange":
case "textInput":
case "compositionstart":
case "compositionend":
case "compositionupdate": // Only enableCreateEventHandleAPI:
// eslint-disable-next-line no-fallthrough

case "beforeblur":
case "afterblur": // Not used by React but could be by user code:
// eslint-disable-next-line no-fallthrough

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:
// eslint-disable-next-line no-fallthrough

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.
var schedulerPriority = getCurrentPriorityLevel();

switch (schedulerPriority) {
case ImmediatePriority:
return DiscreteEventPriority;

case UserBlockingPriority:
return ContinuousEventPriority;

case NormalPriority:
case LowPriority:
// TODO: Handle LowSchedulerPriority, somehow. Maybe the same lane as hydration.
return DefaultEventPriority;

case IdlePriority:
return IdleEventPriority;

default:
return DefaultEventPriority;
}
}

default:
return DefaultEventPriority;
}
}

function createEventListenerWrapperWithPriority(
targetContainer,
domEventName,
eventSystemFlags
) {
var eventPriority = getEventPriority(domEventName);
var 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
);
}

事件执行

当一个事件触发时, 会调用事件绑定时创建的回调函数,这个函数会以批处理的形式调用事件处理方法 dispatchEventsForPlugins

1
2
3
4
5
6
7
8
batchedUpdates(function () {
return dispatchEventsForPlugins(
domEventName, // 事件名称
eventSystemFlags, // 捕获阶段标识
nativeEvent, // 原生事件对象
ancestorInst
);
});

dispatchEventsForPlugins 首先获取触发事件的元素调用 extractEvents 方法,从原生 DOM 上的 stateNode 获取到 FiberNode,并且尝试获取当前元素的绑定事件,在根据不同的事件优先级,包装成不同的合成事件对象

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
function dispatchEventsForPlugins(
domEventName,
eventSystemFlags,
nativeEvent,
targetInst,
targetContainer
) {
var nativeEventTarget = getEventTarget(nativeEvent);
var dispatchQueue = [];

// 内部调用不同的处理函数,用于处理没有原生事件对应的 React 事件,例如 onBeforeInput
// if (shouldProcessPolyfillPlugins) {
// extractEvents$2(dispatchQueue, domEventName, targetInst, nativeEvent, nativeEventTarget);
// extractEvents$1(dispatchQueue, domEventName, targetInst, nativeEvent, nativeEventTarget);
// extractEvents$3(dispatchQueue, domEventName, targetInst, nativeEvent, nativeEventTarget);
// extractEvents(dispatchQueue, domEventName, targetInst, nativeEvent, nativeEventTarget);
// }
extractEvents(
dispatchQueue,
domEventName,
targetInst,
nativeEvent,
nativeEventTarget,
eventSystemFlags
);
processDispatchQueue(dispatchQueue, eventSystemFlags);
}

function extractEvents(
dispatchQueue,
domEventName,
targetInst,
nativeEvent,
nativeEventTarget,
eventSystemFlags,
targetContainer
) {
// 不同的事件类型,对应不同的合成事件构造函数
// 内部实现 preventDefault stopPropagation
switch (domEventName) {
case "click":
SyntheticEventCtor = SyntheticMouseEvent;
case "drag":
SyntheticEventCtor = SyntheticDragEvent;
}

// 从当前节点循环遍历到根节点, 收集所有上级节点中绑定当前方法的节点和它的绑定函数
var _listeners = accumulateSinglePhaseListeners(
targetInst,
reactName,
nativeEvent.type,
inCapturePhase,
accumulateTargetOnly
);

if (_listeners.length > 0) {
// Intentionally create event lazily.
var _event = new SyntheticEventCtor(
reactName,
reactEventType,
null,
nativeEvent,
nativeEventTarget
);

dispatchQueue.push({
event: _event,
listeners: _listeners,
});
}
}

function processDispatchQueue(dispatchQueue, eventSystemFlags) {
var inCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) !== 0;

// 循环事件队列,依次触发合成事件
for (let i = 0; i < dispatchQueue.length; i++) {
const {event, listeners} = dispatchQueue[i];
var previousInstance;

(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;
}
}
}
})()

rethrowCaughtError();
}
打赏
  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2015-2025 SunZhiqi

此时无声胜有声!

支付宝
微信