事件也是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
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)
,这里propKey
是onClick
-enqueuePutListener(this, propKey, nextProp, transaction)
enqueuePutListener
是事件的关键函数,主要处理流程如下
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
。
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结构
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结构
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
合成事件。
合成事件(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
。所以,无法在异步方法(比如setTimeout
、setState
等)中获取到触发的合成事件,。
另外,SyntheticEvent使用的是Proxy
构造。关于Proxy
以后再看。
除SyntheticMouseEvent
外,还有SyntheticKeyboardEvent
、SyntheticFocusEvent
、SyntheticTouchEvent
等合成事件。SyntheticEvent文档 中,有关于onClick
等鼠标事件的描述,可以看一下。