官方文档对React事件的介绍包含如下几点css
那么在看源码以前,有如下疑问:html
React 版本号 16.9.0
了解源码最好的方式是单步调试,找一个最简单的例子,在源码中打断点进行调试。本文采用create-react-app建立了最简单的demo,只含有click事件。页面内容以下react
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过程当中会走到以下逻辑浏览器
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/..等事件this
接下来讲trapBubbledEventspa
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> )}; }