react源码阅读笔记(8)react事件的设计

本文使用的是react 15.6.1的代码

我们先看一段代码

import React, {Component} from "react";
import "./App.css";

class App extends Component {
    componentDidMount() {
        this.refs.div.addEventListener('click', function () {
            alert('dom event');
        })
    }

    componentClick(event) {
        alert('react event');
        event.stopPropagation();
    }

    render() {
        return (
            <div ref="div">
                <button ref="button" onClick={this.componentClick.bind(this)}>
                    button
                </button>
            </div>
        );
    }
}

export default App;

当我们点击按钮的时候,会弹出哪一些框?
答案是分别弹出 dom event以及react event,如果只了解dom事件的童鞋也许会问,在componentClick中明明写了stopPropagation,事件应该不会冒泡到父元素了啊?对此有疑惑的童鞋可以看看下面的分析,帮助你更进一步了解react的事件系统。

事件的注册

熟悉react的同学应该知道,在react中,几乎所有的事件都被代理到了document之上,以达到优化的目的,试想,如果react组件又1000个列表,每个列表都给他绑定一个click事件是多么费性能的事情,即便不用react,我们也会使用代理来处理这样绑定事件过多对性能的开销。既然事件大部分都绑定到了document上,所以stopPropagation没有起到相应的作用,我们来看看是如何绑定的;
在ReactDomComponent.js中有这样一段代码,react组件渲染props的时候,发现如果是事件属性,则会调用这样一个函数enqueuePutListener

function enqueuePutListener(inst, registrationName, listener, transaction) {
  if (transaction instanceof ReactServerRenderingTransaction) {
    return;
  }
  //实体对象中dom节点信息
  var containerInfo = inst._hostContainerInfo;
  var isDocumentFragment =
    containerInfo._node && containerInfo._node.nodeType === DOC_FRAGMENT_TYPE;
  /**
   * ownerDocument:该节点的顶层document
   * [ownerDocument](https://developer.mozilla.org/zh-CN/docs/Web/API/Node/ownerDocument)
   */
  var doc = isDocumentFragment
    ? containerInfo._node
    : containerInfo._ownerDocument;
  //注册事件
  listenTo(registrationName, doc);
  transaction.getReactMountReady().enqueue(putListener, {
    inst: inst,
    registrationName: registrationName,
    listener: listener,
  });
}

在这段代码中,简单做了这样一件事情,首先获取到真实节点,判断节点是否是document,如果不是,则将其节点的顶层document以及事件名传入listenTo方法,对应的listenToReactBrowserEventEmitter.js中,如下

listenTo: function(registrationName, contentDocumentHandle) {
    // 事件挂载节点,大部分是document
    var mountAt = contentDocumentHandle;
    var isListening = getListeningForDocument(mountAt);
    var dependencies =
      EventPluginRegistry.registrationNameDependencies[registrationName];

    for (var i = 0; i < dependencies.length; i++) {
      var dependency = dependencies[i];
      if (
        !(isListening.hasOwnProperty(dependency) && isListening[dependency])
      ) {
        if (dependency === 'topWheel') {
          //.....
        } else if (dependency === 'topScroll') {
          //.....
        } else if (dependency === 'topFocus' || dependency === 'topBlur') {
          //.....
        } else if (topEventMapping.hasOwnProperty(dependency)) {

          //注册冒泡事件
          ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(
            dependency,
            topEventMapping[dependency],
            mountAt,
          );
        }

        isListening[dependency] = true;
      }
    }
  },

在这个方法中,着重对不同浏览器捕获与冒泡不同进行兼容处理,同时,对wheel scroll等事件进行了特殊处理,调用trapCapturedEventtrapBubbledEvent来注册捕获和冒泡事件,我们来看看trapBubbledEvent的实现

return ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(
      topLevelType,
      handlerBaseName,
      handle,
    );

代码很简单,调用了ReactEventListenertrapBubbledEvent方法,可是ReactEventListener对象是从哪里来的呢?在【react源码阅读笔记(3)batchedUpdates与Transaction】介绍过React在启动后会调用inject方法注入一些对象,在ReactDefaultInjection.js中执行了

ReactInjection.EventEmitter.injectReactEventListener(ReactEventListener);

因此,在默认情况下 ReactBrowserEventEmitter.ReactEventListener即为注入的ReactEventListener对象

//ReactEventListener.trapBubbledEvent
trapBubbledEvent: function(topLevelType, handlerBaseName, element) {
    if (!element) {
      return null;
    }
    return EventListener.listen(
      element,
      handlerBaseName,
      //事件回调,注意
      ReactEventListener.dispatchEvent.bind(null, topLevelType),
    );
  }

