浅谈React Event实现原理


Posted in Javascript onSeptember 20, 2018

React 元素的事件处理和 DOM元素的很相似。但是有一点语法上的不同:

  1. React事件绑定属性的命名采用驼峰式写法,而不是小写。
  2. 如果采用 JSX 的语法你需要传入一个函数作为事件处理函数,而不是一个字符串(DOM元素的写法)

并且 React 自己内部实现了一个合成事件,使用 React 的时候通常你不需要使用 addEventListener 为一个已创建的 DOM 元素添加监听器。你仅仅需要在这个元素初始渲染的时候提供一个监听器。

我们看一下这是怎么实现的

React 事件机制分为 事件注册,和事件分发,两个部分

事件注册

// 事件绑定
function handleClick(e) {
  e.preventDefault();
  console.log('The link was clicked.');
}

 return (
  <a href="#" rel="external nofollow" onClick={handleClick}>
   Click me
  </a>
 );

上述代码中, onClick 作为一个 props 传入了一个 handleClick,在组件更新和挂载的时候,会对props处理, 事件绑定流程如下:

浅谈React Event实现原理

核心代码:

在 ReactDOMComponent.js 进行组件加载 (mountComponent)、更新 (updateComponent) 的时候,调用 _updateDOMProperties 方法对 props 进行处理:

ReactDOMComponent.js

_updateDOMProperties: function(lastProps, nextProps, transaction) {
...
if (registrationNameModules.hasOwnProperty(propKey)) {
    if (nextProp) {
     // 如果传入的是事件,去注册事件
     enqueuePutListener(this, propKey, nextProp, transaction);
    } else if (lastProp) {
     deleteListener(this, propKey);
    }
   } 
...
}

// 注册事件
function enqueuePutListener(inst, registrationName, listener, transaction) {
 var containerInfo = inst._nativeContainerInfo;
 var doc = containerInfo._ownerDocument;
  ...
  // 去doc上注册
 listenTo(registrationName, doc);
  // 事务结束之后 putListener
 transaction.getReactMountReady().enqueue(putListener, {
  inst: inst,
  registrationName: registrationName,
  listener: listener,
 });
}

看下绑定方法

ReactBrowserEventEmitter.js

listento

//registrationName:需要绑定的事件
//当前component所属的document,即事件需要绑定的位置
listenTo: function (registrationName, contentDocumentHandle) {
  var mountAt = contentDocumentHandle;
  //获取当前document上已经绑定的事件
  var isListening = getListeningForDocument(mountAt);
  ...
   if (...) {
   //冒泡处理 
   ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(...);
   } else if (...) {
    //捕捉处理
    ReactBrowserEventEmitter.ReactEventListener.trapCapturedEvent(...);
   }
   ...
 },

走到最后其实就是 doc.addEventLister(event, callback, false);

可以看出所有事件绑定在document上

所以事件触发的都是ReactEventListener的dispatchEvent方法

回调事件储存

listenerBank

react 维护了一个 listenerBank 的变量保存了所有的绑定事件的回调。

回到之前注册事件的方法

function enqueuePutListener(inst, registrationName, listener, transaction) {
 var containerInfo = inst._nativeContainerInfo;
 var doc = containerInfo._ownerDocument;
 if (!doc) {
  // Server rendering.
  return;
 }
 listenTo(registrationName, doc);
 transaction.getReactMountReady().enqueue(putListener, {
  inst: inst,
  registrationName: registrationName,
  listener: listener,
 });
}

当绑定完成以后会执行putListener。

var listenerBank = {};
var getDictionaryKey = function (inst) {
//inst为组建的实例化对象
//_rootNodeID为组件的唯一标识
 return '.' + inst._rootNodeID;
}
var EventPluginHub = {
//inst为组建的实例化对象
//registrationName为事件名称
//listner为我们写的回调函数,也就是列子中的this.autoFocus
 putListener: function (inst, registrationName, listener) {
  ...
  var key = getDictionaryKey(inst);
  var bankForRegistrationName = listenerBank[registrationName] || (listenerBank[registrationName] = {});
  bankForRegistrationName[key] = listener;
  ...
 }
}

EventPluginHub在每个React中只实例化一次。也就是说,项目组所有事件的回调都会储存在唯一的listenerBank中。

事件触发

注册事件流程图所示,所有的事件都是绑定在Document上。回调统一是ReactEventListener的dispatch方法。
由于冒泡机制,无论我们点击哪个DOM,最后都是由document响应(因为其他DOM根本没有事件监听)。也即是说都会触发 ReactEventListener.js 里的 dispatch方法。

我们先看一下事件触发的流程图:

浅谈React Event实现原理

dispatchEvent: function (topLevelType, nativeEvent) {
  if (!ReactEventListener._enabled) {
   return;
  }
  // 这里得到TopLevelCallbackBookKeeping的实例对象,本例中第一次触发dispatchEvent时
  // bookKeeping instanceof TopLevelCallbackBookKeeping
  // bookKeeping = TopLevelCallbackBookKeeping {topLevelType: "topClick", nativeEvent: "click", ancestors: Array(0)}
  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.parentNode;
 return ReactDOMComponentTree.getClosestInstanceFromNode(container);
}

我们看一下核心方法 _handleTopLevel

ReactEventEmitterMixin.js

//这就是核心的处理了
handleTopLevel: function (topLevelType, targetInst, nativeEvent, nativeEventTarget) {
  //返回合成事件
  //这里进入了EventPluginHub,调用事件插件方法,返回合成事件,并执行队列里的dispatchListener
  var events = EventPluginHub.extractEvents(topLevelType, targetInst, nativeEvent, nativeEventTarget);
  //执行合成事件
  runEventQueueInBatch(events);
 }
