一看就晕的React事件机制

前言

本篇文章我们从源码来深挖一下React的事件机制。

TL;DR :

  • react事件机制分为两个部分:1、事件注册 2、事件分发
  • 事件注册部分,所有的事件都会注册到document上,拥有统一的回调函数dispatchEvent来执行事件分发
  • 事件分发部分,首先生成合成事件,注意同一种事件类型只能生成一个合成事件Event,如onclick这个类型的事件,dom上所有带有通过jsx绑定的onClick的回调函数都会按顺序(冒泡或者捕获)会放到Event._dispatchListeners 这个数组里,后面依次执行它。

还是使用上次的栗子:

class ExampleApplication extends React.Component {
    componentDidMount() {
        document.addEventListener('click', () => {
            alert('document click');
        })
    }

    outClick(e) {
        console.log(e.currentTarget);
        alert('outClick');
    }

    onClick(e) {
        console.log(e.currentTarget);
        alert('onClick');
        e.stopPropagation();
    }
    render() {        return <div onClick={this.outClick}>
            <button onClick={this.onClick}> 测试click事件 </button>
        </div>
    }
}

分析源码之前,有些工作和知识要提前准备,普及一下:

  • 请各位准备好一个编辑器,自行用react-starter-kit建一个react项目,复制上面的代码,渲染上面的组件,然后打开控制台
  • 下图是整个事件机制的流程图,后面会分部分解析 https://www.processon.com/diagraming/5a8003cde4b0812a0f10adc2
  • 普及几个功能函数,提前了解它的作用
// 作用:如果只是单个next,则直接返回,如果有数组,返回合成的数组,里面有个//current.push.apply(current, next)可以学习一下,我查了一下[资料][3],这样组合数组效率更高function accumulateInto(current, next) {
  if (current == null) {
    return next;
  }  // Both are not empty. Warning: Never call x.concat(y) when you are not
  // certain that x is an Array (x could be a string with concat method).
  if (Array.isArray(current)) {    if (Array.isArray(next)) {
      current.push.apply(current, next);
      return current;
    }
    current.push(next);
    return current;
  }  if (Array.isArray(next)) {    // A bit too dangerous to mutate `next`.
    return [current].concat(next);
  }

  return [current, next];
}// 这个其实就是用来执行函数的,当arr时数组的时候,用forEach,单个的时候则// 执行回调函数function forEachAccumulated(arr, cb, scope) {
  if (Array.isArray(arr)) {
    arr.forEach(cb, scope);
  } else if (arr) {
    cb.call(scope, arr);
  }
}

React事件机制

React事件机制分为两块:

  • 事件注册
  • 事件分发

我们一步步来看:

事件注册

整个过程从ReactDomComponent开始,重点在enqueuePutListener,这个函数做了三件事情,详细请参考下面源码:

ReactDomComponent.js

function enqueuePutListener () {
    // 省略部分代码
    ...    // 1、*重要:在这里取出button所在的document*
      var doc = isDocumentFragment ? containerInfo._node : containerInfo._ownerDocument;    // 2、在document上注册事件,同一个事件类型只会被注册一次
      listenTo(registrationName, doc);    // 3、mountReady之后将回调函数存在ListernBank中
      transaction.getReactMountReady().enqueue(putListener, {
        inst: inst,
        registrationName: registrationName,
        listener: listener
      });
}

接下来看看第二步:在document上注册事件 的过程,流程图如下:

接着我们抽出每个文件的重点函数出来分析:

ReactBrowserEventEmitter.js

listenTo: function (registrationName, contentDocumentHandle) {
    var mountAt = contentDocumentHandle;    // 检测document上是否已经监听onClick事件,所以前面说同一类型事件只会绑定一次
    var isListening = getListeningForDocument(mountAt);    // 获得dependency,将onClick 转成topClick,这只是一种处理方式不用纠结
    var dependencies = 
   EventPluginRegistry.registrationNameDependencies[registrationName];   // 中间是对各种事件类型给document绑定捕获事件或者冒泡事件,大部分都是冒泡,
   ...   // 这里我们的topClick,绑定的是冒泡事件
   else if (topEventMapping.hasOwnProperty(dependency)) {  
   // trapBubbledEvent会在下面分析
 ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(dependency, topEventMapping[dependency], mountAt);
   }   // 最后把topClick标记为已注册过,防止重复注册
   isListening[dependency] = true;
}

由于onclick绑定的是冒泡事件,所以我们来看看trapBubbledEvent

ReactEventListener.js

