使用事件监听
如果你发现自己使用useEffect
添加了大量的事件监听,你也许应该考虑移动这些逻辑到一个自定义hook。下面这个样例,我们创建了一个useEventListener
hook来处理检查addEventListener
方法是否支持,如果支持的话就添加事件监听,并且在清除的时候移除监听。
import React, { useState, useRef, useEffect, useCallback } from 'react';
// Usage
function App() {
// State 用来存储鼠标的坐标
const [coords, setCoords] = useState({ x: 0, y:0 });
// 使用 useCallback 的事件处理程序,这样引用就不会更改
const handler = useCallback(
({ clientX, clientY }) => {
// 更新坐标
setCoords({ x: clientX, y: clientY });
};
);
// 使用我们自己的 hook 添加事件监听
useEventListener('mousemove', handler);
return (
<h1>
The mouse position is ({coords.x}, {coords.y})
</h1>
);
}
// 下面使我们编写的自定义 hook
function useEventListener(eventName, handler, element = window) {
// 创建一个 ref 来存储处理程序
const saveHandler = useRef();
// 如果 handler 变化了,就更新 ref.current 的值。
// 这个让我们下面的 effect 永远获取到最新的 handler
useEffect(() => {
saveHandler.current = handler;
}, [handler]);
useEffect(
() => {
// 确保元素支持 addEventListener
const isSupported = element && element.addEventListener;
if (!isSupported) return;
// 创建事件监听调用存储在 ref 的处理方法
const eventListener = event => saveHandler.current(event);
// 添加事件监听
element.addEventListener(eventName, eventListener);
// 清除的时候移除事件监听
return () => {
element.removeEventListener(eventName, eventListener);
};
},
[eventName, element] // 如果 eventName 或 element 变化,就再次运行
);
};
export default App;
这里使用 useCallback 的原因是防止引用发生变更,如果使用普通的函数声明方式,每次该函数组件再次执行时就会重新声明函数,导致函数引用发生变化,自定义hook里监听 handler 的 useEffect 方法就会重复执行(因为声明函数的引用发生变化)。
const handler = useCallback(
({ clientX, clientY }) => {
// Update coordinates
setCoords({ x: clientX, y: clientY });
}
);
useEffect(() => {
savedHandler.current = handler;
}, [handler]);
useRef 类似于 class 的实例变量,这里的 savaHandler
主要用来存储最新的 handler
const savedHandler = useRef();
useEffect(() => {
savedHandler.current = handler;
}, [handler]);
初次加载代码的执行流程:
- const [coords, setCoords] = useState({ x: 0, y: 0 });
- const handler = useCallback(...
- useEventListener('mousemove', handler); // 进入到自定义 hook
- const savedHandler = useRef(); // 到这里App 和 useEventListener 的同步代码都加载完了(App里面没有 useEffect),下面使用异步的方式按顺序加载 useEventListener 的两个 useEffect
- 同步代码加载完后,异步代码进入加载序列,在异步加载之前,完成 return 操作。此时 coords 为 {x:0,y:0}
return (
<h1>
The mouse position is ({coords.x}, {coords.y})
</h1>
);
- 执行第一个 useEffect。目前 useEffect 闭包函数里面的 handler 变量为 undefined。传入的 handler 为上面声明的方法。两者不相等,所以这里执行内部代码,执行完毕后,saveHandler.curent 就存储了最新的 handler 方法。
useEffect(() => {
savedHandler.current = handler;
}, [handler]);
- 下面执行第二个 useEffect。eventName 和 element 都发生了变化,由 undefined 到对应的赋值,所以执行内部的逻辑代码,为 element 上的 eventName 事件绑定对应的 handler。
- 以上步骤完成初始化流程。
mousemove 事件监听阶段:
- 完成初始化步骤后,成功为 window 上的 mousemove 事件绑定对应的 handler。现在每当鼠标移动,就会触发对应的 hanlder。
- 该方法会调用 setCoords 设置 coords 的值。当值发生变化后,App 会重新执行整个逻辑流程。
const handler = useCallback(
({ clientX, clientY }) => {
// Update coordinates
setCoords({ x: clientX, y: clientY });
}
);
- const [coords, setCoords] = useState({ x: 0, y: 0 }); // 再次执行的时候有些 hook 方法忽略执行。(这里是我猜测的,因为再次执行整的流程的时候,对应的值不可能对重新初始化)。const handler = useCallback(... 同。
- useEventListener('mousemove', handler); // 进入自定义 hook
- const savedHandler = useRef(); // 不执行
- 两个 useEffect 异步执行。所以这里优先执行 return 语句,此时 coords 的值已经发生改变,return 后页面会发生改变。
return (
<h1>
The mouse position is ({coords.x}, {coords.y})
</h1>
);
- 执行两个 useEffect,他们的 deps 的值都没有发生改变,所以两个 useEffect 都不执行。
- 监听流程代码的执行顺序如上。
网友评论