React-事件的注册和触发

March 29, 2019 ... ☕️☕️ 8 min read

事件也是React里使用频率很高的操作,各种onClick、onFocus/onBlur、onChange、onSubmit都是经常使用的。事件触发同样是update,也会使用ReactUpdates.batchedUpdates流程,所以会用到前面文章中的内容。

还是以之前的Hello为例,这次给div添加onClick函数事件,函数名为clickFunc。完整代码如下

class Hello extends React.Component {
  constructor(props) {
    super(props);
    this.state = {now: '2018'};
    this.clickFunc = this.clickFunc.bind(this);
  }
  render() {
    return <div onClick={this.clickFunc}>Hello {this.props.name}! {this.state.now}</div>;
  }
  clickFunc() {
    console.log(this.state.now);
  }
}

事件的注册

按上一篇 React-简单组件到浏览器DOM的渲染 的包装层次渲染,最后会调用到最内层的mountComponent函数,流程跟之前讲的是一样的:

ReactDOMComponent.mountComponent
-_updateDOMProperties(
  lastProps,
  nextProps, //{children: [...], onClick: function(){...}}
  transaction // ReactReconcileTransaction
)

判断registrationNameModules.hasOwnProperty(propKey),这里propKey是onClick

-enqueuePutListener(this, propKey, nextProp, transaction)

enqueuePutListener是事件的关键函数,主要处理流程如下

listener-reg

1、在document注册监听事件

-listenTo(registrationName, doc);
-ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(
  dependency,  // 'topClick'
  topEventMapping[dependency], // 'click'
  mountAt); // #document
-EventListener.listen(
  element, // #document
  handlerBaseName, // 'click'
  ReactEventListener.dispatchEvent.bind(null, topLevelType));
-target.addEventListener(eventType, callback, false);
-return {
  remove: function remove() {
    target.removeEventListener(eventType, callback, false);
  }
};

这一步首先为document绑定了click事件,callback为ReactEventListener.dispatchEvent

2、存储监听事件

这一步完成之后,要达到的目的是将监听事件存储到listenerBank。

listener-bank

-transaction.getReactMountReady() // =CallbackQueue.getPooled(null)
.enqueue(
putListener, {
  inst: inst, // 目标元素ReactDOMComponent
  registrationName: registrationName, // onClick
  listener: listener // clickFunc函数
  }
);

之前说过,ReactReconcileTransaction用到了“池”,即CallBackQueue是用池扩展的,可以调用getPooled/release等一些方法,主要目的是节省开销。用enqueue方法,把callback和对应的context存入Callback的callbacks和contexts。注意,这里用数组下标来对应关系。enqueue的回调函数,会在notifyAll的时候调用,这里会直接在contexts[x]上调用callbacks[x]。

通过以上步骤,更新了CallbackQueue的属性。

callback-queue

notifyAll什么时候调用呢?还是在transaction包装(transactionWrappers)的close函数调用。基本过程如下:

wrapper.close.call // transactionWrappers[2]
-this.reactMountReady.notifyAll() // 调用之前通过enqueue注册的回调函数
-putListener // 之前注册的回调函数
=EventPluginHub.putListener(listenerToPut.inst, listenerToPut.registrationName, listenerToPut.listener);
// listenerToPut是之前enqueue的对象
-var PluginModule = EventPluginRegistry.registrationNameModules[registrationName];

接着会将listener存入listenerBank,这个bank正如其名,存储着所有的listener。

3、为node节点添加监听函数

之前的部分都没有涉及到实际的dom节点,在最后的didPutListener函数里,instance获取html节点node,并存入SimpleEventPlugin.onClickListeners:

-didPutListener
-onClickListeners[key] = EventListener.listen(node, 'click', emptyFunction); // 平台无关
-node.addEventListener(eventType, callback, false); // 给节点添加冒泡监听,这里放入的是空的callback
-return {
  remove: function remove() {
    target.removeListener(eventType, callback, false);
  }
}

最后清空CallbackQueue的contexts和callbacks:

batchedMountComponentIntoNode
-ReactUpdates.ReactReconcileTransaction.release(transaction)
-CallbackQueue.release
-CallbackQueue.reset // 清空_contexts和_callbacks

到这里,事件就注册完成了,总结一下:在document上注册了dispatchEvent这个监听函数,在其他元素上注册了空的监听函数。

事件的触发

事件注册之后,就需要触发。触发的入口是dispatchEvent,关键函数是handleTopLevelImpl,核心是合成事件(SyntheticEvent),描述放在最后。

基本执行流程:

