在 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 代替第三方库的事件,初始代码
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 变化时,事件重复绑定的问题
解决事件重复绑定问题,首先想到的是事件卸载
你很容易就会想到这样写
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 变化后就不是同一个事件函数了,无法移除事件
如何解决这个问题呢?
把事件函数保存为状态:
具体的代码:
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 保存的数据虽然不能用于页面渲染,但可以作为 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 需要手动维护
这一步如何优化呢?
方法一和方法二,优化的方式都一样:将依赖项是 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]);
当前还处于 RFC 阶段
const useEvent = (handler) => {
const handlerRef = useRef(null);
useLayoutEffect(() => {
handlerRef.current = handler;
});
return useCallback((...args) => {
const fn = handlerRef.current;
return fn(...args);
}, []);
};
ahook 实现
function useMemoizedFn(fn) {
const fnRef = useRef(fn);
fnRef.current = useMemo(() => fn, [fn]);
const memoizedFn = useRef();
if (!memoizedFn.current) {
memoizedFn.current = function (_this, ...args) {
return fnRef.current.apply(_this, args);
};
}
return memoizedFn.current;
}
这个问题困扰了我很久,写业务时,我一直用方法一,随着依赖项越来越多,维护是个噩梦(方法三也是噩梦)
我一直在想如何在 addEventListener 中拿到最新的 state
今天本来想记录下方法一和方法三的
在一点点写下笔记的时候,发现了方法二,可以利用闭包解决事件卸载问题,从而又发现了优化维护依赖项的方法
如果今天不写笔记,这个问题还会继续困扰着我
原文:https://segmentfault.com/a/1190000043365896
我们都知道addEventListener() 的参数约定是:useCapture是可选参数,默认值为false,目前DOM 规范做了修订:addEventListener() 的第三个参数可以是个对象值了。passive就是告诉浏览器我可不可以用stopPropagation...
在WebApp或浏览器中,会有点击返回、后退、上一页等按钮实现自己的关闭页面、调整到指定页面、确认离开页面或执行一些其它操作的需求。可以使用 popstate 事件进行监听返回、后退、上一页操作。
具有层级关系的结构中,使用了pointer-events:none 属性将会使当前元素中的事件不会被捕获,从而实现了点穿的效果。而当代码示例中假如top元素具有子元素且显示指定pointer-events属性不为none的时候,top元素注册的事件将会被捕获/冒泡触发
事件对象 event,JavaScript 将事件event作为参数传递,IE中把 event 事件对象作为全局对象 window 的一个属性,获取鼠标在网页中的坐标 = 鼠标在视窗中的坐标 + 浏览器滚动条坐标
何为默认事件?比如 a 会跳转页面,submit 会提交表单等。普通js方法:e.preventDefault()函数。Vue.js方法: .prevent 是vue 的内置修饰符,调用了 event.preventDefault()阻止默认事件
js keyup、keypress和keydown事件都是有关于键盘的事件,当一个按键被pressed 或released在每一个现代浏览器中,都可能有三种客户端事件。
Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行环境。Node.js 使用了一个事件驱动、非阻塞式 I/O的模型,使其轻量又高效。比如,文件操作中的fs事件流,网络编程所用到的tcp,http模块等,当你回想自己写的程序后,会发现很多操作都基于事件驱动,Events类。
在写移动端导航的时候经常用到点击按钮出现/隐藏导航条的情况,最常见的方法当然还是前端框架直接调用,省心省力,不易出错;当然还有使用纯JS实现的小代码段。我这里整理了纯CSS实现方式,给需要的人和给自己做个笔记:实现原理利用CSS伪类:target
我做的是一个table的编辑功能,当移入某行的时候展示编辑状态,在移出某行的时候显示的是原始状态,此时遇到一种情况,就是.当mousenter事件触发之后,由于鼠标移动得太快,同一个tr上绑定的mouseleave事件压根儿就没有执行。
js事件传播流程主要分三个阶段:事件捕获阶段、处于目标阶段、事件冒泡阶段。在我们平常用的addEventListener方法中,一般只会用到两个参数,一个是需要绑定的事件,另一个是触发事件后要执行的函数
内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!