React事件机制 - 源码概览(下)

上篇文档 React事件机制 - 源码概览(上)说到了事件执行阶段的构造合成事件部分,本文接着继续往下分析react

批处理合成事件

入口是 runEventsInBatch数组

// runEventsInBatch
// packages/events/EventPluginHub.js
export function runEventsInBatch( events: Array<ReactSyntheticEvent> | ReactSyntheticEvent | null, simulated: boolean, ) {
  if (events !== null) {
    eventQueue = accumulateInto(eventQueue, events);
  }
  const processingEventQueue = eventQueue;
  eventQueue = null;
  if (!processingEventQueue) {
    return;
  }
  if (simulated) {
    // react-test 才会执行的代码
    // ...
  } else {
    forEachAccumulated(
      processingEventQueue,
      executeDispatchesAndReleaseTopLevel,
    );
  }
  // This would be a good time to rethrow if any of the event handlers threw.
  rethrowCaughtError();
}
复制代码

这个方法首先会将当前须要处理的 events事件,与以前没有处理完毕的队列调用 accumulateInto方法按照顺序进行合并,组合成一个新的队列,由于以前可能就存在还没处理完的合成事件,这里就又有获得执行的机会了浏览器

若是合并后的队列为 null,即没有须要处理的事件,则退出,不然根据 simulated来进行分支判断调用对应的方法,这里的 simulated标志位,字面意思是 仿造的、伪装的,其实这个字段跟 react-test,即测试用例有关,只有测试用例调用 runEventsInBatch方法的时候, simulated标志位的值才为true,除了这个地方之外,React源码中还有其余的不少地方都会出现 simulated,都是跟测试用例有关,看到了不用管直接走 else逻辑便可,因此咱们这里就走 else的逻辑,调用 forEachAccumulated方法app

// packages/events/forEachAccumulated.js
function forEachAccumulated<T>( arr: ?(Array<T> | T), cb: (elem: T) => void, scope: ?any, ) {
  if (Array.isArray(arr)) {
    arr.forEach(cb, scope);
  } else if (arr) {
    cb.call(scope, arr);
  }
}
复制代码

这个方法就是先看下事件队列processingEventQueue是否是个数组,若是是数组,说明队列中不止一个事件,则遍历队列,调用 executeDispatchesAndReleaseTopLevel,不然说明队列中只有一个事件,则无需遍历直接调用便可dom

因此来看下 executeDispatchesAndReleaseTopLevel这个方法:函数

// packages/events/EventPluginHub.js
const executeDispatchesAndReleaseTopLevel = function(e) {
  return executeDispatchesAndRelease(e, false);
};
// ...
const executeDispatchesAndRelease = function( event: ReactSyntheticEvent, simulated: boolean, ) {
  if (event) {
    executeDispatchesInOrder(event, simulated);

    if (!event.isPersistent()) {
      event.constructor.release(event);
    }
  }
};
复制代码

executeDispatchesAndReleaseTopLevel又调用了 executeDispatchesAndRelease,而后 executeDispatchesAndRelease这个方法先调用了 executeDispatchesInOrder,这个方法是事件处理的核心所在:post

