前提
最近通过阅读 React 官方文档的事件模块,有了一些思考和收获,在这里记录一下~
调用方法时需要手动绑定 this
先从一段官方代码看起:
代码中的注释提到了一句话:
This binding is necessary to make this work in the callback
this 的绑定是必须的,其实这一块是比较容易理解的,因为这并不是 React 的一个特殊点,而是 Javascript 这门语言的特性。
可以看到,调用的是 this.handleClick 函数,handleClick 函数里面又读取到了 this 属性,但是该函数的调用位置又是在 render 函数里面,render 返回的是一个 JSX,最后经过 babel 编译成调用 React.createElement 函数,
在这之前,我们掌握的是 this 永远指向的是最后调用它的对象,经过这样的一个转换,实际上 this 最后指向的是 undeined 了,那么调用 handleClick 函数自然会报错。
当然,如果你不在函数里面使用 this 的话,通常会没事,但并不建议这么做。
关于 this 的指向与 function 的原理,推荐阅读 how functions work in JavaScript
既然知道了是因为 this 的指向原因而采用绑定的做法,那当然可以用箭头函数来解决了,箭头函数中的 this 是在定义函数的时候绑定,也就是说 this 是继承自父执行上下文,如下:
这样 this 也能达到我们的预期效果
合成事件 SyntheticEvent
先从官方上的一段话看起,他的意思是合成事件是 React 根据 W3C 标准定义的,无需担心浏览器之间的差异
Here, e is a synthetic event. React defines these synthetic events according to the W3C spec, so you don’t need to worry about cross-browser compatibility
样看起来 React 的合成事件只是兼容浏览器?答案当然是远远不止啦!
在探寻其优点之前,我们先看一下其是怎样的一个机制。
React 的事件机制其实网上有很多同学都分析过了,他并没有将事件注册在对应的元素或者组件上面,而是通过委托的方式,将所有的事件都注册到了 document 对象上,并统一调用一个 dispatch 回调函数,其流程图如下
我们也可以从一个实际的简单例子看看:
我们把回调函数绑定到了 button 上,但是在事件上却没有看到 button 元素,但是却有 document,并且可以看到他的回调函数就是 dispatchInteractiveEvent
最后触发事件的回调函数时,在原生的 DOM 会传入一个事件属性 event,但是因为 React 将 所有事件委托给 document 处理,那么这个 event 就和我们想要的不一样,如 target 指向的是 document,于是 React 就有了自己的一个合成事件,通过一个叫 SyntheticEvent 的基类来生成所需要的事件属性,并传入回调函数作为方法。
说到底,React 就是把所有事件委托给 document 处理,那么这样做有什么好处:
可以统一在组件挂载和卸载时做处理 只需要注册一个事件即可,节省内存开销 可以手动控制事件流程,特别是对 state 的 batch 处理(参考 React 系列的 setState)
可以统一在组件挂载和卸载时做处理
只需要注册一个事件即可,节省内存开销
可以手动控制事件流程,特别是对 state 的 batch 处理(参考 React 系列的 setState)
事件属性会在事件调用后被回收,即不能异步访问
老规矩,先上一段代码:可以看到在 setTimeout 函数中,访问事件属性是 null。这是为啥?
其实这也是合成事件的一个优化手段。React 会在事件调用完成后清理掉属性,否则每点击一次就生成一个事件,那么内存的开销会越来越大,具体的代码可以在后面的源码分析中看到:
当然了,React 也可以手动设置不回收,如下:
If you want to access the event properties in an asynchronous way, you should call event.persist() on the event
我们可以通过调用 event,persist 来设置不回收。
事件机制的源码分析
注册阶段
首先在某一个任务单元 fiber 调用 compeleteWork 函数时,React 会判断其是否具有事件属性,如果有则调用 ensureListeningTo 函数
ensureListeningTo 函数主要是获取到 document 对象,并调用 listenTo 函数
listerTo 函数 主要是通过调用 trapBubbledEvent 或者 trapCapturedEvent 将事件放在 document 事件上监听
trapBubbledEvent 主要是监听事件,但也可以看出,所有事件最后触发的都是注册在 document 上的 dispatch 函数
调用阶段
dispatch 函数,主要是获取实际触发的元素以及对应的 fiber,最后调用 batchedUpdates 函数,batchedUpdates 函数里面的逻辑主要是关于 setState 的,这里主要是看事件机制,只要知道最后调用的是 handleTopLevel(bookkeeping) 就好
handleTopLevel 函数主要是拿到需要触发事件的相关 fiber,并调用 runExtractedEventsInBatch 函数
extractEvents 函数是一个生成 React 事件的函数,React 事件是通过继承一个通用类 SyntheticEvent 生成的,如一个鼠标事件的生成
React 事件内部做了优化,只要生成过 SyntheticMouseEvent 类,就会再释放事件的时候将这个类存储起来,在下一个事件触发时可以直接使用
React 生成事件后,会调用 accumulateTwoPhaseDispatches(event) 函数,该函数一直追溯下去,最后会调用 traverseTwoPhase 函数,
traverseTwoPhase 函数主要是获取祖先组件的 fiber,并进行捕获和冒泡的阶段处理
accumulateDirectionalDispatches 函数相对简单,就是把 fiber 上对应的事件函数赋值给 evnet 的_dispatchListeners 属性
React 事件获取完成后,回到 runExtractedEventsInBatch 函数继续调用 runEventsInBatch(events, false); 函数的中间作了一系列的处理,但最后执行的是 executeDispatchesAndRelease 函数
executeDispatchesAndRelease 函数会在执行完事件后判断用户是否有设置不销毁事件,如果没有,则销毁事件并保存事件类,一个事件类实例一次并重复使用,这也是为什么官方提到事件属性只能在当前循环中读到
继续往下走,最后执行的函数是 invokeGuardedCallbackDev,该函数通过注册一个自定义的元素 <react> 和自定义的事件,并触发它来达到执行回调函数的功能
流程总结
通过 Fiber 中的属性,将事件统一委托 注册到 document 上,并为 document 注册相应的事件回调函数 dispatch 函数。
先获取实际触发元素对应的 fiber.
生成相应的 React 事件属性 event,将对应的回调函数赋值给 event._dispatchListeners,将 fiber 赋值给 event._dispatchInstances
通过 fiber 向上遍历,找到所有的祖先 fiber,并按原生事件的机制先捕获后冒泡的执行事件
注册一个 react 节点,为其注册一个监听事件并触发来执行事件回调函数
最后,根据用户的设置,决定是否释放事件。