这个方法只是简单判断了一下元素是否存在,随后调用EventListener.listen注册事件,EventListener.js位于node_modules/fbjs/lib

listen: function listen(target, eventType, callback) {
    if (target.addEventListener) {
      target.addEventListener(eventType, callback, false);
      return {
        remove: function remove() {
          target.removeEventListener(eventType, callback, false);
        }
      };
    } else if (target.attachEvent) {
      target.attachEvent('on' + eventType, callback);
      return {
        remove: function remove() {
          target.detachEvent('on' + eventType, callback);
        }
      };
    }
  }

代码更加简单了,就是对不同浏览器的addEventListener做兼容处理,到这里我们终于找到了事件注册的源头

但是我们在ReactEventListener.trapBubbledEvent发现了,绑定到dom上的事件回调,并不是我们在react写的对应的回调函数,而是ReactEventListener.dispatchEvent.bind(null, topLevelType),这个又是什么呢?

// topLevelType:带top的事件名,如topClick。不用纠结为什么带一个top字段,知道它是事件名就OK了
// nativeEvent: 用户触发click等事件时,浏览器传递的原生事件
dispatchEvent: function(topLevelType, nativeEvent) {
    if (!ReactEventListener._enabled) {
      return;
    }

    var bookKeeping = TopLevelCallbackBookKeeping.getPooled(
      topLevelType,
      nativeEvent,
    );
    try {
      // Event queue being processed in the same cycle allows
      // `preventDefault`.
      ReactUpdates.batchedUpdates(handleTopLevelImpl, bookKeeping);
    } finally {
      TopLevelCallbackBookKeeping.release(bookKeeping);
    }
  }

简单总结就是

  1. 对dom组件进行遍历,如果是事件的话调用 enqueuePutListener
  2. 对该dom找到其document,绑定对应的事件名
  3. document不管注册的是什么事件,具有统一的回调函数handleTopLevelImpl

事件存储

那么,真正的事件回调在哪里呢?
回到 enqueuePutListener方法,在listenTo后面执行了方法enqueue,把真正的事件绑定回调listener最为参数传入putListener函数中,也就是说,在ReactReconcileTransaction事务的close阶段执行notifyAll的时候就会调用putListener方法,

transaction.getReactMountReady().enqueue(putListener, {
    inst: inst,
    registrationName: registrationName,
    listener: listener,
  });
  
function putListener() {
  var listenerToPut = this;
  EventPluginHub.putListener(
    listenerToPut.inst,
    listenerToPut.registrationName,
    listenerToPut.listener,
  );
}

在这里,调用了 EventPluginHub.putListener方法,将对应的组件对象,事件名以及回调作为参数传递

putListener: function(inst, registrationName, listener) {

    // return '.' + inst._rootNodeID;
    var key = getDictionaryKey(inst);
    // 将所绑定的事件存入 listenerBank中,
    var bankForRegistrationName =
      listenerBank[registrationName] || (listenerBank[registrationName] = {});

    // 将对应的事件回调放入 bankForRegistrationName中,通过id区分不同组件当时相同的事件,如组件A和B都有click事件,通过id进行区分
    bankForRegistrationName[key] = listener;

    var PluginModule =
      EventPluginRegistry.registrationNameModules[registrationName];
    if (PluginModule && PluginModule.didPutListener) {
      PluginModule.didPutListener(inst, registrationName, listener);
    }
  }

在这里,react将不同时间存入listenerBank数组中,然后通过NodeId区分不同节点具体所绑定的事件

事件执行

上面说到,react中会将真正的事件回调函数通过不同id,塞入listenerBank中,大部分事件都是绑定到document上,并使用dispatchEvent进行分发事件,最后调用了handleTopLevelImpl,那么来看看handleTopLevelImpl事件做了些什么

function handleTopLevelImpl(bookKeeping) {
  // 获取这个event的真是dom元素,如果是text节点,则返回父容器
  var nativeEventTarget = getEventTarget(bookKeeping.nativeEvent);
  // 获取事件节点所对应的react实例
  var targetInst = ReactDOMComponentTree.getClosestInstanceFromNode(
    nativeEventTarget,
  );

  var ancestor = targetInst;
  // 将该组件所有父节点放入ancestors,因为事件可能会改变父节点结构,因此在执行事件回调之前缓存当前parent
  do {
    bookKeeping.ancestors.push(ancestor);
    ancestor = ancestor && findParent(ancestor);
  } while (ancestor);


  for (var i = 0; i < bookKeeping.ancestors.length; i++) {
    targetInst = bookKeeping.ancestors[i];
    // 从当前组件开始执行注册的事件,模拟冒泡操作
    ReactEventListener._handleTopLevel(
      bookKeeping.topLevelType,
      targetInst,
      bookKeeping.nativeEvent,
      getEventTarget(bookKeeping.nativeEvent),
    );
  }
}

