React源码解析(四):事件系统

12,523 阅读5分钟

笔者将编写"React源码解析"系列文章三到四篇,阐述React内部的机制。欢迎大家关注我的掘金账号,以便能及时看到最新的文章更新推送。

在前面三篇文章中,我们阐述了react组件的构成与生命周期,setState的机制。这次我们来谈谈React的事件处理。

1.原生事件系统

我们通常监听真实DOM。举🌰来说,我们想监听按钮的点击事件,那么我们在按钮DOM上绑定事件和对应的回调函数即可。 遗憾的是若页面复杂且事件处理频率高,那么对网页性能是个考验。

2.React事件系统

react的事件处理再眼花缭乱终究还是要回归原生的事件系统,但它做的封装却很优雅。我们直接上结论:

  • React实现了SyntheticEvent层处理事件

什么意思呢?详细来说,React并不像原生事件一样将事件和DOM一一对应,而是将所有的事件都绑定在网页的document,通过统一的事件监听器处理并分发,找到对应的回调函数并执行。按照官方文档的说法,事件处理程序将传递SyntheticEvent的实例,那么接下来我们一探SyntheticEvent的究竟。

3.SyntheticEvent

1.事件注册

上文说到,既然React对事件统一进行处理,那么肯定需要先注册程序员写的事件触发函数吧?那么这个过程是在哪里执行的呢?因为我们是把事件"绑定"在"组件DOM"上,例如一个点击事件:

<Component onClick={this.handleClick}/>

其实在这个组件挂载的时候,React就已经开始通过mountCompoent内部的_updateDOMProperties方法进行事件处理了。在这个方法中,执行的是enqueuePutListener方法去注册事件:

顺藤摸瓜,listenTo方法关键调用了以下两个函数:

  • trapBubbledEvent
  • trapCapturedEvent

熟悉原生事件系统的读者从英文翻译就能知道,两个函数是用来处理事件捕获和事件冒泡的。具体处理逻辑不分析,我们直接看这两个函数内部:

上述代码中的target也就是document,也看到了熟悉的document.addEventListenerdocument.removeEventListener。正是这样统一的事件绑定减少了内存的开销。

2.事件存储

我们写的事件回调函数注册完毕后需要存储起来,以便触发时进行回调。存储的入口是EventPluginHub.putListener函数:

可见所有的回调函数都以二维数组的形式存储在listenerBank中,根据组件对应的key来进行管理。

3.事件分发

事件注册和事件存储我们已经清楚了,现在我们看下当事件触发时,React是如何进行事件分发和找到对应回调函数并执行的。分发入口在ReactDOMEventListener.jshandleTopLevelImpl:

上述代码我们理清了流程:因为事件回调函数执行后可能导致DOM结构的变化,那么React先将当前的结构以数组的形式存储起来,依次遍历执行。 上述函数的_handleTopLevel最终对回调函数进行处理,看下源码:

代码中出现了新角色:EventPluginHub.extractEvents。查阅相关资料,得知extractEvents方法是用于合成事件的,也就是根据事件类型的不同,合成不同的跨浏览器的SyntheticEvent对象的实例,比如SyntheticClickEvent。而EventPluginHub顾名思义是React进行合成事件时所用的工具插件:

可以看到对于不同的事件,React将使用不同的功能插件,这些插件都是通过依赖注入的方式进入内部使用的。React合成事件的过程非常繁琐,但可以概括出extractEvents函数内部主要是通过switch函数区分事件类型并调用不同的插件进行处理从而生成SyntheticEvent实例。有兴趣的同学可以自行了解。

4.事件处理

React处理事件的思想与处理setState的思想类似,都是采用批处理的方法。在上面handleTopLevel方法中我们看到最后执行了runEventQueueInBatch方法:

    //事件进入队列
    EventPluginHub.enqueueEvents(events);
    //...
    EventPluginHub.processEventQueue(false);

看下processEventQueue

上述代码遍历队列中的事件,并进入executeDispatchesAndReleaseSimulated

event.constructor.release(event);

这行代码将React的合成事件release掉,减少内存开销。事件处理的核心入口在executeDispatchesInOrder:

var dispatchListeners = event._dispatchListeners;
var dispatchInstances = event._dispatchInstances;

executeDispatch(event, simulated, dispatchListeners[i], dispatchInstances[i]);

重要的代码就这三行,dispatchListeners是事件回调函数,dispatchInstances是对应的组件,将这些参数传入executeDispatch后:

function executeDispatch(event, simulated, listener, inst) {
    var type = event.type || 'unknown-event';
    ReactErrorUtils.invokeGuardedCallback(type, listener, event);
}

invokeGuardedCallback就相当简单了:

function invokeGuardedCallback(name, func, a) {
    func(a);
}

上面的func(a)其实就是listener(event),再往上追溯,就是dispatchListeners(dispatchInstances),这也就说明为什么我们的React事件回调函数可以拿到原生的事件了。

4.总结

React事件系统为了兼容各种版本的浏览器而做了大量工作,我们不必钻牛角尖去研究这些是如何实现的,与原生事件不同的点,只在于React对事件进行统一而不是分散的存储与管理,捕获事件后内部生成合成事件提高浏览器的兼容度,执行回调函数后再进行销毁释放内存,从而大大提高网页的响应性能。

回顾:
《React源码解析(一):组件的实现与挂载》
《React源码解析(二):组件的类型与生命周期》
《React源码解析(三):详解事务与队列》
联系邮箱:ssssyoki@foxmail.com