Axiu Blog
ates`流程,所以会用到前面文章中的内容。 还是以之前的`Hello`为例,这次给`div`添加`onClick`函数事件,函数名为`clickFunc`。完整代码如下 class Hello extends React.Component { constructor(props) { super(props); this.stat
事件也是React里使用频率很高的操作,各种`onClick`、`onFocus/onBlur`、`onChange`、`onSubmit`都是经常使用的。事件触发同样是update,也会使用`ReactUpdates.batchedUpdates`流程,所以会用到前面文章中的内容。 还是以之前的`Hello`为例,这次给`div`添加`onClick`函数事件,函数名为`clickFunc`。
事件也是React里使用频率很高的操作,各种`onClick`、`onFocus/onBlur`、`onChange`、`onSubmit`都是经常使用的。事件触发同样是update,也会使用`ReactUpdates.batchedUpdates`流程,所以会用到前面文章中的内容。 还是以之前的`Hello`为例,这次给`div`添加`onClick`函数事件,函数名为`clickFunc`。
React-事件的注册和触发
Max

事件也是React里使用频率很高的操作,各种onClickonFocus/onBluronChangeonSubmit都是经常使用的。事件触发同样是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

Hello {this.props.name}! {this.state.now}

; } clickFunc() { console.log(this.state.now); } }

事件的注册

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

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

判断registrationNameModules.hasOwnProperty(propKey),这里propKeyonClick

-enqueuePutListener(this, propKey, nextProp, transaction)

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

listenerReg

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事件,callbackReactEventListener.dispatchEvent

2、存储监听事件

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

listenerBank

listenerBank数据结构

-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的属性。

callbackQueue

CallbackQueue结构

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)结构如下:

bookKeeping

bookKeeping结构

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

合成事件(synthecitMouseEvent)的结构

-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。所以,无法在异步方法(比如setTimeoutsetState等)中获取到触发的合成事件,。

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

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

Comments