从源码中看出handleTopLevelImpl主要是模拟了事件的冒泡操作,拿到事件的注册对象,然后依次找到其父级组件,调用ReactEventListener._handleTopLeve

ReactEventListener._handleTopLeve本质上是调用ReactBrowserEventEmitter.handleTopLevel的方法,该代码位于ReactEventEmitterMixin中

handleTopLevel: function(
    topLevelType,
    targetInst,
    nativeEvent,
    nativeEventTarget,
  ) {
    //合成对应的事件
    var events = EventPluginHub.extractEvents(
      topLevelType,
      targetInst,
      nativeEvent,
      nativeEventTarget,
    );
    //将事件放入队列中
    runEventQueueInBatch(events);
  },

在这里,我们发现,这里会将组建实例,具体事件对象等参数调用extractEvents合成react的事件对象,来看看react将事件是如何合成的

事件合成

EventPluginHub.extractEvents方法

extractEvents: function(
    topLevelType,
    targetInst,
    nativeEvent,
    nativeEventTarget,
  ) {
    var events;
    // 获取事件插件,
    var plugins = EventPluginRegistry.plugins;
    for (var i = 0; i < plugins.length; i++) {
      // 通过各个插件返回事件数组,最后将事件数组合并返回
      var possiblePlugin = plugins[i];
      if (possiblePlugin) {
        var extractedEvents = possiblePlugin.extractEvents(
          topLevelType,
          targetInst,
          nativeEvent,
          nativeEventTarget,
        );
        if (extractedEvents) {
          //该方法比较简单,就是,简单来说就是数组合并,
          // 如果param1和2都是数组,那么调用concat合并
          // 如果其中有一个是对象,将其按顺序插入新的数组之中,
          // 最后返回的一定是数组
          events = accumulateInto(events, extractedEvents);
        }
      }
    }
    return events;
  }

这段代码依然相对简单,合成事件的时候,会依次通过EventPluginRegistry.plugins插件列表来生成对应的事件数组,最后将这个生成的事件合并为一个数组返回,
我们来看看具体对应的插件是什么

在我们的老朋友ReactDefaultInjection中,会自动注册5个插件

ReactInjection.EventPluginHub.injectEventPluginsByName({
    SimpleEventPlugin: SimpleEventPlugin,
    EnterLeaveEventPlugin: EnterLeaveEventPlugin,
    ChangeEventPlugin: ChangeEventPlugin,
    SelectEventPlugin: SelectEventPlugin,
    BeforeInputEventPlugin: BeforeInputEventPlugin,
  });

我们来分析其中一个SimpleEventPlugin插件的extractEvents方法

extractEvents: function(
    topLevelType: TopLevelTypes,
    targetInst: ReactInstance,
    nativeEvent: MouseEvent,
    nativeEventTarget: EventTarget,
  ): null | ReactSyntheticEvent {
    var dispatchConfig = topLevelEventsToDispatchConfig[topLevelType];
    if (!dispatchConfig) {
      return null;
    }
    var EventConstructor;
    switch (topLevelType) {
    //.....
      case 'topCopy':
      case 'topCut':
      case 'topPaste':
        EventConstructor = SyntheticClipboardEvent;
        break;
    }
    // 从缓存池中获取对应的event
    var event = EventConstructor.getPooled(
      dispatchConfig,
      targetInst,
      nativeEvent,
      nativeEventTarget,
    );
    //最后实际调用了ReactDOMTraversal中的traverseTwoPhase方法
    mao.accumulateTwoPhaseDispatches(event);
    return event;
  },

function traverseTwoPhase(inst, fn, arg) {
//这里的fn是EventPropagators中的accumulateDirectionalDispatches方法
  var path = [];
  // 依次寻找父节点
  while (inst) {
    path.push(inst);
    inst = inst._hostParent;
  }
  var i;
  //捕获与冒泡
  for (i = path.length; i-- > 0; ) {
    fn(path[i], 'captured', arg);
  }
  for (i = 0; i < path.length; i++) {
    fn(path[i], 'bubbled', arg);
  }
}

在这里对模拟进行了冒泡与捕获,同时调用了accumulateDirectionalDispatches方法,来看看fn中处理了什么