-ReactUpdates.batchedUpdates(handleTopLevelImpl, bookKeeping)
-handleTopLevelImpl(bookKeeping)
-handleTopLevel
-runEventQueueInBatch(events);
-executeDispatch

react的所有事件都是在document上通过注册的监听函数(dispatchEvent)下发并触发的。,并且,触发(dispatchEvent)的位置并不是实际渲染出来的DOM元素,而是一个即用即弃的新建元素。

如何知道是在document上触发的监听,而不是其他地方?在之前往document和其他元素绑定事件的时候,都通过EventListener.listen添加,只不过除了document,其他绑定的监听全部是emptyFunction。

在前篇 React-函数batchedUpdates和Transaction执行 中介绍过batchedUpdates的执行流程,仍旧是ReactDefaultBatchingStrategy.batchedUpdates的执行,这里不再赘述。特别说明:两个参数分别是回调函数,和该回调接收的参数。

bookKeeping(TopLevelCallbackBookKeeping)结构如下:

book-keeping

handleTopLevelImpl函数会根据事件的target获取对应的ReactDOM实例。

如何获取呢?之前在渲染的时候,为每个DOM节点都存储了一个internalInstanceKey(形如__reactInternalInstance$xxx),就是为了这里方便反查。如果没有,就向上查父级,直到查到。

-ReactEventListener._handleTopLevel // 这里是通过注入的函数:ReactBrowserEventEmitter.handleTopLevel
 
handleTopLevel: function (topLevelType, targetInst, nativeEvent, nativeEventTarget) {
  var events = EventPluginHub.extractEvents(topLevelType, targetInst, nativeEvent, nativeEventTarget);
  runEventQueueInBatch(events);
}

handleTopLevel这个函数做了两件事:将原始事件包装为合成事件,然后执行它。

a.包装合成事件

这一步完成之后,要达到的目的是获取到一个synthecitMouseEvent合成事件。

synthetic-mouse-event

-EventPluginHub.extractEvents
-EventPluginRegistry.plugins[1].extractEvent // SimpleEventPlugin.extractEvent
-accumulateInto(events, extractedEvents)
-EventConstructor = SyntheticMouseEvent

这里EventConstructor通过继承,最终使用SyntheticEvent的构造函数。

另外获取到的合成事件的同时,会模拟事件捕获和事件冒泡,按照target来补充合成事件的监听函数(dispatchListeners)和对应ReactDOM实例(dispatchInstances)。

-EventPropagators.accumulateTwoPhaseDispatches(event)
=EventPluginUtils.traverseTwoPhase(
event._targetInst, // 目标DOM,这里是div元素
accumulateDirectionalDispatches, // 回调
event // 合成事件
)
=TreeTraversal.traverseTwoPhase // 模拟事件捕获(从上到下)和事件冒泡(从下到上)

traverseTwoPhase会按照phasedRegistrationNames(’onClickCapture’(捕获)和’onClick’(冒泡))从listenerBank里取注册的监听函数(clickFunc)。

b.执行事件

-runEventQueueInBatch(events)

这个函数先把事件组合成一个队列eventQueue,然后依次执行。

-executeDispatchesAndReleaseTopLevel
-EventPluginUtils.executeDispatchesInOrder
 
-executeDispatch(
event, // SyntheticEvent(合成事件) eg.{target: SyntheticMouseEvent, type: 'click'}
simulated, // false
dispatchListeners, // 自定义的监听函数 eg.clickFunc
dispatchInstances // ReactDOMComponent
);
 
-event.constructor.release // 从池中释放event

根据目标DOM节点创建虚拟节点fakeNode = document.createElement(‘react’),并在其上绑定(addEventListener)并触发(dispatchEvent)事件,最后解除(removeEventListener)。

合成事件SyntheticEvent

合成事件简单来说包装了基础的DOM事件,存储在nativeEvent属性。它包含了DOM事件的接口(包括stopPropagation和preventDefault),不同的是,合成事件是跨平台的。

合成事件是用PooledClass包装的,所以也是会重复利用,也就是说react里的事件是一次性的,一一旦事件执行完,所有属性就会被置null。所以,无法在异步方法(比如setTimeout、setState等)中获取到触发的合成事件,。

另外,SyntheticEvent使用的是Proxy构造。关于Proxy以后再看。

除SyntheticMouseEvent外,还有SyntheticKeyboardEvent、SyntheticFocusEvent、SyntheticTouchEvent等合成事件。SyntheticEvent文档 中,有关于onClick等鼠标事件的描述,可以看一下。

#react