React的onClick和addEventListener有什么区别
2020年4月30日 • ... • ☕️ 5 min read
在React里添加事件处理一般直接添加属性
<button onClick={clickHandler}>
button
</button>
然后把需要处理的内容放到处理函数里即可。
那么它和addEventListener添加的事件监听有什么区别呢?
我们添加可能的几个类型:函数局部变量、useRef存储、useState、custom hooks保存,通过两种方法的输出值查看一下二者区别。
两个例子
通过onClick添加事件处理
function App() {
let counter = 0; // 局部变量
const countRef = React.useRef(0); // ref
let [count, setCount] = useState(0); // state变量
const [ct, setCt] = useCount(0); // 自定义hooks
const btnRef = React.useRef(null);
const clickHandler = () => {
counter++;
countRef.current++;
setCount(() => count + 1);
setCt(() => ct1 + 1);
console.log(`click: counter ${counter}, countRef ${countRef.current}, count ${count}, ct ${ct}`);
return (<div onClick={clickHandler}>button</div>);}
// custom hook
function useCount(init) {
const [count, setCount] = React.useState(init);
return [count, setCount];
};
输出:
click: counter 1, countRef 1, count 0, ct 0
click: counter 1, countRef 2, count 1, ct 1
click: counter 1, countRef 3, count 2, ct 2
click: counter 1, countRef 4, count 3, ct 3
click: counter 1, countRef 5, count 4, ct 4
click: counter 1, countRef 6, count 5, ct 5
直接给元素addEventListener添加事件处理
function App() {
let counter = 0; // 局部变量
const countRef = React.useRef(0); // ref
let [count, setCount] = useState(0); // state变量
const [ct, setCt] = useCount(0); // 自定义hooks
const btnRef = React.useRef(null);
const clickHandler = () => {
counter++;
countRef.current++;
setCount(() => count + 1);
setCt(() => ct1 + 1);
console.log(`click: counter ${counter}, countRef ${countRef.current}, count ${count}, ct ${ct}`);
};
useEffect(() => {
btnRef && btnRef.current.addEventListener('click', clickHandler); }, []);
return (<div ref={btnRef}>button</div>);
}
// custom hook
function useCount(init) {
const [count, setCount] = React.useState(init);
return [count, setCount];
};
输出:
click: counter 1, countRef 1, count 0, ct 0
click: counter 2, countRef 2, count 0, ct 0
click: counter 3, countRef 3, count 0, ct 0
click: counter 4, countRef 4, count 0, ct 0
click: counter 5, countRef 5, count 0, ct 0
click: counter 6, countRef 6, count 0, ct 0
两个例子有什么区别
首先,明确一下,函数内直接定义局部变量的方法严重不推荐,原因是状态无法跟踪,不可控;自定义hooks和useState意思类似,这里也不多讨论。
所以,关注的点主要是:useRef、useState在两种方法中的处理。
首先,可以看出来二者的调用栈不同
作用域和调用栈对比(左侧为onClick, 右侧为addEventListener)
第一次执行时,二者的作用域内容是一致的,由于是函数式组件,this为undefined,外层闭包为函数内容,再外侧是jsx,最外层是React环境。
所以不同点就是二者的执行机制。
之前说过React事件的注册和触发,我们知道事件的监听器其实全部是加在document上的,然后通过回调函数来统一处理。
先看onClick事件的触发
查找对应的fiber node还是在dispatchEvent
里
-dispatchDiscreteEvent
-dispatchEvent
-getClosestInstanceFromNode
每个DOM节点都保存了__reactInternalInstance
和fiber node建立映射关系。通过这个函数可以反查到fiber node,从而可以获取到当前节点的状态。
之后继续执行的流程就和之前基本一致了。虚拟的dom节点(fakeNode),然后触发事件(fakeNode.dispatchEvent
)。
由于调用了setCount,最后会update组件,所以再次执行函数组件(renderRoot)。定义的局部变量会初始化,而useRef、useState
、custom hook的值由于在React环境内,状态得以保留。
第二次调用时,作用域内容已经不一样了。
onclick第二次以后调用时作用域的值
可以想象到,为什么在调用到clickHandler之前,有那么长的调用栈,除了事件一层一层传递,更重要的是保证事件函数执行时,上下文环境是正确的。
如何保证呢?肯定要把函数脱离原始的作用域,放到React里维护,以同步更新作用域内的值。
可以看到,React为每个事件函数,都绑定了单独的作用域。以特殊标记__reactEventHandlers$XXX
存放在fiber node的stateNode属性里。
listener函数存储位置
每次事件执行时,都会同时更新该作用域内的各个值,以保证状态一致。
addEventListener
调用栈可以看到,clickHandler直接调用。
第一次执行,当调用到setCount时,会走dispatchAction
,没问题,这是setState注册时绑定好的
-dispatchAction(fiber, queue, action)
其中fiber是App节点,是各个hooks存在的地方。
queue是记录上次更新的内容
// queue的结构
{
dispatch:function () { … }
last:null
lastRenderedReducer:function basicStateReducer(state, action) { … }
lastRenderedState:0
}
action是setCount方法的调用内容(参数),可以是值或者一个函数。
好了,明白上面三个内容,就开始执行了。
由于上次是0,这次执行之后是1,所以会触发一次组件更新。
第二次,顺序执行到setCount,dispatchAction时,取到上次是1,到这里都是意料之中的。
接着到执行action,可以看到此时的作用域内各个值
第二次以后执行监听事件时作用域的值
就是说,fiber还是一直在更新,但是到这里的时候,setState还是按照闭包的初始值来计算的。
结果就是,state判断一致,直接跳过,而打印出来的,是闭包里的值。
有趣的是,如果放在render出来的内容,那么显示的结果是
click: counter 0, countRef 0, count 0, ct 0
click: counter 0, countRef 1, count 1, ct 1
click: counter 0, countRef 1, count 1, ct 1
click: counter 0, countRef 1, count 1, ct 1
click: counter 0, countRef 1, count 1, ct 1
为什么呢?因为setCount导致的更新只有“0到1”这一次,所以后面再触发,其实DOM元素根本没更新。
结论
在99%的情况下,请使用属性中的onClick(或同类事件),而不要自己通过addEventListener
注册监听函数。特殊情况必须要使用的时候,也请尽量避免监听函数内的state更新操作。
如果非要更新,比如:使用了第三方的库,需要单独添加的监听函数,有没有解决办法呢?
有,需要额外的状态控制,使用uesReducer+useContext
,或者redux。