React源码解读系列 -- 事件机制

前端之家收集整理的这篇文章主要介绍了React源码解读系列 -- 事件机制前端之家小编觉得挺不错的,现在分享给大家,也给大家做个参考。

文章原地址:前往阅读

本文首先分析React在DOM事件上的架构设计、相关优化、合成事件(Synethic event)对象,从源码层面上做到庖丁解牛的效果。同时,简单介绍下react事件可能会遇到的问题。

1. 总体设计

react在事件处理上具有如下优点:

  • 几乎所有的事件代理(delegate)到document,达到性能优化的目的

  • 对于每种类型的事件,拥有统一的分发函数dispatchEvent

  • 事件对象(event)是合成对象(SyntheticEvent),不是原生的

react内部事件系统实现可以分为两个阶段: 事件注册、事件触发。

2. 事件注册

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

  1. ReactDOMComponent.Mixin = {
  2. _updateDOMProperties: function (lastProps,nextProps,transaction) {
  3. ...
  4. for (propKey in nextProps) {
  5. // 判断是否为事件属性
  6. if (registrationNameModules.hasOwnProperty(propKey)) {
  7. enqueuePutListener(this,propKey,nextProp,transaction);
  8. }
  9. }
  10. }
  11. }
  12. function enqueuePutListener(inst,registrationName,listener,transaction) {
  13. ...
  14. var doc = isDocumentFragment ? containerInfo._node : containerInfo._ownerDocument;
  15. listenTo(registrationName,doc);
  16. transaction.getReactMountReady().enqueue(putListener,{
  17. inst: inst,registrationName: registrationName,listener: listener
  18. });
  19. function putListener() {
  20. var listenerToPut = this;
  21. EventPluginHub.putListener(listenerToPut.inst,listenerToPut.registrationName,listenerToPut.listener);
  22. }
  23. }

代码解析:

  • 在props渲染的时候,如何属性是事件属性,则会用enqueuePutListener进行事件注册

  • 上述transaction是ReactUpdates.ReactReconcileTransaction的实例化对象

  • enqueuePutListener进行两件事情: 在document注册相关的事件;对事件进行存储

2.1 document上事件注册

document的事件注册入口位于ReactBrowserEventEmitter:

  1. // ReactBrowserEventEmitter.js
  2. listenTo: function (registrationName,contentDocumentHandle) {
  3. ...
  4. if (...) {
  5. ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(...);
  6. } else if (...) {
  7. ReactBrowserEventEmitter.ReactEventListener.trapCapturedEvent(...);
  8. }
  9. ...
  10. }
  11.  
  12. // ReactEventListener.js
  13. var ReactEventListener = {
  14. ...
  15. trapBubbledEvent: function (topLevelType,handlerBaseName,element) {
  16. ...
  17. var handler = ReactEventListener.dispatchEvent.bind(null,topLevelType);
  18. return EventListener.listen(element,handler);
  19. },trapCapturedEvent: function (topLevelType,element) {
  20. var handler = ReactEventListener.dispatchEvent.bind(null,topLevelType);
  21. return EventListener.capture(element,handler);
  22. }
  23. dispatchEvent: function (topLevelType,nativeEvent) {
  24. ...
  25. ReactUpdates.batchedUpdates(handleTopLevelImpl,bookKeeping);
  26. ...
  27. }
  28. }
  29. function handleTopLevelImpl(bookKeeping) {
  30. ...
  31. ReactEventListener._handleTopLevel(bookKeeping.topLevelType,targetInst,bookKeeping.nativeEvent,getEventTarget(bookKeeping.nativeEvent));
  32. ...
  33. }

代码解析:

  • 事件的注册、触发,具体是在ReactEventListener中实现的

  • 事件的注册有两个方法: 支持冒泡(trapBubbledEvent)、trapCapturedEvent

  • document不管注册的是什么事件,具有统一的回调函数handleTopLevelImpl

  • document的回调函数中不包含任何的事物处理,只起到事件分发的作用

2.2 回调函数存储

函数的存储,在ReactReconcileTransaction事务的close阶段执行:

  1. transaction.getReactMountReady().enqueue(putListener,{
  2. inst: inst,listener: listener
  3. });
  4. function putListener() {
  5. var listenerToPut = this;
  6. EventPluginHub.putListener(listenerToPut.inst,listenerToPut.listener);
  7. }