// packages/events/EventPluginUtils.js
// executeDispatchesInOrder
export function executeDispatchesInOrder(event, simulated) {
  const dispatchListeners = event._dispatchListeners;
  const dispatchInstances = event._dispatchInstances;
  if (__DEV__) {
    validateEventDispatches(event);
  }
  if (Array.isArray(dispatchListeners)) {
    for (let 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;
}
复制代码

首先对拿到的事件上挂在的 dispatchListeners,也就是以前拿到的当前元素以及其全部父元素上注册的事件回调函数的集合,遍历这个集合,若是发现遍历到的事件的 event.isPropagationStopped()true,则遍历的循环直接 break掉,这里的 isPropagationStopped在前面已经说过了,它是用于标识当前 React Node上触发的事件是否执行了 e.stopPropagation()这个方法,若是执行了,则说明在此以前触发的事件已经调用 event.stopPropagation()isPropagationStopped的值被置为 functionThatReturnsTrue,即执行后为 true,当前事件以及后面的事件做为父级事件就不该该再被执行了测试

这里当 event.isPropagationStopped()true时,中断合成事件的向上遍历执行,也就起到了和原生事件调用 stopPropagation相同的效果ui

若是循环没有被中断,则继续执行 executeDispatch方法,这个方法接下来又一层一层地调了不少方法,最终来到 invokeGuardedCallbackImplthis

// packages/shared/invokeGuardedCallbackImpl.js
let invokeGuardedCallbackImpl = function<A, B, C, D, E, F, Context>( name: string | null, func: (a: A, b: B, c: C, d: D, e: E, f: F) => mixed, context: Context, a: A, b: B, c: C, d: D, e: E, f: F, ) {
  const funcArgs = Array.prototype.slice.call(arguments, 3);
  try {
    func.apply(context, funcArgs);
  } catch (error) {
    this.onError(error);
  }
};
复制代码

关键在于这一句:

func.apply(context, funcArgs);
复制代码

funcArgs是什么呢?其实就是合成事件对象,包括原生浏览器事件对象的基本上全部属性和方法,除此以外还另外挂载了额外其余一些跟 React合成事件相关的属性和方法,而 func则就是传入的事件回调函数,对于本示例来讲,就等于clickHandler这个回调方法:

// func === clickHandler
clickHandler(e) {
  console.log('click callback', e)
}
复制代码

funcArgs做为参数传入 func,也便是传入 clickHandler,因此咱们就可以在 clickHandler这个函数体内拿到 e这个回调参数,也就能经过这个回调参数拿到其上面挂载的任何属性和方法,例如一些跟原生浏览器对象相关的属性和方法,以及原生事件对象自己(nativeEvent)

至此,事件执行完毕

这个过程流程图以下:

事件清理

事件执行完毕以后,接下来就是一些清理工做了,由于 React采用了对象池的方式来管理合成事件,因此当事件执行完毕以后就要清理释放掉,减小内存占用,主要是执行了上面提到过的位于 executeDispatchesAndRelease方法中的 event.constructor.release(event);这一句代码

这里面的 release就是以下方法:

// packages/events/SyntheticEvent.js
function releasePooledEvent(event) {
  const EventConstructor = this;
  invariant(
    event instanceof EventConstructor,
    'Trying to release an event instance into a pool of a different type.',
  );
  event.destructor();
  if (EventConstructor.eventPool.length < EVENT_POOL_SIZE) {
    EventConstructor.eventPool.push(event);
  }
}
复制代码

这个方法主要作了两件事,首先释放掉 event上属性占用的内存,而后把清理后的 event对象再放入对象池中,能够被后续事件对象二次利用

event.destructor();这句就是用于释放内存的,destructor这个方法的字面意思是 析构,也就表示它是一个析构函数,了解 C/C++的人应该对这个名词很熟悉,它通常都是用于 清理善后的工做,例如释放掉构造函数申请的内存空间以释放内存,这里的 destructor方法一样是有着这个做用

destructorSyntheticEvent上的方法,因此全部的合成事件都能拿到这个方法:

// packages/events/SyntheticEvent.js
destructor: function() {
  const Interface = this.constructor.Interface;
  for (const propName in Interface) {
    if (__DEV__) {
      Object.defineProperty(
        this,
        propName,
        getPooledWarningPropertyDefinition(propName, Interface[propName]),
      );
    } else {
      this[propName] = null;
    }
  }
  this.dispatchConfig = null;
  this._targetInst = null;
  this.nativeEvent = null;
  this.isDefaultPrevented = functionThatReturnsFalse;
  this.isPropagationStopped = functionThatReturnsFalse;
  this._dispatchListeners = null;
  this._dispatchInstances = null;
  // 如下省略部分代码
  // ...
}
复制代码

JavaScript引擎有本身的垃圾回收机制,通常来讲不须要开发者亲自去回收内存空间,但这并非说开发者就彻底没法影响这个过程了,常见的手动释放内存的方法就是将对象置为 nulldestructor这个方法主要就是作这件事情,遍历事件对象上全部属性,并将全部属性的值置为 null

总结

React的事件机制看起来仍是比较复杂的,我本身看了几遍源码又对着调试了几遍,如今又写了分析文章,回头再想一想其实主线仍是比较明确的,过完了源码以后,再去看 react-dom/src/events/ReactBrowserEventEmitter.js这个源码文件开头的那一段图形化注释,整个流程就更加清晰了

顺便分享一个看源码的技巧,若是某份源码,好比 React这种,比较复杂,代码方法不少,很容易看着看着就乱了,那么就不要再干看着了,直接写个简单的例子,而后在浏览器上打断点,对着例子和源码一步步调试,弄明白每一步的逻辑和目的,多调试几回后,基本上就能抓到关键点了,后续再通读源码的时候,就会流畅不少了

相关文章
相关标签/搜索