啥是react的内置事件?
onClick, onMouseEnter这类事件就是react给开发者提供的内置事件, 我们来看看一些比较常见的应用
// Test组件
import React from 'react';
export default class Test extends React.PureComponent {
divClick = e => {
console.log('react事件: div被点击了')
}
buttonClick = e => {
console.log('react事件: button被点击了');
}
render() {
return (
<div style={ {
width: '200px',
height: '200px',
backgroundColor: 'green',
margin: '100px auto',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
} } onClick={ this.divClick }>
<button onClick={ this.buttonClick }>按钮</button>
</div>
)
}
}
该Test组件渲染进页面, 然后我们点击button按钮效果如下
其实结果如我们所料, 事件肯定会从button
按钮冒泡上div
按钮, 同时我们如果不想冒泡, 调用事件源对象上的stopPropagation
就可以坐到了, 一切看起来都是这么的顺滑, 跟原生js简直是一模一样
但是, 真的是一模一样吗?
我们来做一些其他的操作, 在Test组件下(或者你想写在任何地方都行, 但是不是js中就不行的哈), 我们加上这样一行代码
// Test.js
...
export default class Test extends React.PureComponent {
...
}
// 既然看起来是一样的, 那我们就给原生的dom也绑定个事件看看效果
document.getElementById('root').onclick = () => {
console.log('native事件: id为root的根结点被点击了')
}
当我们点击
button
按钮以后效果如下
id为root的dom元素作为所有react元素的容器, 我们的预期结果是从button
冒泡到div
最终冒泡到id为root的dom元素, 但是情况好像挺惊悚的, 因为我们发现id为root的dom元素的点击事件竟然是最先触发的?
可能有同学会想到是不是捕获了? 那你仔细看看
button
和div
的执行顺序, 你还觉得是捕获的原因吗?
这个问题咱们先放这, 再来看一个现象, 我们在id为root的dom元素的点击事件中加上一行代码
// Test.js
...
export default class Test extends React.PureComponent {
...
}
document.getElementById('root').onclick = e => {
...
e.stopPropagation(); // 没错我们阻止了事件冒泡
}
当我们点击
button
按钮以后效果如下
效果是相当的惊悚, 我们发现button
事件和div
事件双双都失效了???
还不够, 我们再来看一个现象, 我们在button
和在root的点击事件中分别这样操作
buttonClick = e => {
console.log('react事件: button被点击了');
console.log('我是button绑定事件中输出的e.target', e.target);
setTimeout(() => {
console.log('我是button绑定事件中在setTimeout输出的e.target', e.target);
}, 0)
}
...
document.getElementById('root').onclick = () => {
console.log('native事件: id为root的根结点被点击了');
console.log('我是root绑定事件中输出的e.target', e.target);
setTimeout(() => {
console.log('我是root绑定事件中在setTimeout输出的e.target', e.target);
}, 0)
}
我们点击
button
后效果又如下
这下好了, 不仅报错, 而且在
button
中setTimeout
中输出的e.target
竟然是null
看到这里, 笔者相信你心里也清楚了, react一定有着自己的事件机制且跟原生的事件机制完全不一样, 所以在这篇博客我们要探究的就是react的事件到底是什么机制, 不涉及源码, 也不写的过深, 但是接下来的这些总结, 笔者相信你看了以后对你的帮助会非常的大
其实是这样的, React几乎将所有的事件都交给了document
进行委托, 也就是说, react希望所有的事件最终都冒泡到document
身上, 然后从document
往下面去找触发回调函数并依次执行
关于上面的两个问题
- 为什么root的点击事件先于
button
和div
触发
答: 因为当我们点击button
的时候, 这个时候button的点击事件确实已经执行了, 并且开始冒泡, 从button
冒泡到div
再冒泡到root, 但是由于button
和div
绑定的是React事件, 即使这个按钮被真实的点击了, 系统也无法给出相应的反馈, 而root绑定的是真实的dom事件, 所以当冒泡到他的时候, 系统知道, 哦你要打印东西, 于是root的点击事件执行了, 然后继续冒泡, 冒泡到document, document在一开始就已经被react偷偷的绑定了点击事件, 这个时候document的点击事件触发, 通过事件源对象去找到dom元素原来是button
, 这个时候react就去找这个button
上绑定的React事件执行, 当button
执行完毕以后, 就找虚拟dom树中button的父节点, 如果父节点有绑定点击事件且无其他干扰因素就执行, 然后继续往上找, 直到找到虚拟dom树的中没有绑定事件的父级为止, 其实你更应该理解为, 不是button的点击事件没有执行, 而是点击事件发生以后点击事件的回调函数被react延迟执行了
- 为什么我们在root中阻止了事件冒泡以后,
button
和div
的事件都不处理了?
答: 有了前面的铺垫, 这个问题的答案就非常的简单了, 既然react将所有的事件都委托给了真实的document的dom事件中, 那么在root的点击事件中取消了事件冒泡, 那么document上的事件就不能被执行, 既然不能被执行, 那么怎么去找虚拟dom呢?
- 为什么在
react
中setTimeout
中取的e.target
是null
而且报错呢?
答: 这是因为React出于对性能的考虑, 设定了一个事件池, 这么说把, 就是react他会重用事件源对象这个e
, 就比如我们在button
的点击事件里拿到的e
是一个对象对吧, 在div
中拿到的e
也是一个对象是吧, 其实这两个对象是一个, react会在button
的点击事件回调函数的最后一行同步代码完成以后, 将button
的e
对象中的每一个属性赋值为null
, 然后在div
的点击事件回调执行之前, 将这个e
中的每一个属性又重新赋值为div
的各项事件源对象属性, 所以当我们异步操作react元素的事件源对象的时候, 往往得到的都是null
, 所以我们如果用一个全局变量保存了button
的e
的话, 在div
的点击回调函数中我们是发现这两个值是全等的
我们可以来看看这个报错信息
如果你英语好, 相信你已经看懂了, 没看懂也没关系, 这一段话就是react告诉你, 事件源对象是会被重用的, 而且会在重用之前将属性置空为null, 如果你真的想在异步代码中也要操作事件源对象, 那么你必须调用event.persist方法, 这个方法调用以后, react将不再重用该事件源对象, 而是会持久化它, 你随时可以使用
可能有同学注意到了笔者上面说react几乎将所有的事件都注册在了
document
上, 那意思是有例外喽? 没错还真的有例外
比如focus, input, 这些表单事件, 你说怎么冒泡给document, 所以这些事件react是直接在元素本身上监听了
如果你觉得上面的文字看着实在是头疼, 笔者帮你总结好了, 你可以很清晰的知道react的事件原理, 当然, 如果你想彻底理解, 上面的文字是你无法避免的
我们讨论的事件: 指的是react内置dom组件中的事件
-
react在内部实际上是将**几乎**所有元素的点击事件通过事件委托的方式绑定在了
document
上 -
在
document
的事件处理中, react会根据虚拟dom树来一步一步完成事件处理函数的调用 -
如果给真实的dom注册事件回调, 那么事件会先于任何一个react事件执行
-
如果给真实的dom注册事件回调, 回调中阻止了事件冒泡, 那么所有的react同类型事件一个都执行不了, 因为如果事件无法冒泡到
document
, react事件还怎么执行? -
如果在跟其他的库进行联合开发的时候, 要避免document上的事件冲突, 可以调用事件源对象中的e.nativeEvent.stopImmediatePropagation来阻止其他库给document注册事件
<button onclick={ e => { e.nativeEvent.stopImmediatePropagation(); // 这哥们一用, 其他库就没办法给document注册同类型事件了 } }></button>
-
react中的事件参数, 并非原生的dom事件参数, 而是react自行合成的一个对象, 该对象长得很像原生dom的事件参数, 在react事件中:
-
- e.stopPropagation, 可以阻止元素在虚拟dom树中冒泡, 但是它并不能阻止真实dom树的冒泡, 你想啊, react元素的事件都要等到真实dom的document执行了react元素事件才会被执行, 那你怎么阻止真实dom的冒泡呢
-
- e.nativeEvent可以拿到正儿八经的原生事件源对象
-
- 为了提高执行效率, react会重用事件源对象, 所以我们不要在异步操作中去使用事件源对象, 如果我们一定要使用, 请在使用前调用
e.per sist()
来阻止react重用该事件源对象
<button onclick={ e => { e.persist(); console.log(e.target); // button真实dom元素 setTimeout(() => { console.log(e.target); // button真实dom元素 }, 1000) } }></button>
- 为了提高执行效率, react会重用事件源对象, 所以我们不要在异步操作中去使用事件源对象, 如果我们一定要使用, 请在使用前调用
-
-
并不是所有的事件都在document上进行委托, 像input, focus这些表单事件, react还是选择在元素本身上进行监听, 因为他们根本无法冒泡, 你可以仔细想想 focus这种聚焦事件怎么冒泡, 所以也意味着, 即使我们在真实dom中阻止某类事件冒泡, 比如阻止click事件冒泡, react所有click失效, 阻止mouseenter, react所有mouseenter失效, 但是如果阻止focus, 不好意思, react的focus事件依旧生效
ok, 关于react的事件原理, 笔者已经基本写完了, 未来会再从源码层面真正的剖析它的事件池等机制, 希望这篇博客能够帮助到你, 也希望能够让你在读源码的时候对事件机制更加的得心应手