韦陀 發表於 2019-9-19 07:08:00

【React】354- 一文吃透 React 事件机制原理

<div class="rich_media_content" id="js_content">
                  

                  

                  
                  
                  <p><img src="https://ss.csdn.net/p?https://mmbiz.qpic.cn/mmbiz_png/z5Pdm7u7moYdbN4pBBfhfvXYrP8jEZMuFRKvc8suCY5LoQ7Lz8KIrQEtUmo7JGK9W77LenGLMAFTUStTmiaju9A/640?wx_fmt=png" alt="640?wx_fmt=png"></p><h1><span>大纲</span></h1><p><span>主要分为4大块儿,主要是结合源码对 </span><code class="prettyprint code-in-text prettyprinted"><span>react事件机制的原理</span></code><span> 进行分析,希望可以让你对 react事件机制有更清晰的认识和理解。</span></p><p><span>当然肯定会存在一些表述不清或者理解不够标准的地方,还请各位大神、大佬斧正。</span></p><p><span>01 - 对事件机制的初步理解和验证</span></p><p><span>02 - 对于合成的理解</span></p><p><span>03 - 事件注册机制</span></p><p><span>04 - 事件执行机制</span></p><p><span>01 02 是理论的废话,也算是我的个人总结,没兴趣的可以直接跳到 03-事件执行机制。</span></p><p><span><strong>ps:</strong> 本文基于 react15.6.1进行分析,虽然不是最新版本但是也不会影响我们对 react 事件机制的整体把握和理解。</span></p><h1><span>对事件机制的初步理解和验证</span></h1><p><span>对 </span><code class="prettyprint code-in-text prettyprinted"><span>react事件机制</span></code><span> 的表象理解,验证,意义和思考。</span></p><h2><span>表象理解</span></h2><p><span>先回顾下 对react 事件机制基本理解,react自身实现了一套自己的事件机制,包括事件注册、事件的合成、事件冒泡、事件派发等,虽然和原生的是两码事,但也是基于浏览器的事件机制下完成的。</span></p><p><span>我们都知道react 的所有事件并没有绑定到具体的dom节点上而是绑定在了document 上,然后由统一的事件处理程序来处理,同时也是基于浏览器的事件机制(冒泡),所有节点的事件都会在 document 上触发。</span></p><p><span>既然已经有了对 </span><code class="prettyprint code-in-text prettyprinted"><span>react事件</span></code><span> 的一个基本的认知,那这个认知是否正确呢?我们可以通过简单的方法进行验证。</span></p><h2><span>验证</span></h2><p><span>验证内容:</span></p><p><span>所有事件均注册到了元素的最顶层-document 上
节点的事件由统一的入口处理
为了方便,直接通过 cli 创建一个项目。</span></p><pre class="has"><code class="language-javascript">
componentDidMount(){        
      document.getElementById('btn-reactandnative').addEventListener('click', (e) =&gt; {       
            console.log('原生+react 事件:   原生事件执行');       
      });       
    }       
    handleNativeAndReact = (e) =&gt; {       
      console.log('原生+react 事件:当前执行react事件');       
    }       
    handleClick=(e)=&gt;{       
      console.log('button click');       
    }       
