共计 4597 个字符,预计需要花费 12 分钟才能阅读完成。
谈谈 React 的新提案:useEvent
2022 年 5 月 5 日,Dan Abramov 在 React RFC 上提交了一个新 hook 的提案:useEvent。其目标是返回一个永远援用不变 (always-stable) 的事件处理函数。
没有 useEvent 时咱们如何写事件函数
首先咱们来看一下这段代码
function Chat() {const = useState("");
const onClick = () => {sendMessage(text);
};
return <SendButton onClick={onClick} />;
}
为了拜访最新的 state,onClick
在每次 Chat
组件产生更新时,都会申明一个新的函数 (援用变动),这会导致SendButton
组件每次都承受一个新的 prop,React 的比拟两个组件节点是否要 diff 前,会对 props 做浅比拟(Object.is),所以每次 props 无意义的变动显然是对 diff 性能不利的。
同时它还会毁坏你的 memo 优化,比方你的 SendButton
做了如下设计:
const SendButton = React.memo(() => {});
这时你可能会想到应用 useMemo
或者 useCallback
来优化父组件的 onClick
函数
function Chat() {const = useState("");
const onClick = useCallback(() => {sendMessage(text);
}, );
return <SendButton onClick={onClick} />;
}
然而这样当 text
变动时,援用还是会变动,仍然会带来子组件的不必要更新,设计不当甚至会触发子组件 useEffect 的 re-fired。SendButton
基本不关怀 text
的变动。而且当函数非常复杂时,可能会漏写依赖(当然你能够通过 eslint 来保障),导致每次应用的都是初始 state,从而造成难以追踪的 bug。
而新的 hook 提案 useEvent
,你能够做到这样:
function Chat() {const = useState("");
const onClick = useEvent(() => {sendMessage(text);
});
return <SendButton onClick={onClick} />;
}
onClick
曾经始终是援用不变的了,而且能够拜访到最新的 text。
useEvent 是如何实现的
它看上去如同很神奇,你也能够本人简略实现一个相似的 hook,最外围的中央就是应用 useRef 维持最新援用以及缓存住外层的 function:
const useEvent = (eventHandler) => {const eventHandlerRef = useRef(eventHandler);
// 每次 useEvent 被调用都返回不变的值,但外部理论执行的是最新的函数
return useMemo((...args) => {return eventHandlerRef.current(...args);
}, []);
};
官网给的一个相似实现是这样的:
// (!) Approximate behavior
function useEvent(handler) {const handlerRef = useRef(null);
// In a real implementation, this would run before layout effects
useLayoutEffect(() => {handlerRef.current = handler;});
return useCallback((...args) => {
// In a real implementation, this would throw if called during render
const fn = handlerRef.current;
return fn(...args);
}, []);
}
其实,真正的实现比起上述两种形式要简单一些,作为一个应用度极广的框架,必须要须要思考一些边界条件和束缚。
- 在组件 render 时应用被 useEvent 包裹的函数须要抛出谬误。因为它的设计是为了包裹事件函数,事件函数不应该在 render 时调用。这也是为什么上述代码有
useLayoutEffect
,它也保障了每次事件触发时都是最新的,因为视图 / 事件的更新肯定在useLayoutEffect
之后。同时,useEvent 外部批改 state 也是平安的,因为它不会在 render 期间被调用,不会批改组件的 output。 - 其实
handlerRef.current
的更新产生在比所有useLayoutEffect
更提前的时刻,这个保障了当 layout 时,不会存在旧版本的 handler,不会呈现状态割裂的问题 - 第 1 处的设计还间接的优化了服务端渲染的平安和性能,因为它不能在 render 时运行,而服务端是不存在事件的,防止了报错。同时,既然 useEvent 对服务端渲染没有意义,那么服务端构建的包里能够跳过 useEvent 的打包,优化了包体积。
你什么时候不应该应用 useEvent
- 一般的函数(非事件回调)仍然用原来的 useCallback
function ThemedGrid() {const theme = useContext(ThemeContext);
const renderItem = useCallback((item) => {
// Called during rendering, so it's not an event.
return <Row {...item} theme={theme} />;
},
[theme]
);
return <Grid renderItem={renderItem} />;
}
因为有 render 期间的报错机制,开发者也不太可能在这种场景下用 useEvent
- 不是所有的 useEffect 依赖函数都应该是事件
function Chat({selectedRoom}) {const { createKeys} = useContext(EncryptionSettings);
// ...
useEffect(() => {const socket = createSocket("/chat/" + selectedRoom, createKeys());
// ...
socket.connect();
return () => socket.disconnect();
}, [selectedRoom, createKeys]); // ✅ Re-runs when room or createKeys changes
}
这里的 createKeys
不应该应用 useEvent,因为 effect 中的函数不是事件,也不须要放弃援用不变,因为它须要在 createKeys
变动时从新建设 socket
- 可能会导致 useEffect 不再响应式
上面是一个谬误的写法
function Chat({selectedRoom, theme}) {
// ...
// 🔴 This should not be an event!
const createSocket = useEvent(() => {const socket = createSocket("/chat/" + selectedRoom);
socket.on("connected", async () => {await checkConnection(selectedRoom);
onConnected(selectedRoom);
});
socket.on("message", onMessage);
socket.connect();
return () => socket.disconnect();
});
useEffect(() => {return createSocket();
}, []);
}
要晓得一点的是,useEvent 是非响应式的。因为它是事件,最终会被动调用,并不需要随着状态变动而立刻响应。所以当 selectedRoom
变动时,effect 不再从新建设 socket 了,只管 createSocket
始终能够拿到最新的selectedRoom
,但它须要的是被动触发。
正确的写法应该是应用 useCallback
且依赖 selectedRoom
,useEffect
依赖useCallback
useEvent 的『毛病』是什么
- 毫无疑问它减少了 hooks 的概念,带来了更多的心智累赘,你须要判断这里该不该用 useEvent,还是用 useCallback
- 因为须要一个比 layoutEffect 更提前的期间,它不可避免的须要改变 fiber tree commit 阶段的逻辑。然而相比于让社区在第三方库中自行提供各自的不完满的解决方案,这种付出还是值得的。
- 它的体现仿佛超出了单纯的 event 边界,更应该叫
useStableCallback
或者useCommittedCallback
,官网给它取useEvent
这一名字,是为了帮忙开发者们更容易建设『它应该被用于事件』这一心智模式。 - 它有一些非凡的边界条件下会呈现问题,不过这次要是因为代码编写有问题带来的,并不是它本身的问题。但正因为人是最难管制的,所以这种问题也是最难阻止的,开发者应该更留神本人的书写标准:
比方 useEvent 外面有异步逻辑
function App() {const [count, setCount] = useState(0);
const sayCount = useEvent(async () => {console.log(count);
await wait(1000);
console.log(count);
});
return <Child onClick={sayCount} />;
}
await 前后输入值是一样的,因为 await 前面的回调保留了 count 闭包。count 仅仅是本次 render 的状态快照,所以函数内异步期待时,即使内部又把 count 改了,以后这次函数调用还是拿不到最新的 count,而 ref 办法是能够的。所以事件中尽量不要有异步。
另外还有『条件判断式的 event』,比方你写出了这样的代码onSomething={cond ? handler1 : handler2}
,天然是没方法帮你放弃援用不变的。
此外在 react 更新中也会有『割裂』问题,unmounting layout effects 时应用的是上一次 render 时的 event,然而 非 layout effect 卸载时应用的是新版本的 event(下一次更新时的 event 可能发生变化了)。这就相似于在 unmounting layout 和 non-layout effects 期间读 ref 后果不统一的状况。
集体对 useEvent 的认识
useEvent 次要作用是维持援用不变的事件,能够用非常简洁的代码缩小援用变动带来的问题。然而它自身也带来了更多的概念。正如下面的毛病里写的,你须要时刻留神那些问题。而且目前官网也仍然有一些待解决的问题 https://github.com/reactjs/rfcs/blob/useevent/text/0000-useevent.md#unresolved-questions。总之对于这个 RFC 集体并没有太多欣慰,未来有则用,毕竟是官网给出的最佳实际,没有也能够有其余解决办法。