// 输入: topClick, click, doctrapBubbledEvent: function (topLevelType, handlerBaseName, element) {    if (!element) {      return null;
    }    //  EventListener 要做的事情就是把事件绑定到document上,注意这里无论是注册冒泡还是捕获事件,最终的回调函数都是dispatchEvent

    return EventListener.listen(element, handlerBaseName, ReactEventListener.dispatchEvent.bind(null, topLevelType));
  },// EventListener.js// 输入doc, click, dispatchEvent// 这个函数其实就是我们熟悉的兼容浏IE浏览器事件绑定的方法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);
        }
      };
    }
  },

注意这里无论是注册冒泡还是捕获事件,最终的回调函数都是dispatchEvent,所以我们来看看dispatchEvent怎么处理事件分发。

dispatchEvent

看到这里大家会奇怪,所有的事件的回调函数都是dispatchEvent来处理,那事件onClick原来的回调函数存到哪里去了呢?

再回来看事件注册的第三步:mountReady之后将回调函数存在ListernBank中

ReactDomComponent.js

function enqueuePutListener () {
    // 省略部分代码
    ...    // 1、*重要:在这里取出button所在的document*
      var doc = isDocumentFragment ? containerInfo._node : containerInfo._ownerDocument;    // 2、在document上注册事件,同一个事件类型只会被注册一次
      listenTo(registrationName, doc);    // 3、mountReady之后将回调函数存在ListernBank中
      transaction.getReactMountReady().enqueue(putListener, {
        inst: inst,
        registrationName: registrationName,
        listener: listener
      });
}

document上注册完所有的事件之后,还需要把listener 放到listenerBank中以listenerBank[registrationName][key]这样的形式存起来,然后在dispatchEvent里面使用。

将listener放到listenerBank中储存的过程如下:

ReactDomComponent.js

// 在putListener里存入listenerfunction putListener() {  var listenerToPut = this;  // 先put的是外层的listener - outClick,所以这里的inst是外层div
  // registrationName是onclick,listener是outClick
  EventPluginHub.putListener(listenerToPut.inst, listenerToPut.registrationName, listenerToPut.listener);
}

EventPluginHub.js

  /**
   * Stores `listener` at `listenerBank[registrationName][key]`. Is idempotent.
   *
   * @param {object} inst The instance, which is the source of events.
   * @param {string} registrationName Name of listener (e.g. `onClick`).
   * @param {function} listener The callback to store.
   */
  putListener: function (inst, registrationName, listener) {    var key = getDictionaryKey(inst); // 先根据inst得到唯一的key
    var bankForRegistrationName = listenerBank[registrationName] || (listenerBank[registrationName] = {});    // 可以看到最终listener 在 listenerBank里,最终以listenerBank[registrationName][key] 存在

    bankForRegistrationName[key] = listener; 

    var PluginModule = EventPluginRegistry.registrationNameModules[registrationName];    if (PluginModule && PluginModule.didPutListener) {      // 这里的didPutListener只是为了兼容手机safari对non-interactive元素
      // 双击响应不正确,详情可以参考这篇[文章][7]
      //https://www.quirksmode.org/blog/archives/2010/09/click_event_del.html
      PluginModule.didPutListener(inst, registrationName, listener);
    }
  },

以上就是事件注册的过程,接下来在看dispatchEvent如何处理事件分发。

事件分发

在介绍事件分发之前,有必要先介绍一下生成合成事件的过程,链接是https://segmentfault.com/a/1190000013363525

了解合成事件生成的过程之后,我们需要get一个点:合成事件收集了一波同类型(例如click)的回调函数存在了合成事件event._dispatchListeners这个数组里,然后将它们事件对应的虚拟dom节点放到_dispatchInstances 就本例来说,_dispatchListeners= [onClick, outClick],之后在一起执行。

接下来看看事件分发的过程:

EventListener.js