事件的存储由EventPluginHub来进行管理,来看看其中的具体实现:

  1. //
  2. var listenerBank = {};
  3. var getDictionaryKey = function (inst) {
  4. return '.' + inst._rootNodeID;
  5. }
  6. var EventPluginHub = {
  7. putListener: function (inst,listener) {
  8. ...
  9. var key = getDictionaryKey(inst);
  10. var bankForRegistrationName = listenerBank[registrationName] || (listenerBank[registrationName] = {});
  11. bankForRegistrationName[key] = listener;
  12. ...
  13. }
  14. }

react中的所有事件的回调函数均存储在listenerBank对象里面,根据事件类型、component对象的_rootNodeID为两个key,来存储对应的回调函数

3. 事件的执行

事件注册完之后,就可以依据事件委托进行事件的执行。由事件注册可以知道,几乎所有的事件均委托到document上,而document上事件的回调函数只有一个: ReactEventListener.dispatchEvent,然后进行相关的分发:

  1. var ReactEventListener = {
  2. dispatchEvent: function (topLevelType,bookKeeping);
  3. ...
  4. }
  5. }
  6. function handleTopLevelImpl(bookKeeping) {
  7. var nativeEventTarget = getEventTarget(bookKeeping.nativeEvent);
  8. var targetInst = ReactDOMComponentTree.getClosestInstanceFromNode(nativeEventTarget);
  9.  
  10. // 初始化时用ReactEventEmitterMixin注入进来的
  11. ReactEventListener._handleTopLevel(...,nativeEventTarget,targetInst);
  12. }
  13. // ReactEventEmitterMixin.js
  14. var ReactEventEmitterMixin = {
  15. handleTopLevel: function (...) {
  16. var events = EventPluginHub.extractEvents(...);
  17. runEventQueueInBatch(events);
  18. }
  19. }
  20. function runEventQueueInBatch(events) {
  21. EventPluginHub.enqueueEvents(events);
  22. EventPluginHub.processEventQueue(false);
  23. }

代码解析:

  • handleTopLevelImpl: 根据原生的事件对象,找到事件触发的dom元素以及该dom对应的compoennt对象

  • ReactEventEmitterMixin: 一方面生成合成的事件对象,另一方面批量执行定义的回调函数

  • runEventQueueInBatch: 进行批量更新

3.1 合成事件的生成过程

react中的事件对象不是原生的事件对象,而是经过处理后的对象,下面从源码层面解析是如何生成的:

  1. // EventPluginHub.js
  2. var EventPluginHub = {
  3. extractEvents: function (...) {
  4. var events;
  5. var plugins = EventPluginRegistry.plugins;
  6. for (var i = 0; i < plugins.length; i++) {
  7. var possiblePlugin = plugins[i];
  8. if (possiblePlugin) {
  9. var extractedEvents = possiblePlugin.extractEvents(topLevelType,nativeEvent,nativeEventTarget);
  10. if (extractedEvents) {
  11. events = accumulateInto(events,extractedEvents);
  12. }
  13. }
  14. }
  15. return events;
  16. }
  17. }

EventPluginHub不仅存储事件的回调函数,而且还管理其中不同的plugins,这些plugins是在系统启动过程中注入(injection)过来的:

  1. // react-dom模块的入口文件ReactDOM.js:
  2. var ReactDefaultInjection = require('./ReactDefaultInjection');
  3. ReactDefaultInjection.inject();
  4. ...
  5. // ReactDefaultInjection.js
  6. module.exports = {
  7. inject: inject
  8. };
  9. function inject() {
  10. ...
  11. ReactInjection.EventPluginHub.injectEventPluginsByName({
  12. SimpleEventPlugin: SimpleEventPlugin,EnterLeaveEventPlugin: EnterLeaveEventPlugin,ChangeEventPlugin: ChangeEventPlugin,SelectEventPlugin: SelectEventPlugin,BeforeInputEventPlugin: BeforeInputEventPlugin
  13. });
  14. ...
  15. }