render(){       
      return &lt;div className="pageIndex"&gt;&lt;p&gt;react event!!!&lt;/p       
                &lt;button id="btn-confirm" onClick={this.handleClick}&gt;react 事件&lt;/button&gt;       
                &lt;button id="btn-reactandnative" onClick={this.handleNativeAndReact}&gt;原生 + react 事件&lt;/button&gt;       
      &lt;/div&gt;       
    }</code></pre><p><span>代码中给两个 </span><code class="prettyprint code-in-text prettyprinted"><span>button</span></code><span>绑定了合成事件,单独给 </span><code class="prettyprint code-in-text prettyprinted"><span>btn#btn-reactandnative</span></code><span>绑定了一个原生的事件。</span></p><p><span>然后看下 </span><code class="prettyprint code-in-text prettyprinted"><span>chrome</span></code><span> 控制台,查看元素上注册的事件。</span></p><p><img src="https://ss.csdn.net/p?https://mmbiz.qpic.cn/mmbiz_png/z5Pdm7u7moYdbN4pBBfhfvXYrP8jEZMujNFia1Sgl5FUw1TUNhJ68oHul0wM3cawp7qoH71gZKyXf9O6PXug4eQ/640?wx_fmt=png" alt="640?wx_fmt=png"></p><p><img src="https://ss.csdn.net/p?https://mmbiz.qpic.cn/mmbiz_png/z5Pdm7u7moYdbN4pBBfhfvXYrP8jEZMuLCVXQ1bOCB3zupmjQoEdkmic9VDZvvvayYyWNziaOsjtEibmgDLt21QPQ/640?wx_fmt=png" alt="640?wx_fmt=png"></p><p><span>经过简单的验证,可以看到所有的事件根据不同的事件类型都绑定在了 </span><code class="prettyprint code-in-text prettyprinted"><span>document</span></code><span> 上,触发函数统一是 </span><code class="prettyprint code-in-text prettyprinted"><span>dispatchEvent</span></code><span>。</span></p><h2><span>试想一下</span></h2><p><span>如果一个节点上同时绑定了合成和原生事件,那么禁止冒泡后执行关系是怎样的呢?</span></p><p><span>其实读到这里答案已经有了。我们现在基于目前的知识去分析下这个关系。</span></p><p><span>因为合成事件的触发是基于浏览器的事件机制来实现的,通过冒泡机制冒泡到最顶层元素,然后再由 </span><code class="prettyprint code-in-text prettyprinted"><span>dispatchEvent</span></code><span>统一去处理。</span></p><p><span><span>* 得出的结论:</span>*</span></p><p><span>原生事件阻止冒泡肯定会阻止合成事件的触发。</span></p><p><span>合成事件的阻止冒泡不会影响原生事件。</span></p><p><span>为什么呢?先回忆下浏览器事件机制</span></p><p><img src="https://ss.csdn.net/p?https://mmbiz.qpic.cn/mmbiz_png/z5Pdm7u7moYdbN4pBBfhfvXYrP8jEZMumeDNuyOBQTU2gtLZXmmfVbCsBNjRFzejzleW1hGsuTEicQBWaHVmEcA/640?wx_fmt=png" alt="640?wx_fmt=png"></p><p><span>浏览器事件的执行需要经过三个阶段,捕获阶段-目标元素阶段-冒泡阶段。</span></p><p><span>节点上的原生事件的执行是在目标阶段,然而合成事件的执行是在冒泡阶段,所以原生事件会先合成事件执行,然后再往父节点冒泡。</span></p><p><span>既然原生都阻止冒泡了,那合成还执行个啥嘞。</span></p><p><span>好,轮到合成的被阻止冒泡了,那原生会执行吗?当然会了。</span></p><p><span>因为原生的事件先于合成的执行,所以合成事件内阻止的只是合成的事件冒泡。(代码我就不贴了)</span></p><p><span><strong>所以得出结论:</strong></span></p><p><span>原生事件(阻止冒泡)会阻止合成事件的执行</span></p><p><span>合成事件(阻止冒泡)不会阻止原生事件的执行</span></p><p><span>两者最好不要混合使用,避免出现一些奇怪的问题</span></p><h2><span>意义</span></h2><p><span>react 自己做这么多的意义是什么?</span></p><ol class="list-paddingleft-2"><li><p><span>减少内存消耗,提升性能,不需要注册那么多的事件了,一种事件类型只在 document 上注册一次</span></p></li><li><p><span>统一规范,解决 ie 事件兼容问题,简化事件逻辑</span></p></li><li><p><span>对开发者友好</span></p></li></ol><h2><span>思考</span></h2><p><span>既然 react 帮我们做了这么多事儿,那他的背后的机制是什么样的呢?</span></p><p><span>事件怎么注册的,事件怎么触发的,冒泡机制怎样实现的呢?</span></p><p><span>请继续往后看......</span></p><h1><span>对于合成的理解</span></h1><p><span>刚听说合成这个词时候,感觉是特别高大上,很有深度,不是很好理解。</span></p><p><span>当我大概的了解过react事件机制后,略微了解一些皮毛,我觉得合成不单单是事件的合成和处理,从广义上来说还包括:</span></p><ol class="list-paddingleft-2"><li><p><span>对原生事件的封装</span></p></li><li><p><span>对某些原生事件的升级和改造</span></p></li><li><p><span>不同浏览器事件兼容的处理</span></p></li></ol><h2><span>对原生事件的封装</span></h2><p><img src="https://ss.csdn.net/p?https://mmbiz.qpic.cn/mmbiz_png/z5Pdm7u7moYdbN4pBBfhfvXYrP8jEZMuly8gUj30Wx7Cican5osNXnzOEbbMjoSVibjgiamK0c9AtqicHMDwPXibBGw/640?wx_fmt=png" alt="640?wx_fmt=png"></p><p><span>上面代码是给一个元素添加 </span><code class="prettyprint code-in-text prettyprinted"><span>click</span></code><span>事件的回调方法,方法中的参数 </span><code class="prettyprint code-in-text prettyprinted"><span>e</span></code><span>,其实不是原生事件对象而是react包装过的对象,同时原生事件对象被放在了属性 </span><code class="prettyprint code-in-text prettyprinted"><span>e.nativeEvent</span></code><span>内。</span></p><p><span>通过调试,在执行栈里看下这个参数 </span><code class="prettyprint code-in-text prettyprinted"><span>e</span></code><span>包含哪些属性</span></p><p><img src="https://ss.csdn.net/p?https://mmbiz.qpic.cn/mmbiz_png/z5Pdm7u7moYdbN4pBBfhfvXYrP8jEZMupPOsMR87PhHNUIibsB4ibX9nsWbIzKmiaFiaKvPHU2jueiaePpblAsOUZiag/640?wx_fmt=png" alt="640?wx_fmt=png"></p><p><span>再看下官方说明文档</span></p><p><img src="https://ss.csdn.net/p?https://mmbiz.qpic.cn/mmbiz_png/z5Pdm7u7moYdbN4pBBfhfvXYrP8jEZMupCrLZktZcUTHFrRsgRhgUcWcTIdLbaf11TCJvmZUovRnFDAEYAXic1A/640?wx_fmt=png" alt="640?wx_fmt=png"></p><p><span>SyntheticEvent是react合成事件的基类,定义了合成事件的基础公共属性和方法。</span></p><p><span>react会根据当前的事件类型来使用不同的合成事件对象,比如鼠标单机事件 - SyntheticMouseEvent,焦点事件-SyntheticFocusEvent等,但是都是继承自SyntheticEvent。</span></p><p><img src="https://ss.csdn.net/p?https://mmbiz.qpic.cn/mmbiz_png/z5Pdm7u7moYdbN4pBBfhfvXYrP8jEZMuEqZB1ScjbQia2CE3ZfcshTEPg9WWxw3yLibeKlicmQqdOWwfABQ6uKKyA/640?wx_fmt=png" alt="640?wx_fmt=png"></p><p><img src="https://ss.csdn.net/p?https://mmbiz.qpic.cn/mmbiz_png/z5Pdm7u7moYdbN4pBBfhfvXYrP8jEZMuibCOXzSzhx66Fox6sJG7rrULWBicfDbhe5UV74RsKIeNiabHucRib4YYyQ/640?wx_fmt=png" alt="640?wx_fmt=png"></p><p><img src="https://ss.csdn.net/p?https://mmbiz.qpic.cn/mmbiz_png/z5Pdm7u7moYdbN4pBBfhfvXYrP8jEZMuXVKM7Pmn78ECQWLYQJONqou9iaN7I2zzr1NVRoP2iaAMC45MtGoyrjUw/640?wx_fmt=png" alt="640?wx_fmt=png"></p><h2><span>对原生事件的升级和改造</span></h2><p><span>对于有些dom元素事件,我们进行事件绑定之后,react并不是只处理你声明的事件类型,还会额外的增加一些其他的事件,帮助我们提升交互的体验。</span></p><p><span>这里就举一个例子来说明下:</span></p><p><span>当我们给input声明个onChange事件,看下 react帮我们做了什么?</span></p><p><img src="https://ss.csdn.net/p?https://mmbiz.qpic.cn/mmbiz_png/z5Pdm7u7moYdbN4pBBfhfvXYrP8jEZMukvl9LPThxa7uOuotWv6ZWGyMWgicicBNn2ibsibKxP9FDXlvCxNHjl45fA/640?wx_fmt=png" alt="640?wx_fmt=png"></p><p><span>可以看到react不只是注册了一个onchange事件,还注册了很多其他事件。</span></p><p><span>而这个时候我们向文本框输入内容的时候,是可以实时的得到内容的。</span></p><p><span>然而原生只注册一个onchange的话,需要在失去焦点的时候才能触发这个事件,所以这个原生事件的缺陷react也帮我们弥补了。</span></p><p><span><strong>ps:</strong> 上面红色箭头中有一个 invalid事件,这个并没有注册到document上,而是在具体的元素上。我的理解是这个是html5新增的一个事件,当输入的数据不符合验证规则的时候自动触发,然而验证规则和配置都要写在当前input元素上,如果注册到document上这个事件就无效了。</span></p><h2><span>浏览器事件的兼容处理</span></h2><p><span>react在给document注册事件的时候也是对兼容性做了处理。</span></p><p><img src="https://ss.csdn.net/p?https://mmbiz.qpic.cn/mmbiz_png/z5Pdm7u7moYdbN4pBBfhfvXYrP8jEZMush5wh8o7TWPSwkctabicDOnl3NPjmG3vkoYSe9DBJK8o5WpWK48GeJw/640?wx_fmt=png" alt="640?wx_fmt=png"></p><p><span>上面这个代码就是给document注册事件,内部其实也是做了对 </span><code class="prettyprint code-in-text prettyprinted"><span>ie浏览器</span></code><span>的兼容做了处理。</span></p><p><span>以上就是我对于react合成这个名词的理解,其实react内部还处理了很多,我只是简单的举了几个栗子,后面开始聊事件注册和事件派发的机制。</span></p><h1><span>事件注册机制</span></h1><p><span>这是 </span><code class="prettyprint code-in-text prettyprinted"><span>react</span></code><span> 事件机制的第三节 - 事件注册,在这里你将了解 </span><code class="prettyprint code-in-text prettyprinted"><span>react</span></code><span>事件的注册过程,以及在这个过程中主要经过了哪些关键步骤,同时结合源码进行验证和增强理解。</span></p><p><span>在这里并不会说非常细节的内容,而是把大概的流程和原理性的内容进行介绍,做到对整体流程有个认知和理解。</span></p><h2><span>大致流程</span></h2><p><span>react 事件注册过程其实主要做了2件事:事件注册、事件存储。</span></p><p><span>a. 事件注册 - 组件挂载阶段,根据组件内的声明的事件类型-onclick,onchange 等,给 document 上添加事件 -addEventListener,并指定统一的事件处理程序 dispatchEvent。</span></p><p><span>b. 事件存储 - 就是把 react 组件内的所有事件统一的存放到一个对象里,缓存起来,为了在触发事件的时候可以查找到对应的方法去执行。</span></p><p><img src="https://ss.csdn.net/p?https://mmbiz.qpic.cn/mmbiz_png/z5Pdm7u7moYdbN4pBBfhfvXYrP8jEZMudLCsFiaQ2Kvo3iaczYO14XQdCHZxeN2FjvlkIYvpdundoNI7BHL5l8cQ/640?wx_fmt=png" alt="640?wx_fmt=png"></p><h2><span>关键步骤</span></h2><p><span>上面大致说了事件注册需要完成的两个目标,那完成目标的过程需要经过哪些关键处理呢?</span></p><p><span>首先 react 拿到将要挂载的组件的虚拟 dom(其实就是 react element 对象),然后处理 </span><code class="prettyprint code-in-text prettyprinted"><span>react dom</span></code><span> 的 props ,判断属性内是否有声明为事件的属性,比如 </span><code class="prettyprint code-in-text prettyprinted"><span>onClick,onChange</span></code><span>,这个时候得到事件类型 </span><code class="prettyprint code-in-text prettyprinted"><span>click,change</span></code><span> 和对应的事件处理程序 </span><code class="prettyprint code-in-text prettyprinted"><span>fn</span></code><span>,然后执行后面 </span><code class="prettyprint code-in-text prettyprinted"><span>3步</span></code></p><p><span>a. 完成事件注册</span></p><p><span>b. 将 </span><code class="prettyprint code-in-text prettyprinted"><span>react dom</span></code><span> ,事件类型,处理函数 </span><code class="prettyprint code-in-text prettyprinted"><span>fn</span></code><span> 放入数组存储</span></p><p><span>c. 组件挂载完成后,处理 b 步骤生成的数组,经过遍历把事件处理函数存储到 </span><code class="prettyprint code-in-text prettyprinted"><span>listenerBank(一个对象)</span></code><span>中</span></p><p><img src="https://ss.csdn.net/p?https://mmbiz.qpic.cn/mmbiz_png/z5Pdm7u7moYdbN4pBBfhfvXYrP8jEZMu3nP0zuvN1TJAxibAXggkh4nT15AonrbESuXJrjEQKCiaBUa3KibV1Uo7Q/640?wx_fmt=png" alt="640?wx_fmt=png"></p><h2><span>源码解析</span></h2><h3><span>从 jsx 说起</span></h3><p><span>看个最熟悉的代码,也是我们日常的写法</span></p><pre class="has"><code class="language-javascript">//此处代码省略       
    handleFatherClick=()=&gt;{       
    }       
    handleChildClick=()=&gt;{       
    }       
    render(){       
      return &lt;div className="box"&gt;       
                  &lt;div className="father" onClick={this.handleFatherClick}&gt;       
                        &lt;div className="child" onClick={this.handleChildClick}&gt;child &lt;/div&gt;       
                  &lt;/div&gt;       
               &lt;/div&gt;       
    }</code></pre><p><span>经过 </span><code class="prettyprint code-in-text prettyprinted"><span>babel</span></code><span> 编译后,可以看到最终调用的方法是 </span><code class="prettyprint code-in-text prettyprinted"><span>react.createElement</span></code><span>,z而且声明的事件类型和回调就是个 </span><code class="prettyprint code-in-text prettyprinted"><span>props</span></code><span>。</span></p><p><img src="https://ss.csdn.net/p?https://mmbiz.qpic.cn/mmbiz_png/z5Pdm7u7moYdbN4pBBfhfvXYrP8jEZMu8qmeDqtddPKyvczuk8LCNEN2f8S41MVu8BecW1qJia1EVZ8KQt9gVRQ/640?wx_fmt=png" alt="640?wx_fmt=png"></p><p><code class="prettyprint code-in-text prettyprinted"><span>react.createElement</span></code><span>执行的结果会返回一个所谓的虚拟 dom (react element object)</span></p><p><img src="https://ss.csdn.net/p?https://mmbiz.qpic.cn/mmbiz_png/z5Pdm7u7moYdbN4pBBfhfvXYrP8jEZMuJ9pDs13ZHjyYDCDY2CL8SQvATazbGngKiaicHjGJ65WZrMTWFCiathbQg/640?wx_fmt=png" alt="640?wx_fmt=png"></p><h3>处理组件props,拿到事件类型和回调 fn</h3><p><code class="prettyprint code-in-text prettyprinted"><span>ReactDOMComponent</span></code><span>在进行组件加载(mountComponent)、更新(updateComponent)的时候,需要对props进行处理(_updateDOMProperties):</span></p><p><img src="https://ss.csdn.net/p?https://mmbiz.qpic.cn/mmbiz_png/z5Pdm7u7moYdbN4pBBfhfvXYrP8jEZMulEaYVy5P4eOt9icBoamr9jzELA9rk9cmWFrXf0CRMMGiboDibYXDh6Wnw/640?wx_fmt=png" alt="640?wx_fmt=png"></p><p><span>可以看下 registrationNameModules 的内容,就不细说了,他就是一个内置的常量。</span></p><p><img src="https://ss.csdn.net/p?https://mmbiz.qpic.cn/mmbiz_png/z5Pdm7u7moYdbN4pBBfhfvXYrP8jEZMu83qzm421Jrb3p4jKlvmbblRPiadtiaPf9aqhld3jfq7eJOvzorVmEvdQ/640?wx_fmt=png" alt="640?wx_fmt=png"></p><h3>事件注册和事件的存储</h3><h4>事件注册</h4><p><span>接着上面的代码执行到了这个方法</span></p><pre class="has"><code class="language-javascript">enqueuePutListener(this, propKey, nextProp, transaction);</code></pre><p><span>在这个方法里会进行事件的注册以及事件的存储,包括冒泡和捕获的处理</span></p><p><img src="https://ss.csdn.net/p?https://mmbiz.qpic.cn/mmbiz_png/z5Pdm7u7moYdbN4pBBfhfvXYrP8jEZMumgWg6om5iakpv318cTtCcob56GE7V7qRRoqudkpA07OLSmFhAibjic4hg/640?wx_fmt=png" alt="640?wx_fmt=png"></p><p><span>根据当前的组件实例获取到最高父级-也就是document,然后执行方法 </span><code class="prettyprint code-in-text prettyprinted"><span>listenTo</span></code><span> - 也是最关键的一个方法,进行事件绑定处理。</span></p><p><code class="prettyprint code-in-text prettyprinted"><span>源码文件:ReactBrowerEventEmitter.js</span></code></p><p><img src="https://ss.csdn.net/p?https://mmbiz.qpic.cn/mmbiz_png/z5Pdm7u7moYdbN4pBBfhfvXYrP8jEZMuWibUw0LtdSDceoicEmGEtFHfnZOInVLgI6lsb0tUtBtNBa83WJnbOFWw/640?wx_fmt=png" alt="640?wx_fmt=png"></p><p><span>最后执行 </span><code class="prettyprint code-in-text prettyprinted"><span>EventListener.listen(冒泡)</span></code><span>或者 </span><code class="prettyprint code-in-text prettyprinted"><span>EventListener.capture(捕获)</span></code><span>,单看下冒泡的注册,其实就是 </span><code class="prettyprint code-in-text prettyprinted"><span>addEventListener</span></code><span>的第三个参数是 </span><code class="prettyprint code-in-text prettyprinted"><span>false</span></code><span>。</span></p><p><img src="https://ss.csdn.net/p?https://mmbiz.qpic.cn/mmbiz_png/z5Pdm7u7moYdbN4pBBfhfvXYrP8jEZMurnPRMBG0DsnVT1YLLnickkHL65rECydRtIqbOGeT3JFWvsF9pbJtubw/640?wx_fmt=png" alt="640?wx_fmt=png"></p><p><span>也可以看到注册事件的时候也对 ie 浏览器做了兼容。</span></p><p><span>上面没有看到 dispatchEvent 的定义,下面可以看到传入 dispatchEvent 方法的代码。</span></p><p><img src="https://ss.csdn.net/p?https://mmbiz.qpic.cn/mmbiz_png/z5Pdm7u7moYdbN4pBBfhfvXYrP8jEZMuLqdnBSBaJpP5u90KyremD4Sc9kDP0862Q9AdsIlhsWTsv7kC2ImrIw/640?wx_fmt=png" alt="640?wx_fmt=png"></p><p>到这里事件注册就完事儿了。</p><h4>事件存储</h4><p><span>开始事件的存储,在 react 里所有事件的触发都是通过 dispatchEvent方法统一进行派发的,而不是在注册的时候直接注册声明的回调,来看下如何存储的 。</span></p><p><span>react 把所有的事件和事件类型以及react 组件进行关联,把这个关系保存在了一个 map里,也就是一个对象里(键值对),然后在事件触发的时候去根据当前的 组件id和 事件类型查找到对应的 事件fn。</span></p><p><img src="https://ss.csdn.net/p?https://mmbiz.qpic.cn/mmbiz_png/z5Pdm7u7moYdbN4pBBfhfvXYrP8jEZMuF1oTOefgxUTpxsx53RTOic0XmtHozUyibIsbUHdJABamkUqfEdyX1d1Q/640?wx_fmt=png" alt="640?wx_fmt=png"></p><p><span>结合源码:</span></p><pre class="has"><code class="language-javascript">function enqueuePutListener(inst, registrationName, listener, transaction) {       
var containerInfo = inst._hostContainerInfo;       
var isDocumentFragment = containerInfo._node &amp;&amp; containerInfo._node.nodeType === DOC_FRAGMENT_TYPE;       
var doc = isDocumentFragment ? containerInfo._node : containerInfo._ownerDocument;       
listenTo(registrationName, doc);//这个方法上面已说完       
//这里涉及到了事务,事物会在以后的章节再介绍,主要看事件注册       
//下面的代码是将putListener放入数组,当组件挂载完后会依次执行数组的回调。也就是putListener会依次执行       
transaction.getReactMountReady().enqueue(putListener, {       
    inst: inst,//组件实例       
    registrationName: registrationName,//事件类型 click       
    listener: listener //事件回调 fn       
});       
}       
function putListener() {       
var listenerToPut = this;       
//放入数组,回调队列       
EventPluginHub.putListener(listenerToPut.inst, listenerToPut.registrationName, listenerToPut.listener);       
}</code></pre><p><span>大致的流程就是执行完 listenTo(事件注册),然后执行 putListener 方法进行事件存储,所有的事件都会存储到一个对象中 - listenerBank,具体由 EventPluginHub进行管理。</span></p><pre class="has"><code class="language-javascript"> //拿到组件唯一标识 id       
    var getDictionaryKey = function getDictionaryKey(inst) {       
      return '.' + inst._rootNodeID;       
    }       
   putListener: function putListener(inst, registrationName, listener) {       
    //得到组件 id       
      var key = getDictionaryKey(inst);       
      //得到listenerBank对象中指定事件类型的对象       
      var bankForRegistrationName = listenerBank || (listenerBank = {});       
      //存储回调 fn       
      bankForRegistrationName = listener;       
      //....       
}</code></pre><p><span>listenerBank其实就是一个二级 map,这样的结构更方便事件的查找。</span></p><p><span>这里的组件 id 就是组件的唯一标识,然后和fn进行关联,在触发阶段就可以找到相关的事件回调。</span></p><p><img src="https://ss.csdn.net/p?https://mmbiz.qpic.cn/mmbiz_png/z5Pdm7u7moYdbN4pBBfhfvXYrP8jEZMuqlPHlfuM5ujFAsdODPsJRJoN02WcPFr0bf5VETWD607WhTgGpUQ27A/640?wx_fmt=png" alt="640?wx_fmt=png"></p><p><span>看到这个结构是不是很熟悉呢?就是我们平常使用的 object.</span></p><p><span>到这里大致的流程已经说完,是不是感觉有点明白又不大明白。</span></p><p><span>没关系,再来个详细的图,重新理解下。</span></p><p><img src="https://ss.csdn.net/p?https://mmbiz.qpic.cn/mmbiz_png/z5Pdm7u7moYdbN4pBBfhfvXYrP8jEZMuHSjUCnzfUUCAlnc7LPXyEdo3UusYvQl2u2qpWjBrFgzM971ibaWQc4g/640?wx_fmt=png" alt="640?wx_fmt=png"></p><h1><span>事件执行机制</span></h1><p><span>在事件注册阶段,最终所有的事件和事件类型都会保存到 listenerBank中。</span></p><p><span>那么在事件触发的过程中上面这个对象有什么用处呢?</span></p><p><strong>其实就是用来查找事件回调</strong></p><h2><span>大致流程</span></h2><p><span>事件触发过程总结为主要下面几个步骤:</span></p><p><span>1.进入统一的事件分发函数(dispatchEvent)</span></p><p><span>2.结合原生事件找到当前节点对应的ReactDOMComponent对象</span></p><p><span>3.开始 事件的合成</span></p><p><span>3.1 根据当前事件类型生成指定的合成对象</span></p><p><span>3.2 封装原生事件和冒泡机制</span></p><p><span>3.3 查找当前元素以及他所有父级</span></p><p><span>3.4 在 listenerBank查找事件回调并合成到 event(合成事件结束)</span></p><p><span>4.批量处理合成事件内的回调事件(事件触发完成 end)</span></p><p><img src="https://ss.csdn.net/p?https://mmbiz.qpic.cn/mmbiz_png/z5Pdm7u7moYdbN4pBBfhfvXYrP8jEZMuUmNbp6No0QMma6CR2ozFfyobwDiaNlR6L6Th58Oo0KxHqb1gv4uF4ibw/640?wx_fmt=png" alt="640?wx_fmt=png"></p><p><strong>举个栗子</strong></p><p><span>在说具体的流程前,先看一个栗子,后面的分析也是基于这个栗子</span></p><pre class="has"><code class="language-javascript">handleFatherClick=(e)=&gt;{       
      console.log('father click');       
    }       
    handleChildClick=(e)=&gt;{       
      console.log('child click');       
    }       
    render(){       
      return &lt;div className="box"&gt;       
                  &lt;div className="father" onClick={this.handleFatherClick}&gt; father       
                        &lt;div className="child" onClick={this.handleChildClick}&gt;child &lt;/div&gt;       
                  &lt;/div&gt;       
               &lt;/div&gt;       
    }</code></pre><p><span>看到这个熟悉的代码,我们就已经知道了执行结果。</span></p><p><span>当我点击 child div 的时候,会同时触发father的事件。</span></p><p><img src="https://ss.csdn.net/p?https://mmbiz.qpic.cn/mmbiz_png/z5Pdm7u7moYdbN4pBBfhfvXYrP8jEZMucXa63TCiaZ75rGleuk3ampM7hmn0MzoPIYz10fwAA5lho25sMu2hHRQ/640?wx_fmt=png" alt="640?wx_fmt=png"></p><h2><span>源码解析</span></h2><h3><span>dispatchEvent 进行事件分发</span></h3><p><span>进入统一的事件分发函数 (dispatchEvent)。</span></p><p><span>当我点击child div 的时候,这个时候浏览器会捕获到这个事件,然后经过冒泡,事件被冒泡到 document 上,交给统一事件处理函数 dispatchEvent 进行处理。(上一文中我们已经说过 document 上已经注册了一个统一的事件处理函数 dispatchEvent)。</span></p><p><img src="https://ss.csdn.net/p?https://mmbiz.qpic.cn/mmbiz_png/z5Pdm7u7moYdbN4pBBfhfvXYrP8jEZMuVTqlNVXickAWWQHRqBibibia2hDrdYPan4jlnUTiaY5JgaEwyiaZQPflvL3g/640?wx_fmt=png" alt="640?wx_fmt=png"></p><h3><span>查找ReactDOMComponent</span></h3><p><span>结合原生事件找到当前节点对应的 ReactDOMComponent对象,在原生事件对象内已经保留了对应的 ReactDOMComponent实例的引用,应该是在挂载阶段就已经保存了。</span></p><p><img src="https://ss.csdn.net/p?https://mmbiz.qpic.cn/mmbiz_png/z5Pdm7u7moYdbN4pBBfhfvXYrP8jEZMuib7FVyc9MPURn4ydOHsb8mgnia5NN9oxib8ckX6jBia3icaaLWHlIhibkhDg/640?wx_fmt=png" alt="640?wx_fmt=png"></p><p><span>看下ReactDOMComponent实例的内容</span></p><p><img src="https://ss.csdn.net/p?https://mmbiz.qpic.cn/mmbiz_png/z5Pdm7u7moYdbN4pBBfhfvXYrP8jEZMug8e6ibt9eCfcibHAQvScqlZAHAlic6STZGWKJt7dgDzKb5oPFibMavCr4A/640?wx_fmt=png" alt="640?wx_fmt=png"></p><h3><span>事件合成ing</span></h3><p><span>事件的合成,冒泡的处理以及事件回调的查找都是在合成阶段完成的。</span></p><p><img src="https://ss.csdn.net/p?https://mmbiz.qpic.cn/mmbiz_png/z5Pdm7u7moYdbN4pBBfhfvXYrP8jEZMuvRIxLQYk1bwFehmRJDJ1HwJtzVC0SeAWXqJgRVwLNn8cwYf8n9tLVQ/640?wx_fmt=png" alt="640?wx_fmt=png"></p><h3><span>合成对象的生成</span></h3><p><span>根据当前事件类型找到对应的合成类,然后进行合成对象的生成</span></p><pre class="has"><code class="language-javascript">//进行事件合成,根据事件类型获得指定的合成类       