dispatchEvent: function (topLevelType, nativeEvent) {
    if (!ReactEventListener._enabled) {
      return;
    }    // 这里得到TopLevelCallbackBookKeeping的实例对象,本例中第一次触发dispatchEvent时
    // bookKeeping = {ancestors: [],nativeEvent,‘topClick’}
    var bookKeeping = TopLevelCallbackBookKeeping.getPooled(topLevelType, nativeEvent);
    try {      // Event queue being processed in the same cycle allows
      // `preventDefault`.
      // 接着执行handleTopLevelImpl(bookKeeping)
      ReactUpdates.batchedUpdates(handleTopLevelImpl, bookKeeping);
    } finally {
      TopLevelCallbackBookKeeping.release(bookKeeping);
    }
  }function handleTopLevelImpl(bookKeeping) {
  var nativeEventTarget = getEventTarget(bookKeeping.nativeEvent);  // 获取当前事件的虚拟dom元素
  var targetInst = ReactDOMComponentTree.getClosestInstanceFromNode(nativeEventTarget);

  var ancestor = targetInst;
  do {
    bookKeeping.ancestors.push(ancestor);
    ancestor = ancestor && findParent(ancestor);
  } while (ancestor);  for (var i = 0; i < bookKeeping.ancestors.length; i++) {
    targetInst = bookKeeping.ancestors[i];    // 这里的_handleTopLevel 对应的就是ReactEventEmitterMixin.js里的handleTopLevel
    ReactEventListener._handleTopLevel(bookKeeping.topLevelType, targetInst, bookKeeping.nativeEvent, getEventTarget(bookKeeping.nativeEvent));
  }
}// 这里的findParent曾经给我带来误导,我以为去找当前元素所有的父节点,但其实不是的,// 我们知道一般情况下,我们的组件最后会被包裹在<div id='root'></div>的标签里// 一般是没有组件再去嵌套它的,所以通常返回null/**
 * Find the deepest React component completely containing the root of the
 * passed-in instance (for use when entire React trees are nested within each
 * other). If React trees are not nested, returns null.
 */function findParent(inst) {
  while (inst._hostParent) {
    inst = inst._hostParent;
  }
  var rootNode = ReactDOMComponentTree.getNodeFromInstance(inst);
  var container = rootNode;
  return ReactDOMComponentTree.getClosestInstanceFromNode(container);
}

上面这段代码的重点就是_handleTopLevel,它可以获取合成事件,并且去执行它。

下面看看具体是如何执行:

ReactEventEmitterMixin.js

function runEventQueueInBatch(events) {
  // 1、先将事件放进队列里
  EventPluginHub.enqueueEvents(events);  // 2、执行它
  EventPluginHub.processEventQueue(false);
}

var ReactEventEmitterMixin = {  /**
   * Streams a fired top-level event to `EventPluginHub` where plugins have the
   * opportunity to create `ReactEvent`s to be dispatched.
   */
  handleTopLevel: function (topLevelType, targetInst, nativeEvent, nativeEventTarget) {
     // 用EventPluginHub生成合成事件
    var events = EventPluginHub.extractEvents(topLevelType, targetInst, nativeEvent, nativeEventTarget);    //  执行合成事件
    runEventQueueInBatch(events);
  }
};

执行的过程分成两步:

  • 将事件放进队列
  • 执行

执行的细节如下:

EventPluginHub.js

  var executeDispatchesAndReleaseTopLevel = function (e) {
    return executeDispatchesAndRelease(e, false);
  };

  var executeDispatchesAndRelease = function (event, simulated) {
      if (event) {         // 在这里dispatch事件
        EventPluginUtils.executeDispatchesInOrder(event, simulated);         // 释放事件
        if (!event.isPersistent()) {
          event.constructor.release(event);
        }
      }
  };

  enqueueEvents: function (events) {
    if (events) {
      eventQueue = accumulateInto(eventQueue, events);
    }
  },  /**
   * Dispatches all synthetic events on the event queue.
   *
   * @internal
   */
  processEventQueue: function (simulated) {
    // Set `eventQueue` to null before processing it so that we can tell if more
    // events get enqueued while processing.
    var processingEventQueue = eventQueue;
    eventQueue = null;    if (simulated) {
      forEachAccumulated(processingEventQueue, executeDispatchesAndReleaseSimulated);
    } else {
      forEachAccumulated(processingEventQueue, executeDispatchesAndReleaseTopLevel);
    }    // This would be a good time to rethrow if any of the event fexers threw.
    ReactErrorUtils.rethrowCaughtError();
  },

上段代码里,我们最终会走到

forEachAccumulated(processingEventQueue, executeDispatchesAndReleaseTopLevel);

forEachAccumulated这个函数我们之前讲过,就是对数组processingEventQueue的每一个合成事件都使用executeDispatchesAndReleaseTopLevel来dispatch 事件。

所以各位同学们,注意到这里我们已经走到最核心的部分,dispatch 合成事件了,下面看看dispatch的详细过程:

EventPluginUtils.js