合成事件如何生成,请看上方事件触发的流程图

runEventQueuelnBatch(events)做了两件事

  1. 把 dispatchListener里面的事件排队push进 eventQueue
  2. 执行 EventPluginHub.processEventQueue(false);

执行的细节如下:

EventPluginHub.js

// 循环 eventQueue调用
 var executeDispatchesAndReleaseTopLevel = function (e) {
  return executeDispatchesAndRelease(e, false);
 };
 /* 从event._dispatchListener 取出 dispatchlistener,然后dispatch事件,
  * 循环_dispatchListeners,调用executeDispatch
  */
 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();
 },
/**
 * 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;
}
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;
}

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

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

其实在 EventPluginHub.js 里主要做了两件事情.

1.从event._dispatchListener 取出 dispatchlistener,然后dispatch事件,
循环_dispatchListeners,调用executeDispatch,然后走到ReactErrorUtils.invokeGuardedCallback;
2.释放 event

上面这个函数最重要的功能就是将事件对应的dom元素绑定到了currentTarget上,

这样我们通过e.currentTarget就可以找到绑定事件的原生dom元素。

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

ReactErrorUtils.js

var fakeNode = document.createElement('react');
ReactErrorUtils.invokeGuardedCallback = function(name, func, a, b) {
   var boundFunc = func.bind(null, a, b);
   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、按顺序去执行它

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
javascript 判断字符串是否包含某字符串及indexOf使用示例
Oct 18 Javascript
jquery解析JSON数据示例代码
Mar 17 Javascript
extjs 如何给column 加上提示
Jul 29 Javascript
jquery实现一个简单好用的弹出框
Sep 26 Javascript
node.js中的path.sep方法使用说明
Dec 08 Javascript
jQuery使用toggleClass方法动态添加删除Class样式的方法
Mar 26 Javascript
JS模拟bootstrap下拉菜单效果实例
Jun 17 Javascript
使用BootStrap实现悬浮窗口的效果
Dec 13 Javascript
COM组件中调用JavaScript函数详解及实例
Feb 23 Javascript
微信小程序 实例开发总结
Apr 26 Javascript
JavaScript实现各种排序的代码详解
Aug 28 Javascript
浅析Vue.js 中的条件渲染指令
Nov 19 Javascript
vue-cli项目代理proxyTable配置exclude的方法
Sep 20 #Javascript
Node批量爬取头条视频并保存方法
Sep 20 #Javascript
vue 本地环境跨域请求proxyTable的方法
Sep 19 #Javascript
vue 优化CDN加速的方法示例
Sep 19 #Javascript
Vue前后端不同端口的实现方法
Sep 19 #Javascript
vue-cli 3.x 修改dist路径的方法
Sep 19 #Javascript
浅谈React之状态(State)
Sep 19 #Javascript
You might like
一个域名查询的程序
2006/10/09 PHP
PHP的面试题集
2006/11/19 PHP
PHP中图片等比缩放的实例
2013/03/24 PHP
php获取一个变量的名字的方法
2014/09/05 PHP
PHP中你应该知道的require()文件包含的正确用法
2015/06/12 PHP
Yii 2.0中场景的使用教程
2017/06/02 PHP
php基于数组函数实现关联表的编辑操作示例
2017/07/04 PHP
漂亮的提示信息(带箭头)
2007/03/21 Javascript
JS正则中的RegExp对象对象
2012/11/07 Javascript
Flash图片上传组件 swfupload使用指南
2015/03/14 Javascript
json+jQuery实现的无限级树形菜单效果代码
2015/08/27 Javascript
js实现随屏幕滚动的带缓冲效果的右下角广告代码
2015/09/04 Javascript
VueJs与ReactJS和AngularJS的异同点
2016/12/12 Javascript
BootStrap给table表格的每一行添加一个按钮事件
2017/09/07 Javascript
JS解决IOS中拍照图片预览旋转90度BUG的问题
2017/09/13 Javascript
最新Javascript程序员面试试题和解题方法
2017/11/23 Javascript
vue-cli3 从搭建到优化的详细步骤
2019/01/20 Javascript
js实现点赞效果
2020/03/16 Javascript
vue 页面回退mounted函数不执行的解决方案
2020/07/26 Javascript
[05:00]TI9战队采访 - Royal Never Give Up
2019/08/20 DOTA
python从sqlite读取并显示数据的方法
2015/05/08 Python
Python模拟百度登录实例详解
2016/01/20 Python
Python BS4库的安装与使用详解
2018/08/08 Python
python实现简单登陆系统
2018/10/18 Python
Python的条件锁与事件共享详解
2019/09/12 Python
关于win10在tensorflow的安装及在pycharm中运行步骤详解
2020/03/16 Python
需要知道的CSS3动画技术
2010/01/01 HTML / CSS
法国春天百货官网:Printemps.com
2020/06/29 全球购物
国际经济贸易专业推荐信
2013/11/06 职场文书
小学生学雷锋演讲稿
2014/04/25 职场文书
企业文化宣传标语
2014/06/09 职场文书
2014年教学工作总结
2014/11/13 职场文书
廉政承诺书
2015/01/19 职场文书
老龙头导游词
2015/02/11 职场文书
大学生创业,为什么都会选择快餐饮?
2019/08/08 职场文书
Java使用jmeter进行压力测试
2021/07/09 Java/Android