原理篇:事件系统
React为什么要实现一套自己的事件系统?
A:为了兼容各浏览器对事件处理的差异性/
冒泡和捕获
- 冒泡:事件从最内层的元素开始,一直向上传播,直到document对象。
- 捕获:事件从最外层开始,直到最具体的元素。
- addEventListener(event, function,useCapture):useCapture=true,事件捕获阶段调用处理函数;useCapture=false,事件冒泡阶段调用处理函数;
react 中, 想要在捕获阶段执行可以将事件后面加上 Capture 后缀。
<div>
<button onClick={handleClick} onClickCapture={ handleClickCapture } >点击</button>
</div>
- 阻止冒泡:e.stopPropagation();
- 阻止默认行为
原生事件: e.preventDefault() 和 return false ;
react事件:e.preventDefault()
在react中给元素绑定的事件并不是真正的事件处理函数,所以无法使用return false 阻止默认行为。
React如何模拟阻止事件冒泡
function runEventsInBatch(){
const dispatchListeners = event._dispatchListeners;
if (Array.isArray(dispatchListeners)) {
for (let i = 0; i < dispatchListeners.length; i++) {
if (event.isPropagationStopped()) { /* 判断是否已经阻止事件冒泡 */
break;
}
dispatchListeners[i](event) /* 执行真正的处理函数 及handleClick1... */
}
}
}
事件合成
- React的事件不是绑定在元素上,而是统一绑定在顶部的容器上。
- 元素绑定的事件不是原生事件,而是react合成事件。比如onClick绑定的是click事件,onChange事件又blur ,change ,focus等多个事件组成。
绑定在document上的事件处理函数,是上面所写的handleClick吗?
A:绑定在 document 的事件,是由react中的dispatchEvent统一处理。只要是 React 事件触发,首先执行的就是 dispatchEvent 。
const listener = dispatchEvent.bind(null,'click',eventSystemFlags,document)
/* TODO: 重要, 这里进行真正的事件绑定。*/
document.addEventListener('click',listener,false)
事件绑定
给元素绑定的事件 onChange onClick ,最后去了哪里呢?
A: 会保存在对应 DOM 元素对应的 fiber 对象上。通过dispatchEvent 注册事件,dispatchEvent的执行会传入事件元素本身,通过元素可以找到对应的 fiber。
真实DOM和fiber是通过key和stateNode一一对应的。
const listener = dispatchEvent.bind(null,'click',eventSystemFlags,document)
/* TODO: 重要, 这里进行真正的事件绑定。*/
document.addEventListener('click',listener,false)
事件触发流程
下面代码触发顺序是怎样的?
export default function Index(){
const handleClick1 = () => console.log(1)
const handleClick2 = () => console.log(2)
const handleClick3 = () => console.log(3)
const handleClick4 = () => console.log(4)
return <div onClick={ handleClick3 } onClickCapture={ handleClick4 } >
<button onClick={ handleClick1 } onClickCapture={ handleClick2 } >点击</button>
</div>
}
- dispatchEvent 执行会传入真实的事件源 button 元素本身,接下来就是批量更新。
export function batchedEventUpdates(fn,a){
isBatchingEventUpdates = true; //打开批量更新开关
try{
fn(a) // 事件在这里执行
}finally{
isBatchingEventUpdates = false //关闭批量更新开关
}
}
- 通过 onClick 找到对应的处理插件 SimpleEventPlugin ,合成新的事件源 e 。
- 形成事件执行队列:
3.1 如果遇到捕获阶段事件 onClickCapture ,就会 unshift 放在数组前面。以此模拟事件捕获阶段。
3.2 如果遇到冒泡阶段事件 onClick ,就会 push 到数组后面,模拟事件冒泡阶段。
3.3 一直收集到最顶端 app ,形成执行队列。 - 依次执行队列的函数。
如上点击一次按钮,4个事件执行顺序是这样的:
- 第一次事件源查找在button上。
- handleClick1冒泡事件push处理,handleClick2捕获事件unshift处理。形成结构 [ handleClick2 , handleClick1 ]。
- 接着向上查找事件源,遇到div。
- handleClick3 冒泡事件 push 处理,handleClick4 捕获事件 unshift 处理。[handleClick4, handleClick2 , handleClick1, handleClick3 ]
- 依次执行数组里面的事件,所以打印 4 2 1 3。
面试题1:react中的事件是怎么绑定的?
A: react的事件都是合成事件,在17之前被统一绑定在document上,17之后被绑定在app容器上。
面试题2: react是如何确定绑定事件对应的元素?
A: 绑定的事件会保存在DOM对应的fiber上。通过dispatchEvent注册事件,dispatchEvent在执行的时候会传入事件元素本身,通过该元素可以找到对应的fiber。元素fiber和dom是一一对应的。