/**
 * Standard/simple iteration through an event's collected dispatches.
 */function executeDispatchesInOrder(event, simulated) {  var dispatchListeners = event._dispatchListeners;  var dispatchInstances = event._dispatchInstances;  if (Array.isArray(dispatchListeners)) {    for (var i = 0; i < dispatchListeners.length; i++) {     // 由这里可以看出,合成事件的stopPropagation只能阻止react合成事件的冒泡,
     // 因为event._dispatchListeners 只记录了由jsx绑定的绑定的事件,对于原生绑定的是没有记录的
      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;
}

由上面的函数可知,dispatch 合成事件分为两个步骤:

  • 通过_dispatchListeners里得到所有绑定的回调函数,在通过_dispatchInstances的绑定回调函数的虚拟dom元素
  • 循环执行_dispatchListeners里所有的回调函数,这里有一个特殊情况,也是react阻止冒泡的原理

当回调函数里使用了stopPropagation会使得数组后面的回调函数不能执行,这样就做到了阻止事件冒泡。

目前还是还有看到执行事件的代码,再接着看:

EventPluginHub.js

function executeDispatch(event, simulated, listener, inst) {  var type = event.type || 'unknown-event';  // 注意这里将事件对应的dom元素绑定到了currentTarget上
  event.currentTarget = EventPluginUtils.getNodeFromInstance(inst);  if (simulated) {
    ReactErrorUtils.invokeGuardedCallbackWithCatch(type, listener, event);
  } else {    // 一般都是非模拟的情况,执行invokeGuardedCallback
    ReactErrorUtils.invokeGuardedCallback(type, listener, event);
  }  event.currentTarget = null;
}

上面这个函数最重要的功能就是将事件对应的dom元素绑定到了currentTarget上, 这样我们通过e.currentTarget就可以找到绑定事件的原生dom元素。

下面就是整个执行过程的尾声了:

ReactErrorUtils.js

var fakeNode = document.createElement('react');
ReactErrorUtils.invokeGuardedCallback = function (name, func, a) {  var boundFunc = function () {
    func(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);
};

invokeGuardedCallback可知,最后react调用了faked元素的dispatchEvent方法来触发事件,并且触发完毕之后立即移除监听事件。

总的来说,整个click事件被分发的过程就是:

1、用EventPluginHub生成合成事件,这里注意同一事件类型只会生成一个合成事件,里面的_dispatchListeners里储存了同一事件类型的所有回调函数

2、按顺序去执行它

就辣么简单!

原文作者:IMWeb 黄qiong

原文链接:http://imweb.io/topic/5aa0de1f16bc830d673d42df

原文发布于微信公众号 - 腾讯NEXT学位(NextDegree)

原文发表时间:2018-04-04

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏跟着阿笨一起玩NET

WCF自我学习之(一)

本人最近才开始学习WCF服务框架,今天顺便做了一个小小的练手。适合初学者学习,故上传上来,望和大家一起共勉!高手勿进,见笑了。

781
来自专栏游戏杂谈

Unity插件扩展中组件常用的几个方法

最近为美术编写一个Unity编辑器的扩展,主要为了减轻美术在修改预制对象时的机械化操作的繁琐和出错。具体实现的几个功能:

3251
来自专栏kalifaの日々

动态规划真的可以为所欲为的(Leetcode 62/63)

看起来不错的运行效率 62题: 动态规划递推公式: 站在当前方块上可选择的路径数量 = 我正下方那个方块可选择的路径数量 + 我右侧那个方块可选择的路径数量; ...

3636
来自专栏阿炬.NET

FineUIMvc表格数据库分页,使用CYQ.Data组件

4168
来自专栏Python小屋

Python查找Word文件中红色和加粗的文字

背景知识:docx文件的结构分为三层,1、Docment对象表示整个文档;2、Docment包含了Paragraph对象的列表,每个Paragraph对象用来表...

3598
来自专栏数据小魔方

动态图表系列1|数据有效性(index+match函数)

今天开始跟大家分享动态图表的技巧1——数据有效性(index+match函数)! 动态图表之——数据有效性(index+match) 首先利用数据验证制作下拉菜...

4287
来自专栏ionic3+

【组件篇】ionic3均分列等宽高图像显示(上)

我在《ionic3开源组件》提到了图片选择组件,但是后来发现其实现功能很简单,而且我不喜欢它写死了宽高大小,这对于不同分别率不太友好。于是尝试实现了一下,先上效...

805
来自专栏落影的专栏

Metal入门教程(五)视频渲染

Metal入门教程(一)图片绘制 Metal入门教程(二)三维变换 Metal入门教程(三)摄像头采集渲染 Metal入门教程(四)灰度计算

6596
来自专栏偏前端工程师的驿站

CSS魔法堂:你真的理解z-index吗?

一、前言                                 假如只是开发简单的弹窗效果,懂得通过z-index来调整元素间的层叠关系就够了。但要将...

1985
来自专栏macOS 开发学习

Mac开发跬步积累(四):ImageIO解析Gif 图像数据

这里可以看到Gif 是保存了多幅图像的一个图像文件,有了这个基础认识,我们就可以使用代码来解析Gif图像了.

1113

扫码关注云+社区

领取腾讯云代金券