从上面代码可以看到,默认情况下,react注入了五种事件plugin,针对不同的事件,得到不同的合成事件,以最常见的SimpleEventPlugin为例进行分析:

  1. var SimpleEventPlugin = {
  2. extractEvents: function (topLevelType,...) {
  3. var EventConstructor;
  4. switch (topLevelType) {
  5. EventConstructor = one of [ SyntheticEvent,SyntheticKeyboardEvent,SyntheticFocusEvent,SyntheticMouseEvent,SyntheticDragEvent,SyntheticTouchEvent,SyntheticAnimationEvent,SyntheticTransitionEvent,SyntheticUIEvent,SyntheticWheelEvent,SyntheticClipboardEvent];
  6. }
  7. var event = EventConstructor.getPooled(dispatchConfig,nativeEventTarget);
  8. EventPropagators.accumulateTwoPhaseDispatches(event);
  9. return event;
  10. }
  11. }

代码解析:

  • 针对不同的事件类型,会生成不同的合成事件

  • EventPropagators.accumulateTwoPhaseDispatches: 用于从EventPluginHub中获取回调函数,后面小节会具体分析获取过程

以其中的最基本的SyntheticEvent为例进行分析:

  1. function SyntheticEvent(dispatchConfig,nativeEventTarget) {
  2. ...
  3. this.dispatchConfig = dispatchConfig;
  4. this._targetInst = targetInst;
  5. this.nativeEvent = nativeEvent;
  6.  
  7. var Interface = this.constructor.Interface;
  8. for (var propName in Interface) {
  9. var normalize = Interface[propName];
  10. if (normalize) {
  11. this[propName] = normalize(nativeEvent);
  12. } else {
  13. if (propName === 'target') {
  14. this.target = nativeEventTarget;
  15. } else {
  16. this[propName] = nativeEvent[propName];
  17. }
  18. }
  19. }
  20. ...
  21. }
  22. _assign(SyntheticEvent.prototype,{
  23. preventDefault: function () { ... },stopPropagation: function () { ... },...
  24. });
  25. var EventInterface = {
  26. type: null,target: null,// currentTarget is set when dispatching; no use in copying it here
  27. currentTarget: emptyFunction.thatReturnsNull,eventPhase: null,bubbles: null,cancelable: null,timeStamp: function (event) {
  28. return event.timeStamp || Date.now();
  29. },defaultPrevented: null,isTrusted: null
  30. };
  31. SyntheticEvent.Interface = EventInterface;
  32.  
  33. // 实现继承关系
  34. SyntheticEvent.augmentClass = function (Class,Interface) {
  35. ...
  36. }

3.2 获取具体的回调函数

上述合成事件对象在生成的过程中,会从EventPluginHub获取相关的回调函数,具体实现如下:

  1. // EventPropagators.js
  2. function accumulateTwoPhaseDispatches(events) {
  3. forEachAccumulated(events,accumulateTwoPhaseDispatchesSingle);
  4. }
  5. function accumulateTwoPhaseDispatchesSingle(event) {
  6. if (event && event.dispatchConfig.phasedRegistrationNames) {
  7. EventPluginUtils.traverseTwoPhase(event._targetInst,accumulateDirectionalDispatches,event);
  8. }
  9. }
  10. function accumulateDirectionalDispatches(inst,phase,event) {
  11. var listener = listenerAtPhase(inst,event,phase);
  12. if (listener) {
  13. event._dispatchListeners = accumulateInto(event._dispatchListeners,listener);
  14. event._dispatchInstances = accumulateInto(event._dispatchInstances,inst);
  15. }
  16. }
  17. var getListener = EventPluginHub.getListener;
  18. function listenerAtPhase(inst,propagationPhase) {
  19. var registrationName = event.dispatchConfig.phasedRegistrationNames[propagationPhase];
  20. return getListener(inst,registrationName);
  21. }
  22. // EventPluginHub.js
  23. getListener: function (inst,registrationName) {
  24. var bankForRegistrationName = listenerBank[registrationName];
  25. var key = getDictionaryKey(inst);
  26. return bankForRegistrationName && bankForRegistrationName[key];
  27. },

3.3 批量执行事件的具体回调函数

