结合源码彻底理解-react事件机制原理-03-事件注册

4次阅读

共计 3334 个字符,预计需要花费 9 分钟才能阅读完成。

前言

这是 react 事件机制的第三节 – 事件注册,通过本文你将了解 react 事件的注册过程,以及在这个过程中主要经过了哪些关键步骤,同时结合源码进行验证和增强理解。

文章涉及到的源码是基于 react15.6.1 版本,虽然不是最新版本但是也不会影响我们对 react 事件机制的整体把握和理解。

文中不会说非常细节的内容,而是会把大概的流程和原理性的内容进行介绍,做到对整体流程有个认知和理解。

内容大纲

  1. 主要做两件事(事件注册、事件存储)
  2. 大致流程
  3. 具体执行过程
  4. 总结

1. 主要做两件事

按照我的理解,react 事件注册过程其实主要做了 2 件事:

a. 事件注册

b. 事件存储

a. 事件注册 – 组件挂载阶段,根据组件内的声明的事件类型 -onclick,onchange 等,给 document 上添加事件 -addEventListener,并指定统一的事件处理程序 dispatchEvent。

b. 事件存储 – 就是把 react 组件内的所有事件统一的存放到一个地方,也就是缓存起来,可以理解成放入一个对象内,为了在触发事件的时候可以查找到对应的方法去执行。

再配个图

2. 大致流程

上面大致说了事件注册需要完成的两个目标,那完成目标的过程需要经过哪些关键处理呢?

首先 react 拿到将要挂载的组件的虚拟 dom(其实就是 react dom,类似一个对象),然后处理 react dom 的 props,判断属性内是否有声明为事件的属性,比如 onclick, 这个时候得到事件类型 click 和对应的事件处理程序 fn,然后直行后面 3 步

a. 执行事件注册

b. 将 react dom,事件类型,处理函数 fn 放入数组存储

c. 组件挂载完成后,处理 b 步骤生成的数组,经过遍历把事件处理函数存储到 listenerBank 中

再配个图

3. 具体执行过程

3.1 得先从 jsx 说起

看个最熟悉的代码,也是我们日常的写法

handleFatherClick=()=>{}

    handleChildClick=()=>{}

    render(){
        return <div className="box">
                    <div className="father" onClick={this.handleFatherClick}>
                        <div className="child" onClick={this.handleChildClick}>child </div>
                    </div>
               </div>
    }

经过 babel 编译后,可以看到最终调用的方法是 react.createElement, 而且声明的事件类型和回调也是一个 props。

react.createElement 执行的结果会返回一个所谓的虚拟 dom(react element 或者 react dom), 看下图

3. 2 开始处理 props,拿到事件类型和回调 fn

ReactDOMComponent 在进行组件加载 (mountComponent)、更新(updateComponent) 的时候,需要对 props 进行处理(_updateDOMProperties):


可以看下 registrationNameModules 的内容,就不细说了。

3.3 注册事件和事件的存储

【注册事件】

接着上面的代码执行到了这个方法

          enqueuePutListener(this, propKey, nextProp, transaction);

在这个方法里会进行事件的注册以及事件的存储,包括冒泡和捕获的处理

根据当前的组件实例获取获取到最高父级 - 也就是 document,然后执行方法 listenTo – 也是最关键的一个方法,进行事件绑定处理

源码文件:ReactBrowerEventEmitter.js

最后执行 EventListener.listen(冒泡)或者 EventListener.capture(捕获),

单看下冒泡的注册,其实就是 addEventListener 的第三个参数是 false

也可以看到注册事件的时候也对 ie 做了兼容。

上面没有看到 dispatchEvent 的定义,下面可以看到传入 dispatchEvent 方法的代码。

到这里事件注册就完事儿了。

【事件存储】

下一步开始事件的存储,在 react 里所有事件的触发都是通过 dispatchEvent 方法统一进行派发的,而不是在注册的时候直接注册声明的回调,来看下如何存储的。

【事件存储结论】

react 把所有的事件和事件类型以及 react 组件进行关联,把这个关系保存在了一个 map 里,也就是一个对象里(键值对),然后在事件触发的时候去根据当前的组件 id 和事件类型查找到对应的事件。

再加个简易图

看源码:

function enqueuePutListener(inst, registrationName, listener, transaction) {

  var containerInfo = inst._hostContainerInfo;
  var isDocumentFragment = containerInfo._node && 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);
}

大致的流程就是执行完 listenTo(事件注册),然后执行 putListener 方法进行事件存储,所有的事件都会存储到一个对象中 – listenerBank,具体由 EventPluginHub 进行管理。

 // 拿到组件唯一标识 id
    var getDictionaryKey = function getDictionaryKey(inst) {return '.' + inst._rootNodeID;}

   putListener: function putListener(inst, registrationName, listener) {

    // 得到组件 id
        var key = getDictionaryKey(inst);

        // 得到 listenerBank 对象中指定事件类型的对象
        var bankForRegistrationName = listenerBank[registrationName] || (listenerBank[registrationName] = {});

        // 存储回调 fn
        bankForRegistrationName[key] = listener;

        //....
  }

listenerBank 其实就是一个二级 map,这样的结构更方便事件的查找。

这里的组件 id 就是组件的唯一标识,然后和 fn 进行关联,在触发阶段就可以找到相关的事件回调。

看下 listenerBank 结构:

看到这个结构是不是很熟悉呢?就是我们平常使用的 object.

到这里大致的流程已经说完,是不是感觉有点明白又不大明白。

没关系,再来个详细的图,重新理解下

4. 最后

本文主要是从整体流程上介绍了下 react 事件中事件的注册过程,并没有深入到源码的细节,有兴趣的小伙儿可以自查下源码,也希望本文能够带给你一些启发,若文章有表述不清或有问题的地方欢迎留言交流。

更多精彩内容欢迎关注我的公众号 - 前端张大胖

正文完
 0