function accumulateDispatches(inst, ignoredDirection, event) {
  if (event && event.dispatchConfig.registrationName) {
    var registrationName = event.dispatchConfig.registrationName;
    var listener = getListener(inst, registrationName);
    if (listener) {
      // 将具体的event以及实例放入event下面的数组之中
      event._dispatchListeners = accumulateInto(
        event._dispatchListeners,
        listener,
      );
      event._dispatchInstances = accumulateInto(event._dispatchInstances, inst);
    }
  }
}

参考上文,也就是说,合成的event保存了绑定事件节点到其父节点/实例的所有listener以及inst,但是我们还没有看到具体的作用,我们继续分析。

事件分发

事件已经由插件合成完毕,我们回到 handleTopLevel方法,看看 runEventQueueInBatch(events)具体做了些什么?

function runEventQueueInBatch(events) {
  // 先将events事件放入队列中,不做分析,类似push
  EventPluginHub.enqueueEvents(events);
  //触发该事件队列中的所有事件
  EventPluginHub.processEventQueue(false);
}

来看看processEventQueue方法

processEventQueue: function(simulated) {
    // 获取已合成事件队列
    var processingEventQueue = eventQueue;
    eventQueue = null;
    if (simulated) {
      forEachAccumulated(
        processingEventQueue,
        executeDispatchesAndReleaseSimulated,
      );
    } else {
      // 让所有事件执行executeDispatchesAndReleaseTopLevel方法,
      forEachAccumulated(
        processingEventQueue,
        executeDispatchesAndReleaseTopLevel,
      );
    }
    // This would be a good time to rethrow if any of the event handlers threw.
    ReactErrorUtils.rethrowCaughtError();
  }

往下分析

/**
 *
 * @param e 事件队列中的某个
 */
var executeDispatchesAndReleaseTopLevel = function(e) {
  return executeDispatchesAndRelease(e, false);
};
var executeDispatchesAndRelease = function(event, simulated) {
  if (event) {
    EventPluginUtils.executeDispatchesInOrder(event, simulated);

    if (!event.isPersistent()) {
      event.constructor.release(event);
    }
  }
};

executeDispatchesAndReleaseTopLevel最后本质是调用了EventPluginUtils.executeDispatchesInOrder(event, simulated)方法

function executeDispatchesInOrder(event, simulated) {
  // 合成事件中的对象以及事件回调
  var dispatchListeners = event._dispatchListeners;
  var dispatchInstances = event._dispatchInstances;
  if (Array.isArray(dispatchListeners)) {
    for (var i = 0; i < dispatchListeners.length; i++) {
      if (event.isPropagationStopped()) {
        break;
      }
      // Listeners and Instances are two parallel arrays that are always in sync.
      executeDispatch(
        event,
        simulated,
        dispatchListeners[i],
        dispatchInstances[i],
      );
    }
  } else if (dispatchListeners) {
    executeDispatch(event, simulated, dispatchListeners, dispatchInstances);
  }
  event._dispatchListeners = null;
  event._dispatchInstances = null;
}

这段代码相对简单了,如果有处理这个事件的回调函数,就调用executeDispatch进行处理

function executeDispatch(event, simulated, listener, inst) {
  var type = event.type || 'unknown-event';
  // 获取具体dom
  event.currentTarget = EventPluginUtils.getNodeFromInstance(inst);
  
  if (simulated) {
    // 在15.6版本invokeGuardedCallbackWithCatch和下面的invokeGuardedCallback代码是相同的,具体的代码就是执行listener,并将event作为参数
    ReactErrorUtils.invokeGuardedCallbackWithCatch(type, listener, event);
  } else {
    //
    ReactErrorUtils.invokeGuardedCallback(type, listener, event);
  }
  event.currentTarget = null;
}

到这里,拿到真实的dom,将合成的event赋予currentTarget,是不是和原生了event有点神似,最后,执行了所绑定的回调函数,直到这里我们发现了最开始的问题,为什么我们在componentClick中调用的stopPropagation并没有干扰到dom的事件,因为这个时候我们拿到的event并不是真实dom的事件,是完全由react封装实现了标准dom的虚拟事件。

总结

react为什么要做这么一套复杂的事件系统?用原生的不好吗?
我觉得这么设计至少可以有(还有什么好处欢迎告诉我)

  1. 自己实现事件,可以有效的规避不同浏览器对事件的兼容性错误。
  2. 全局事件代理,对系统优化无疑的巨大的性能提升。

参考资料

[React源码分析7 — React合成事件系统](http://blog.csdn.net/u013510838/article/details/612247600

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349

推荐阅读更多精彩内容