var SimpleEventPlugin = {       
    eventTypes: eventTypes,       
    extractEvents: function extractEvents(topLevelType, targetInst, nativeEvent, nativeEventTarget) {       
      var dispatchConfig = topLevelEventsToDispatchConfig;       
      //代码已省略....       
      var EventConstructor;       
      switch (topLevelType) {       
            //代码已省略....       
            case 'topClick'://【这里有一个不解的地方】 topLevelType = topClick,执行到这里了,但是这里没有做任何操作       
                if (nativeEvent.button === 2) {       
                  return null;       
                }       
            //代码已省略....       
            case 'topContextMenu'://而是会执行到这里,获取到鼠标合成类       
                EventConstructor = SyntheticMouseEvent;       
                break;       
            case 'topAnimationEnd':       
            case 'topAnimationIteration':       
            case 'topAnimationStart':       
                EventConstructor = SyntheticAnimationEvent;//动画类合成事件       
                break;       
            case 'topWheel':       
                EventConstructor = SyntheticWheelEvent;//鼠标滚轮类合成事件       
                break;       
            case 'topCopy':       
            case 'topCut':       
            case 'topPaste':       
                EventConstructor = SyntheticClipboardEvent;       
                break;       
      }       
      var event = EventConstructor.getPooled(dispatchConfig, targetInst, nativeEvent, nativeEventTarget);       
      EventPropagators.accumulateTwoPhaseDispatches(event);       
      return event;//最终会返回合成的事件对象       
    }</code></pre><h3><span>封装原生事件和冒泡机制</span></h3><p><span>在这一步会把原生事件对象挂到合成对象的自身,同时增加事件的默认行为处理和冒泡机制</span></p><pre class="has"><code class="language-javascript">/**       
*       
* @param {obj} dispatchConfig 一个配置对象 包含当前的事件依赖 ["topClick"],冒泡和捕获事件对应的名称 bubbled: "onClick",captured: "onClickCapture"       
* @param {obj} targetInst 组件实例ReactDomComponent       
* @param {obj} nativeEvent 原生事件对象       
* @param {obj} nativeEventTarget事件源 e.target = div.child       
*/       
function SyntheticEvent(dispatchConfig, targetInst, nativeEvent, nativeEventTarget) {       
    this.dispatchConfig = dispatchConfig;       
    this._targetInst = targetInst;       
    this.nativeEvent = nativeEvent;//将原生对象保存到 this.nativeEvent       
    //此处代码略.....       
    var defaultPrevented = nativeEvent.defaultPrevented != null ? nativeEvent.defaultPrevented : nativeEvent.returnValue === false;       
    //处理事件的默认行为       
    if (defaultPrevented) {       
      this.isDefaultPrevented = emptyFunction.thatReturnsTrue;       
    } else {       
      this.isDefaultPrevented = emptyFunction.thatReturnsFalse;       
    }       
    //处理事件冒泡 ,thatReturnsFalse 默认返回 false,就是不阻止冒泡       
    this.isPropagationStopped = emptyFunction.thatReturnsFalse;       
    return this;       
}</code></pre><p><span>下面是增加的默认行为和冒泡机制的处理方法,其实就是改变了当前合成对象的属性值, 调用了方法后属性值为 true,就会阻止默认行为或者冒泡。</span></p><pre class="has"><code class="language-javascript">//在合成类原型上增加preventDefault和stopPropagation方法       
_assign(SyntheticEvent.prototype, {       
    preventDefault: function preventDefault() {       
      // ....略       
      this.isDefaultPrevented = emptyFunction.thatReturnsTrue;       
    },       
    stopPropagation: function stopPropagation() {       
      //....略       
      this.isPropagationStopped = emptyFunction.thatReturnsTrue;       
    }       
);</code></pre><p><span>看下 emptyFunction 代码就明白了</span></p><p><img src="https://ss.csdn.net/p?https://mmbiz.qpic.cn/mmbiz_png/z5Pdm7u7moYdbN4pBBfhfvXYrP8jEZMuuXNvhFbnEkmrSic8y8acbyMuTlgm6Ztb1vHWdAFDpIWnO5G2lSfgcMA/640?wx_fmt=png" alt="640?wx_fmt=png"></p><h3><span>查找所有父级实例</span></h3><p><span>根据当前节点实例查找他的所有父级实例存入path</span></p><pre class="has"><code class="language-javascript">/**       
*       
* @param {obj} inst 当前节点实例       
* @param {function} fn 处理方法       
* @param {obj} arg 合成事件对象       
*/       
function traverseTwoPhase(inst, fn, arg) {       
    var path = [];//存放所有实例 ReactDOMComponent       
    while (inst) {       
      path.push(inst);       
      inst = inst._hostParent;//层级关系       
    }       
    var i;       
    for (i = path.length; i-- &gt; 0;) {       
      fn(path, 'captured', arg);//处理捕获 ,反向处理数组       
    }       
    for (i = 0; i &lt; path.length; i++) {       
      fn(path, 'bubbled', arg);//处理冒泡,从0开始处理,我们直接看冒泡       
    }       
}</code></pre><p><span>看下 path 长啥样</span></p><p><img src="https://ss.csdn.net/p?https://mmbiz.qpic.cn/mmbiz_png/z5Pdm7u7moYdbN4pBBfhfvXYrP8jEZMuUIngIR7NlJBghwRforX1yvb54VAnoOJPV6iaOuM3YiboBbtG4RzYDFDg/640?wx_fmt=png" alt="640?wx_fmt=png"></p><h3><span>事件合成结束</span></h3><p><span>在listenerBank查找事件回调并合成到 event。</span></p><p><span>紧接着上面代码</span></p><pre class="has"><code class="language-javascript">fn(path, 'bubbled', arg);</code></pre><p><span>上面的代码会调用下面这个方法,在 </span><code class="prettyprint code-in-text prettyprinted"><span>listenerBank</span></code><span>中查找到事件回调,并存入合成事件对象。</span></p><pre class="has"><code class="language-javascript">/**EventPropagators.js       
* 查找事件回调后,把实例和回调保存到合成对象内       
* @param {obj} inst 组件实例       
* @param {string} phase 事件类型       
* @param {obj} event 合成事件对象       
*/       
function accumulateDirectionalDispatches(inst, phase, event) {       
    var listener = listenerAtPhase(inst, event, phase);       
    if (listener) {//如果找到了事件回调,则保存起来 (保存在了合成事件对象内)       
      event._dispatchListeners = accumulateInto(event._dispatchListeners, listener);//把事件回调进行合并返回一个新数组       
      event._dispatchInstances = accumulateInto(event._dispatchInstances, inst);//把组件实例进行合并返回一个新数组       
    }       
}       
/**       
* EventPropagators.js       
* 中间调用方法 拿到实例的回调方法       
* @param {obj} inst实例       
* @param {obj} event 合成事件对象       
* @param {string} propagationPhase 名称,捕获capture还是冒泡bubbled       
*/       
function listenerAtPhase(inst, event, propagationPhase) {       
    var registrationName = event.dispatchConfig.phasedRegistrationNames;       
    return getListener(inst, registrationName);       
}       
/**EventPluginHub.js       
* 拿到实例的回调方法       
* @param {obj} inst 组件实例       
* @param {string} registrationName Name of listener (e.g. `onClick`).       
* @return {?function} 返回回调方法       
*/       
getListener: function getListener(inst, registrationName) {       
    var bankForRegistrationName = listenerBank;       
    if (shouldPreventMouseEvent(registrationName, inst._currentElement.type, inst._currentElement.props)) {       
      return null;       
    }       
    var key = getDictionaryKey(inst);       
    return bankForRegistrationName &amp;&amp; bankForRegistrationName;       
}</code></pre><p><img src="https://ss.csdn.net/p?https://mmbiz.qpic.cn/mmbiz_png/z5Pdm7u7moYdbN4pBBfhfvXYrP8jEZMuWwMl8iciapzvnYp5t636E5qiaCGOfoic6Qw3WDekqx2JO5p4z480zgCOpg/640?wx_fmt=png" alt="640?wx_fmt=png"></p><p><span>为什么能够查找到的呢?</span></p><p><span>因为 inst (组件实例)里有_rootNodeID,所以也就有了对应关系。</span></p><p><img src="https://ss.csdn.net/p?https://mmbiz.qpic.cn/mmbiz_png/z5Pdm7u7moYdbN4pBBfhfvXYrP8jEZMueKK99gAJdBIWkowaZSzOH7N97NP4wISgOsbhy065RSGzfhkAn8Zv0g/640?wx_fmt=png" alt="640?wx_fmt=png"></p><p><span>到这里事件合成对象生成完成,所有的事件回调已保存到了合成对象中。</span></p><h3><span>批量处理事件合成对象</span></h3><p><span>批量处理合成事件对象内的回调方法(事件触发完成 end)。</span></p><p><span>生成完 合成事件对象后,调用栈回到了我们起初执行的方法内。</span></p><p><img src="https://ss.csdn.net/p?https://mmbiz.qpic.cn/mmbiz_png/z5Pdm7u7moYdbN4pBBfhfvXYrP8jEZMuDJjsU8lUnTaVXPsqDS2o5HduVkcm7O9ro5nNic6k0lc29ATf0aiaXjxQ/640?wx_fmt=png" alt="640?wx_fmt=png"></p><pre class="has"><code class="language-javascript">//在这里执行事件的回调       
runEventQueueInBatch(events);</code></pre><p><img src="https://ss.csdn.net/p?https://mmbiz.qpic.cn/mmbiz_png/z5Pdm7u7moYdbN4pBBfhfvXYrP8jEZMuicMMUWnndlibw4HibSG9ibibvVFdNQfbniaVOmIIOl3SkgxQ7XTGCAUAibEGw/640?wx_fmt=png" alt="640?wx_fmt=png"></p><p><span>到下面这一步中间省略了一些代码,只贴出主要的代码,下面方法会循环处理 合成事件内的回调方法,同时判断是否禁止事件冒泡。</span></p><p><img src="https://ss.csdn.net/p?https://mmbiz.qpic.cn/mmbiz_png/z5Pdm7u7moYdbN4pBBfhfvXYrP8jEZMuXC0tdfwqjBhwrrWEUlDoCxgZslvHXGrQmccrN2hYHZqWmk7FgBzThQ/640?wx_fmt=png" alt="640?wx_fmt=png"></p><p><span>贴上最后的执行回调方法的代码</span></p><pre class="has"><code class="language-javascript">/**       
*       
* @param {obj} event 合成事件对象       
* @param {boolean} simulated false       
* @param {fn} listener 事件回调       
* @param {obj} inst 组件实例       
*/       
function executeDispatch(event, simulated, listener, inst) {       
    var type = event.type || 'unknown-event';       
    event.currentTarget = EventPluginUtils.getNodeFromInstance(inst);       
    if (simulated) {//调试环境的值为 false,按说生产环境是 true       
      //方法的内容请往下看       
      ReactErrorUtils.invokeGuardedCallbackWithCatch(type, listener, event);       
    } else {       
      //方法的内容请往下看       
      ReactErrorUtils.invokeGuardedCallback(type, listener, event);       
    }       
    event.currentTarget = null;       
}       
/** ReactErrorUtils.js       
* @param {String} name of the guard to use for logging or debugging       
* @param {Function} func The function to invoke       
* @param {*} a First argument       
* @param {*} b Second argument       
*/       
var caughtError = null;       
function invokeGuardedCallback(name, func, a) {       
    try {       
      func(a);//直接执行回调方法       
    } catch (x) {       
      if (caughtError === null) {       
            caughtError = x;       
      }       
    }       
}       
var ReactErrorUtils = {       
    invokeGuardedCallback: invokeGuardedCallback,       
    invokeGuardedCallbackWithCatch: invokeGuardedCallback,       
    rethrowCaughtError: function rethrowCaughtError() {       
      if (caughtError) {       
            var error = caughtError;       
            caughtError = null;       
            throw error;       
      }       
    }       
};       
if (process.env.NODE_ENV !== 'production') {//非生产环境会通过自定义事件去触发回调       
    if (typeof window !== 'undefined' &amp;&amp; typeof window.dispatchEvent === 'function' &amp;&amp; typeof document !== 'undefined' &amp;&amp; typeof document.createEvent === 'function') {       
      var fakeNode = document.createElement('react');       
      ReactErrorUtils.invokeGuardedCallback = function (name, func, a) {       
            var boundFunc = func.bind(null, a);       
            var evtType = 'react-' + name;       
            fakeNode.addEventListener(evtType, boundFunc, false);       
            var evt = document.createEvent('Event');       
            evt.initEvent(evtType, false, false);       
            fakeNode.dispatchEvent(evt);       
            fakeNode.removeEventListener(evtType, boundFunc, false);       
      };       
    }       
}</code></pre><p><img src="https://ss.csdn.net/p?https://mmbiz.qpic.cn/mmbiz_png/z5Pdm7u7moYdbN4pBBfhfvXYrP8jEZMutlEXfSm2Jpwyic5gQ131MyL0MchtDazD5wfjYgnwp1LyD1psPpjKhCg/640?wx_fmt=png" alt="640?wx_fmt=png"></p><p><span>最后react 通过生成了一个临时节点fakeNode,然后为这个临时元素绑定事件处理程序,然后创建自定义事件 Event,通过fakeNode.dispatchEvent方法来触发事件,并且触发完毕之后立即移除监听事件。</span></p><p><span>到这里事件回调已经执行完成,但是也有些疑问,为什么在非生产环境需要通过自定义事件来执行回调方法。可以看下上面的代码在非生产环境对 </span><code class="prettyprint code-in-text prettyprinted"><span>ReactErrorUtils.invokeGuardedCallback</span></code> <span>方法进行了重写。</span></p><h1><span>总结</span></h1><p><span>主要是从整体流程上介绍了下 react事件的原理,其中并没有深入到源码的各个细节,包括事务处理、合成的细节等,另外梳理过程中自己也有一些疑惑的地方,感觉说原理还能比较容易理解一些,但是一结合源码来写就会觉得乱,因为 react代码过于庞大,而且盘根错节,很难抽离,对源码有兴趣的小伙儿可以深入研究下,当然还是希望本文能够带给你一些启发,若文章有表述不清或有问题的地方欢迎留言、
交流、斧正。</span></p><h1><span>参考资料</span></h1><ul class="list-paddingleft-2"><li><p><span>https://zhuanlan.zhihu.com/p/35468208</span></p></li><li><p><span>https://react.docschina.org/docs/events.html</span></p></li></ul><p></p><div><div class="_135editor"><div><div><div>▼</div><div class="135brush">原创系列推荐</div><div>▼</div></div><div><div class="135brush"><br></div><div class="135brush"><br></div><div class="135brush"><br></div><div class="135brush"><span>4.&nbsp;</span></div><div class="135brush"><span>5.&nbsp;</span></div><div class="135brush"><span>6.&nbsp;</span></div><div class="135brush"><span>7.&nbsp;</span></div></div></div></div></div><p></p><p>回复“<span><strong>加群</strong></span>”与大佬们一起交流学习~</p><div><div><div><img src="https://ss.csdn.net/p?https://mmbiz.qpic.cn/mmbiz_png/dy9CXeZLlCXVLQpUrib6FP2ubLv4xQr3O1Yic7E36SCGNZ92I2TIboy79WKWTEjIia45SbfcJZ1Kk7g9jVQK1akTQ/640?wx_fmt=png" alt="640?wx_fmt=png"></div><div><div><div><span>点这,与大家一起分享本文吧~</span></div></div></div></div></div>
                </div>
                                    

</div>
<div id="MySignature" role="contentinfo">
    个人博客:http://www.pingan8787.com

微信公众号【前端自习课】和千万网友一起,每日清晨,享受一篇前端优秀文章。

目前已连续推送文章 600+ 天,愿每个人的初心都能一直坚持下去!<br><br>
来源:https://www.cnblogs.com/pingan8787/p/11838083.html
頁: [1]
查看完整版本: 【React】354- 一文吃透 React 事件机制原理