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

栏目: 服务器 · 发布时间: 6年前

内容简介:入口是这个方法首先会将当前需要处理的如果合并后的队列为

入口是 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 方法

// 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 ,否则说明队列中只有一个事件,则无需遍历直接调用即可

所以来看下 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 ,这个方法是事件处理的核心所在:

// 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 相同的效果

如果循环没有被中断,则继续执行 executeDispatch 方法,这个方法接下来又一层一层地调了很多方法,最终来到 invokeGuardedCallbackImpl

// 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事件机制 - 源码概览(下)

事件清理

事件执行完毕之后,接下来就是一些清理工作了,因为 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 这种,比较复杂,代码方法很多,很容易看着看着就乱了,那么就不要再干看着了,直接写个简单的例子,然后在浏览器上打断点,对着例子和源码一步步调试,弄明白每一步的逻辑和目的,多调试几次后,基本上就能抓到关键点了,后续再通读源码的时候,就会流畅很多了


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Cyberwar

Cyberwar

Kathleen Hall Jamieson / Oxford University Press / 2018-10-3 / USD 16.96

The question of how Donald Trump won the 2016 election looms over his presidency. In particular, were the 78,000 voters who gave him an Electoral College victory affected by the Russian trolls and hac......一起来看看 《Cyberwar》 这本书的介绍吧!

HTML 压缩/解压工具
HTML 压缩/解压工具

在线压缩/解压 HTML 代码

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具