在 React Hooks
中使用第三方库的事件时,很多人会写成这样(指的就是我):
const [count, setCount] = useState(0); useEffect(() => { const library = new Library(); library.on("click", () => { console.log(count); // 拿不到最新的 count }); }, []);
这样写会有问题:
它只会在这个组件加载时,绑定事件,如果这个事件中用到了其他的 state
,那么这个状态发生变化时事件中是拿不到最新的 state
你会想到,我把 state
放到依赖项中:
const [count, setCount] = useState(0); useEffect(() => { const library = new Library(); // click 事件会重复绑定 library.on("click", () => { console.log(count); }); }, [count]);
这样做又会有新问题:click
事件会重复绑定
这时候你说那我先卸载 click
事件,在绑定事件:
const [count, setCount] = useState(0); useEffect(() => { const library = new Library(); library.on("click", handleClick); return () => { // 卸载不掉事件,还是会重复绑定 handleClick && library.un("click", handleClick); }; }, [count]); const handleClick = () => { console.log(count); };
你惊奇的发现,居然卸载不掉之前的事件,还是会重复绑定事件。
如何解决这个问题呢?
使用 addEventListener 代替第三方库的事件
这里使用 addEventListener
代替第三方库的事件,初始代码
const Test = (props) => { const ref = useRef(); const [count, setCount] = useState(0); useEffect(() => { const handleClick = (event) => { console.log("clicked"); console.log("count", count); }; const element = ref.current; element.addEventListener("click", handleClick); return () => { element.removeEventListener("click", handleClick); }; }, []); const onClickIncrement = () => { setCount(count + 1); }; return ( <> <h2>Test</h2> <button onClick={onClickIncrement}>点击 +1</button> <div>count: {count}</div> <button ref={ref}>Click Test Button</button> </> ); };
方法一:state 变化,卸载/绑定事件
将 state
放在依赖项中,就要解决 state
变化时,事件重复绑定的问题
解决事件重复绑定问题,首先想到的是事件卸载
你很容易就会想到这样写
useEffect(() => { handleClick && ref.current.removeEventListener("click", handleClick); ref.current.addEventListener("click", handleClick); }, [count]); const handleClick = () => { console.log(count); };
这在 React Hooks
中是一个坑,state
变化后会 handleClick
事件函数会重新声明,新的 handleClick
和之前的 handleClick
不是一个事件函数,导致 removeEventListener
移除的事件函数不是之前的事件函数
那你又会想到,我给 handleClick
加个 useCallback
useEffect(() => { handleClick && ref.current.removeEventListener("click", handleClick); ref.current.addEventListener("click", handleClick); }, [count]); const handleClick = useCallback(() => { console.log(count); }, []);
这样写的话还是会有同一个问题:依赖项为空数组,就拿不到最新的 state
;依赖项中放入 state
,state
变化后就不是同一个事件函数了,无法移除事件
如何解决这个问题呢?
把事件函数保存为状态:
- 当
count
变化时,挂载事件,同时将事件函数保存为state
- 当
eventFn.fn
变化时,在useEffect return
中卸载之前的事件函数(这里利用的是闭包)
具体的代码:
const Test = () => { const ref = useRef(); const [count, setCount] = useState(0); const [eventFn, setEventFn] = useState({ fn: null }); useEffect(() => { mountEvent(); }, [count]); const mountEvent = () => { if (!ref.current) return; // eventFn.fn && ref.current.removeEventListener("click", eventFn.fn); // 下面看不懂的话,也可以这样写 ref.current.addEventListener("click", handleClick); setEventFn({ fn: handleClick }); }; useEffect(() => { return () => { eventFn.fn && ref.current.removeEventListener("click", eventFn.fn); // 这里用的是闭包,和上面注释部分任选其一 }; }, [eventFn.fn]); const handleClick = () => { console.log(count); }; const onClickIncrement = () => { setCount(count + 1); }; return ( <> <h2>Test</h2> <button onClick={onClickIncrement}>点击 +1</button> <div>count: {count}</div> <button ref={ref}>Click Test Button</button> </> ); };
方法二:使用闭包的方式卸载事件
利用闭包,可以将方法一简化
const Test = () => { const ref = useRef(); const [count, setCount] = useState(0); useEffect(() => { const element = ref.current; element.addEventListener("click", handleClick); return () => { element.removeEventListener("click", handleClick); }; }, [count]); const handleClick = () => { console.log(count); }; const onClickIncrement = () => { setCount(count + 1); }; return ( <> <h2>Test</h2> <button onClick={onClickIncrement}>点击 +1</button> <div>count: {count}</div> <button ref={ref}>Click Test Button</button> </> ); };
useEffect return
中的变量用的是闭包,这点刚开始学的时候不好理解
方法三:使用 ref 保存状态
ref
保存的数据虽然不能用于页面渲染,但可以作为 state
备份,在 state
变化时更新 ref
在事件函数中就能拿到最新的 stateRef
const Test = () => { const ref = useRef(); const [count, setCount] = useState(0); const countRef = useRef(count); useEffect(() => { countRef.current = count; }, [count]); useEffect(() => { const element = ref.current; element.addEventListener("click", handleClick); }, []); const handleClick = () => { console.log(countRef.current); }; const onClickIncrement = () => { setCount(count + 1); }; return ( <> <h2>Test</h2> <button onClick={onClickIncrement}>点击 +1</button> <div>count: {count}</div> <button ref={ref}>Click Test Button</button> </> ); };
优化 state 手动维护
上面三种方法,都有个问题,state
需要手动维护
这一步如何优化呢?
方法一和方法二,优化的方式都一样:将依赖项是 count
改为 state
const [state, setState] = useState({ count: 0 }); useEffect(() => { // ... }, [state]);
方法三的优化是,用 stateRef
保存 ref
对象,当 state
变化时,遍历 state
给 stateRef
赋值
事件函数中使用 stateRef
const [state, setState] = useState({ count: 0 }); const stateRef = useRef({}); useEffect(() => { Object.keys(state).forEach((key) => { stateRef.current[key] = state[key]; }); }, [state]);
到此这篇关于详解如何在React中优雅的使用addEventListener的文章就介绍到这了,更多相关React addEventListener内容请搜索好代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持好代码网!