官方文档对React事件的介绍包含如下几点css
那么在看源码以前,有如下疑问:html
React 版本号 16.9.0react
了解源码最好的方式是单步调试,找一个最简单的例子,在源码中打断点进行调试。本文采用create-react-app建立了最简单的demo,只含有click事件。页面内容以下浏览器
import React from 'react';
import './App.css';
class App extends React.Component {
spanClickEvent = null;
headerClickEvent = null;
componentDidMount () {
// document.addEventListener('click', () => {
// console.log('document click');
// })
}
spanClick (event) {
event.stopPropagation();
console.log('spanClick');
console.log(event);
// this.spanClickEvent = event;
}
headerClick (event) {
console.log('headerClick');
console.log(event);
// this.headerClickEvent = event;
// console.log(this.headerClickEvent === this.spanClickEvent);
}
inputChange (event) {
console.log('inputChange');
console.log(event);
}
render () {
return (
<div className="App">
<header className="App-header" onClick={(event) => this.headerClick(event)}>
<div className="btn-wrapper">
<span className="btn" onClick={(event) => this.spanClick(event)}>
<span>点击</span>
</span>
{/* <input onChange={(event) => this.inputChange(event)}/> */}
</div>
</header>
</div>
)};
}
export default App;
复制代码
首先刷新页面,在render过程当中会走到以下逻辑bash
function createInstance(type, props, rootContainerInstance, hostContext, internalInstanceHandle) {
var parentNamespace = void 0;
{
// 此处省略代码...
}
var domElement = createElement(type, props, rootContainerInstance, parentNamespace);
// 将internalInstanceHandle和props挂载在真实的DOM上,后面会用到
precacheFiberNode(internalInstanceHandle, domElement);
updateFiberProps(domElement, props);
return domElement;
}
复制代码
updateFiberProps会走到setInitialDOMProperties里面app
function setInitialDOMProperties(tag, domElement, rootContainerElement, nextProps, isCustomComponentTag) {
// 此处省略代码
else if (registrationNameModules.hasOwnProperty(propKey)) {
if (nextProp != null) {
if (true && typeof nextProp !== 'function') {
warnForInvalidEventListener(propKey, nextProp);
}
// 若是props含有事件相关的属性,则去监听对应的事件
ensureListeningTo(rootContainerElement, propKey);
}
}
}
复制代码
注意此处使用的registrationNameModules存放了全部React事件。dom
ensureListeningTo会判断当前是否在iframe里面,以决定监听哪里的事件。最后走到listenTo逻辑里面。异步
function listenTo(registrationName, mountAt) {
//listeningSet存放了已经监听过的事件,避免重复去监听。
var listeningSet = getListeningSetForElement(mountAt);
var dependencies = registrationNameDependencies[registrationName];
for (var i = 0; i < dependencies.length; i++) {
var dependency = dependencies[i];
if (!listeningSet.has(dependency)) {
// 初始化span标签的时候会走到这个逻辑里面,header的时候就不会再重复去监听click了
switch (dependency) {
// 此处省略代码
default:
var isMediaEvent = mediaEventTypes.indexOf(dependency) !== -1;
if (!isMediaEvent) {
trapBubbledEvent(dependency, mountAt);
}
break;
}
listeningSet.add(dependency);
}
}
}
复制代码
registrationNameDependencies存放了React事件与原生事件须要监听的对应关系。以下图中,若是使用onBlur则会监听window的blur事件,若是使用onChange则会监听blur/change/..等事件ui
接下来讲trapBubbledEventthis
function addEventBubbleListener(element, eventType, listener) {
// 注意此处element是document,第三个参数是false
element.addEventListener(eventType, listener, false);
}
复制代码
此时的listener为dispatchDiscreteEvent
至此,事件注册完成。值得注意的是,React在生成的真实DOM中加入了两个React属性,一个放了元素的props,一个放了元素对应的FiberNode。 原生DOM和FiberNode的一个双向关系。
点击span元素后,会走到dispatchDiscreteEvent逻辑里面,会带着nativeEvent调dispatchEvent方法。
经过getEventTarget(nativeEvent)
拿到当前的nativeEvent.target
为<span>点击元素</span>
,而后拿到DOM上含有__reactInternalInstance***的最近的元素,此处为<span>点击元素</span>
。调用dispatchEventForPluginEventSystem,调用batchedEventUpdates,中间会调用runExtractedPluginEventsInBatch处理原生事件,将原生事件合成为合成事件。最后会调用到traverseTwoPhase。这个方法主要是找到当前的path链
温习currentTarget和target currentTarget表示事件处理程序当前正在处理事件的那个元素 target 事件的目标
function traverseTwoPhase(inst, fn, arg) {
var path = [];
while (inst) {
path.push(inst);
inst = getParent(inst);
}
var i = void 0;
for (i = path.length; i-- > 0;) {
// 从外层到里层遍历元素,模拟捕获
fn(path[i], 'captured', arg);
}
for (i = 0; i < path.length; i++) {
// 从里层到外层遍历元素,模拟冒泡
fn(path[i], 'bubbled', arg);
}
}
复制代码
调用对应的fn也就是accumulateDirectionalDispatches
function accumulateDirectionalDispatches(inst, phase, event) {
// 省略代码
// 在'bubble'阶段的onClick对应onClick,而captured的onClick对应onClickCaptured。所以咱们在捕获阶段没有事件能够触发。感兴趣的能够将demo中的onClick更改成onClickCaptured模拟捕获触发
var listener = listenerAtPhase(inst, event, phase);
if (listener) {
// 依次拿到span.btn和header上的onClick,而且放进event._dispatchListeners
event._dispatchListeners = accumulateInto(event._dispatchListeners, listener);
event._dispatchInstances = accumulateInto(event._dispatchInstances, inst);
}
}
复制代码
最后调用executeDispatchesInOrder,遍历_dispatchListeners依次触发。触发的时候会判断event.isPropagationStopped()是true仍是false
function executeDispatch(event, listener, inst) {
var type = event.type || 'unknown-event';
// 赋值给currentTarget
event.currentTarget = getNodeFromInstance(inst);
invokeGuardedCallbackAndCatchFirstError(type, listener, undefined, event);
event.currentTarget = null;
}
复制代码
最后调用到fakeNode.dispatchEvent触发callCallback真正的onClick事件。此处采用fakeNode.dispatchEvent是为了让事件仍然是浏览器发起的。
调用完毕后,会将event初始化为最初的状态
整个过程,能够发现如下问题
回到最初的问题提问
例子中的span调用了stopPropagation,那么如下代码会触发吗
componentDidMount () {
document.addEventListener('click', () => {
// 依然会触发。为何?
console.log('document click');
})
window.addEventListener('click', () => {
// 不会触发。为何?
console.log('document click');
})
}
复制代码
另外一个问题,React是何时removeEventListner的?目前的出来的结论是并无。以下例子,在点击header时会触发React的DispatchEvent没有问题。可是在isShow为false后,点击span元素,仍然会触发DispatchEvent。所以目前的结论是,React并无去移除无用的EventListner。这个问题欢迎在评论区交流
class App extends React.Component {
constructor () {
super();
this.state = {
isShow: true
};
}
headerClick (event) {
this.setState({
isShow: false
});
}
render () {
return (
<div className="App">
{
this.state.isShow ?
<header className="App-header" onClick={(event) => this.headerClick(event)}>
</header>
: <span>点击</span>
}
</div>
)};
}
复制代码