react会进行批量处理具体的回调函数,回调函数的执行为了两步,第一步是将所有的合成事件放到事件队列里面,第二步是逐个执行:

  1. var eventQueue = null;
  2. var EventPluginHub = {
  3. enqueueEvents: function (events) {
  4. if (events) {
  5. eventQueue = accumulateInto(eventQueue,events);
  6. }
  7. },processEventQueue: function (simulated) {
  8. var processingEventQueue = eventQueue;
  9. ...
  10. forEachAccumulated(processingEventQueue,executeDispatchesAndReleaseSimulated);
  11. ...
  12. },}
  13. var executeDispatchesAndReleaseSimulated = function (e) {
  14. return executeDispatchesAndRelease(e,true);
  15. };
  16. var executeDispatchesAndRelease = function (event,simulated) {
  17. if (event) {
  18. EventPluginUtils.executeDispatchesInOrder(event,simulated);
  19.  
  20. if (!event.isPersistent()) {
  21. event.constructor.release(event);
  22. }
  23. }
  24. };
  25. // EventPluginUtils.js
  26. function executeDispatchesInOrder(event,simulated) {
  27. var dispatchListeners = event._dispatchListeners;
  28. var dispatchInstances = event._dispatchInstances;
  29. ...
  30. executeDispatch(event,simulated,dispatchListeners,dispatchInstances);
  31. ...
  32. event._dispatchListeners = null;
  33. event._dispatchInstances = null;
  34. }

4. 可能存在的问题

4.1 合成事件与原生事件混用

在开发过程中,有时候需要使用到原生事件,例如存在如下的业务场景: 点击input框展示日历,点击文档其他部分,日历消失,代码如下:

  1. // js部分
  2. var React = require('react');
  3. var ReactDOM = require('react-dom');
  4. class App extends React.Component {
  5. constructor(props) {
  6. super(props);
  7. this.state = {
  8. showCalender: false
  9. };
  10. }
  11. componentDidMount() {
  12. document.addEventListener('click',() => {
  13. this.setState({showCalender: false});
  14. console.log('it is document')
  15. },false);
  16. }
  17. render() {
  18. return (<div>
  19. <input
  20. type="text"
  21. onClick={(e) => {
  22. this.setState({showCalender: true});
  23. console.log('it is button')
  24. e.stopPropagation();
  25. }}
  26. />
  27. <Calendar isShow={this.state.showCalender}></Calendar>
  28. </div>);
  29. }
  30. }

上述代码: 在点击input的时候,state状态变成true,展示日历,同时阻止冒泡,但是document上的click事件仍然触发了?到底是什么原因造成的呢?

原因解读: 因为react的事件基本都是委托到document上的,并没有真正绑定到input元素上,所以在react中执行stopPropagation并没有什么用处,document上的事件依然会触发。

解决办法:

4.1.1 input的onClick事件也使用原生事件

  1. class App extends React.Component {
  2. constructor(props) {
  3. super(props);
  4. this.state = {
  5. showCalender: false
  6. };
  7. }
  8. componentDidMount() {
  9. document.addEventListener('click',false);
  10. this.refs.myBtn.addEventListener('click',(e) => {
  11. this.setState({showCalender: true});
  12. e.stopPropagation();
  13. },false);
  14. }
  15. render() {
  16. return (<div>
  17. <input
  18. type="text"
  19. ref="myBtn"
  20. />
  21. <Calendar isShow={this.state.showCalender}></Calendar>
  22. </div>);
  23. }
  24. }

4.1.2 在document中进行判断,排除目标元素

  1. class App extends React.Component {
  2. constructor(props) {
  3. super(props);
  4. this.state = {
  5. showCalender: false
  6. };
  7. }
  8. componentDidMount() {
  9. document.addEventListener('click',(e) => {
  10. var tar = document.getElementById('myInput');
  11. if (tar.contains(e.target)) return;
  12. console.log('document!!!');
  13. this.setState({showCalender: false});
  14. },false);
  15. }
  16. render() {
  17. return (<div>
  18. <input
  19. id="myInput"
  20. type="text"
  21. onClick={(e) => {
  22. this.setState({showCalender: true});
  23. console.log('it is button')
  24. // e.stopPropagation();
  25. }}
  26. />
  27. <Calendar isShow={this.state.showCalender}></Calendar>
  28. </div>);
  29. }
  30. }

5. 小结

React在设计事件机制的时候,利用冒泡原理充分提高事件绑定的效率,使用EventPluginHub对回调函数、事件插件进行管理,然后通过一个统一的入口函数实现事件的分发,整个设计思考跟jQuery的事件实现上存在相似的地方,非常值得学习借鉴。

猜你在找的React相关文章