资讯专栏INFORMATION COLUMN

React Event 实现原理

xiguadada / 1305人阅读

摘要:为组建的实例化对象为组件的唯一标识为组建的实例化对象为事件名称为我们写的回调函数,也就是列子中的在每个中只实例化一次。

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

React事件绑定属性的命名采用驼峰式写法,而不是小写。

如果采用 JSX 的语法你需要传入一个函数作为事件处理函数,而不是一个字符串(DOM元素的写法)

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

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

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

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

  return (
    
      Click me
    
  );

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

核心代码:
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方法。

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

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曾经给我带来误导,我以为去找当前元素所有的父节点,但其实不是的,
// 我们知道一般情况下,我们的组件最后会被包裹在
的标签里 // 一般是没有组件再去嵌套它的,所以通常返回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)做了两件事

把 dispatchListener里面的事件排队push进 eventQueue

执行 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 合成事件分为两个步骤:

通过_dispatchListeners里得到所有绑定的回调函数,在通过_dispatchInstances的绑定回调函数的虚拟dom元素

循环执行_dispatchListeners里所有的回调函数,这里有一个特殊情况,也是react阻止冒泡的原理

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

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

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

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

ReactErrorUtils.js
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、按顺序去执行它

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/96270.html

相关文章

  • React Native 原理

    摘要:对象和原生代码交互对象可以和原生对象之间相互调用,关系如上图。和本地代码间的通信其中通信的特点是异步的序列化的批量的,对于大批量的通信事件可以将其分成几部分,减少时间延迟参考 React Native简介 React Native是一个建立在JavaScript和React上用于构建本地应用的框架,它具有React和JavaScript相似的代码风格,编写一次可以运行在多个平台之上(>...

    jackzou 评论0 收藏0
  • 从路由原理出发,深入阅读理解react-router 4.0的源码

    摘要:通过前端路由可以实现单页应用本文首先从前端路由的原理出发,详细介绍了前端路由原理的变迁。接着从的源码出发,深入理解是如何实现前端路由的。执行上述的赋值后,页面的发生改变。   react-router等前端路由的原理大致相同,可以实现无刷新的条件下切换显示不同的页面。路由的本质就是页面的URL发生改变时,页面的显示结果可以根据URL的变化而变化,但是页面不会刷新。通过前端路由可以实现...

    Miyang 评论0 收藏0
  • 结合源码彻底理解 react事件机制原理 04 - 事件执行

    摘要:文章涉及到的源码是基于版本,虽然不是最新版本但是也不会影响我们对事件机制的整体把握和理解。总结本文主要是从整体流程上介绍了下事件触发的过程。 showImg(https://segmentfault.com/img/bVbtvI3?w=1048&h=550); 前言 这是 react 事件机制的第四节-事件执行,一起研究下在这个过程中主要经过了哪些关键步骤,本文也是react 事件机制...

    marser 评论0 收藏0
  • 浅析React之事件系统(二)

    摘要:因为阻止事件冒泡的行为只能用于合成事件中,没法阻止原生事件的冒泡。同时的创建和冒泡是在原生事件冒泡到最顶层的之后的。浅析之事件系统一 上篇文章中,我们谈到了React事件系统的实现方式,和在React中使用原生事件的方法,那么这篇文章我们来继续分析下,看看React中合成事件和原生事件混用的各种情况。 上一个例子 在上篇文章中,我们举了个例子。为了防止大家不记得,我们来看看那个例子的代...

    villainhr 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<