今年夏天的一个好日子,React 传奇人物 Dan Abramov 为期待已久的 useEvent 钩子发布了一个 polyfill。 介意我们看看吗? 一点上下文 如果您最近没有关注"新闻",您可能错过了 useEvent 的 RFC。 长话短说,以下是 React 团队对此的评价: 我们怀疑 useEvent 是 Hooks 编程模型中一个基本缺失的部分,它将提供正确的方法来修复过度触发效果,而不会出现像跳过依赖项这样容易出错的 hack。 实际上,在引入 useEvent 之前,您可能很难编写某些效果,而不必忽略数组中的依赖关系或对所需行为做出妥协。 以 RFC 中的这个例子为例。 目标是在用户访问页面时记录分析:function Page({ route, currentUser }) { useEffect(() => { logAnalytics("visit_page", route.url, currentUser.name); }, [route.url, currentUser.name]); // ... } 当路由更改时,会记录一个带有路由 URL 和用户名的事件。 现在假设用户在"个人资料"页面上,她决定编辑她的姓名。 效果再次运行并记录一个新条目,这不是我们想要的。 使用 useEvent,您可以从效果中提取事件: function Page({ route, currentUser }) { // Stable identity const onVisit = useEvent(visitedUrl => { logAnalytics("visit_page", visitedUrl, currentUser.name); }); useEffect(() => { onVisit(route.url); }, [route.url]); // Re-runs only on route change // ... } 日志分析现在是为了响应由路由更改触发的事件而完成的。 事件处理程序 (onVisit) 是通过 useEvent 创建的,它返回一个稳定的函数。这意味着即使组件重新渲染,useEvent 返回的函数也将始终相同(相同的标识)。正因为如此,它不再需要作为依赖传递给 useEffect 您可以在 RFC 本身中阅读有关 useEvent 的其他示例和很酷的内容,例如在使用站点包装事件。但在我写这篇文章时,useEvent 仍在进行中。所以在它发布之前,人们仍然会想知道从他们的依赖数组中省略依赖是否安全...... ....或者他们可以开始使用 Dan Abramov 在新的 React 文档中发布的 shim 一个shim 诞生了 如果你真的没有关注"新闻",那么你可能错过了 React 团队今年一直忙于重写他们的文档网站的事实(如果你来自未来,这里是 2022 年)。 它仍处于测试阶段,但已经比旧版本好得多。我希望所有文档都和这个一样好。我一直提到丹·阿布拉莫夫的原因是他是主要作者(国王万岁)。 效果在这些新文档中有一个特殊的部分。可能是因为自 React 16.8 发布以来,包括我自己在内的人们一直在错误地使用它们(或过度使用它们)。或者可能是因为当人们注意到升级到 React 18 后,他们的 1,000 个效果开始在 StrictMode 下运行两次时,他们开始抱怨。 因此,毫不奇怪,您会在新文档中找到多达 5 页专门介绍效果的页面!您还将详细了解 useEvent 如何将您从依赖地狱中拯救出来。但当你开始接触它时,你会偶然发现以下陷阱之一: 幸运的是,在这个炎热的夏天的一个美好的一天,除了这个大的免责声明之外,没有太多解释,在示例和挑战中添加了一个 polyfill:import { useRef, useInsertionEffect, useCallback } from "react"; // The useEvent API has not yet been added to React, // so this is a temporary shim to make this sandbox work. // You"re not expected to write code like this yourself. export function useEvent(fn) { const ref = useRef(null); useInsertionEffect(() => { ref.current = fn; }, [fn]); return useCallback((...args) => { const f = ref.current; return f(...args); }, []); } 有趣的。 我不了解你,但我忍不住想看看 shim 里面的代码。 即使这只是一个临时的,我也不希望自己写! 你呢? 我是这么想的。 然后让我们从第 7 行开始,这里声明了 useEvent shim。 正如预期的那样,钩子在参数中接收一个名为 fn 的回调函数,就像 RFC 中的那样:export function useEvent(fn) { 接下来,使用 useRef 声明一个引用,该引用最初包含"null"值(第 8 行): const ref = useRef(null); 接下来是有趣的部分:参考 (ref) 是从效果中设置的,而不是在 fn 更改时运行(第 9-11 行): useInsertionEffect(() => { ref.current = fn; }, [fn]); 这不是任何一种效果:React 团队选择使用在 React 18 中引入的插入效果。 如果你不知道,React 中有几种效果:普通效果(由 useEffect 触发)、布局效果(useLayoutEffect)和插入效果(useInsertionEffect)。 它们中的每一个都在组件生命周期的不同阶段触发。 首先是插入效果(在应用 DOM 突变之前),然后是布局效果(在 DOM 更新之后),最后是普通效果(在组件完成渲染之后),import { useEffect, useInsertionEffect, useLayoutEffect } from "react"; import "./styles.css"; export default function App() { useEffect(() => console.log("useEffect"), []); useInsertionEffect(() => console.log("useInsertionEffect"), []); useLayoutEffect(() => console.log("useLayoutEffect")); const setRef = (e) => console.log("setRef"); return (Open the console to see in which order the various effects run{" "} ); } 您应该在控制台中看到以下输出: > useInsertionEffect > setRef > useLayoutEffect > useEffect 这与我们之前所说的一致。我们还可以看到,插入效果的触发时间与 React 设置引用的时间差不多,更重要的是,在布局效果之前。 RFC 中的详细设计规定: 在所有布局效果运行之前切换 [event] 处理程序的"当前"版本。这避免了用户态版本中存在的陷阱,即一个组件的效果可以观察到另一个组件状态的先前版本。不过,切换的确切时间是一个悬而未决的问题(在底部列出了其他悬而未决的问题)。 现在可以理解为什么将引用设置在插入效果而不是任何其他类型的效果上。在布局效果内或以后运行的代码期望调用更新的引用。所以需要先更新参考。 使用插入效果当然不是万无一失的。可以尝试在另一种插入效果中使用事件处理程序。在这种情况下,参考可能还不是最新的。这就是为什么 useEvent 不能在用户空间中安全实现的原因。 React 内部的未来实现将解决这个问题。 但让我们回到垫片。我再贴一次,这样更容易理解:import { useRef, useInsertionEffect, useCallback } from "react"; // The useEvent API has not yet been added to React, // so this is a temporary shim to make this sandbox work. // You"re not expected to write code like this yourself. export function useEvent(fn) { const ref = useRef(null); useInsertionEffect(() => { ref.current = fn; }, [fn]); return useCallback((...args) => { const f = ref.current; return f(...args); }, []); } 最后一部分涉及 useCallback 返回的函数(第 12-15 行): return useCallback((...args) => { const f = ref.current; return f(...args); }, []); 该回调没有任何依赖项 [](第 15 行),因此它只创建一次。因此,useCallback 总是返回相同的函数。正因为如此,垫片返回一个满足规范的稳定函数。 现在是回调本身。我们看到: 1.返回 useCallback((...args)=> { 它接受一个可变的参数列表(代码没有对处理程序接受的参数数量做出任何假设): 2.常量 f = 参考电流; 它访问 ref 的当前值,其中包含最新的 fn 函数(感谢效果行 10 中的代码): 3.返回 f(...args); 最后,它调用该函数,转发收到的参数 在这里,我们有一个始终保持最新的稳定事件处理程序!而且由于事件处理程序是稳定的,因此无论是否将其包含在效果的依赖数组中都没有关系:它永远不会导致效果再次自行运行。 但为什么它会起作用? 是的,我很确定每个人仍然不清楚为什么这确实有效。事件处理程序如何始终是"最新的"?最新,我不仅仅意味着它的引用是最新的,还意味着它可以在运行时访问"新"值。 让我们回到 RFC 中的示例:function Page({ route, currentUser }) { // Stable identity const onVisit = useEvent(visitedUrl => { logAnalytics("visit_page", visitedUrl, currentUser.name); }); useEffect(() => { onVisit(route.url); }, [route.url]); // Re-runs only on route change // ... } 为什么当效果调用 onVisit 时,currentUser.name 是最新的,即使我们没有在任何地方指定它作为依赖项? 好吧,每次组件渲染时,我们都会使用新的箭头函数visitedUrl => { ... }调用useEvent。该函数访问 currentUser.name,该名称在组件范围的上层定义。这就是我们所说的闭包。因此,该函数会在渲染组件时"捕获" currentUser.name 的值。 由于我们使用的是 React,我们知道组件会在其 props 更改时重新渲染。这就是为什么每次组件渲染时我们都有一个新的 up-date 函数,useEvent 负责将其存储在它的 ref 中。然后,每当调用事件处理程序 (onVisit) 时,代码都会调用存储在 ref 中的函数,该函数"捕获"组件中的最新值。 当您尝试用它们的值替换属性时,更容易理解: 第一次渲染 假设该组件是使用以下内容呈现的: Page({ route: { url: "/profile" }, currentUser: { name: "Dan" }, }); 当它发生时,您可以想象使用一个函数调用 useEvent,其中 currentUser.name 被 Dan 替换: const onVisit = useEvent(visitedUrl => { logAnalytics("visit_page", visitedUrl, "Dan"); }); 在这个表示中,visitedUrl => { logAnalytics("visit_page",visitedUrl,"Dan"); } 是存储在 useEvent 中的 ref 中的内容。 因此,当效果使用它所依赖的 route.url 调用 onVisit 时,实际上使用以下值调用 logAnalytics: logAnalytics("visit_page", "/profile", "Dan"); 第二次渲染 现在想象一下,丹将他的名字改为"瑞克"(对不起丹)。 React 使用以下命令重新渲染组件: Page({ route: { url: "/profile" }, currentUser: { name: "Rick" }, }); 再次调用 useEvent ,这次是用一个函数将 currentUser.name 替换为 Rick(更新后的值):const onVisit = useEvent(visitedUrl => { logAnalytics("visit_page", visitedUrl, "Rick"); }); useEvent 再次使用visitedUrl => { logAnalytics("visit_page",visitedUrl,"Rick"); }。 但是由于 route.url 没有改变,所以效果没有运行,因此 onVisit 也没有被调用。 不记录任何分析。 第三次渲染 然后,Dan Rick 导航到主页。 组件再次渲染:Page({ route: { url: "/home" }, currentUser: { name: "Rick" }, }); useEvent 再次调用了一个函数,其中 currentUser.name 被 Rick 替换:const onVisit = useEvent(visitedUrl => { logAnalytics("visit_page", visitedUrl, "Rick"); }); 尽管 currentUser.name 的值与之前("Rick")相同,但传递给 useEvent 的函数严格来说仍然是一个新函数。 它们是不同的实例,因此它们具有不同的身份(如果我们将该函数与前一个渲染中的函数进行比较,Object.is 将返回 false)。 所以 useEvent 再次更新它的 ref。 我们不在乎! 开销可以忽略不计。 最后,效果再次运行,因为它的依赖关系(route.url)发生了变化。 这意味着这次使用 /home 调用 onVisit ,然后调用 logAnalytics :logAnalytics("visit_page", "/home", "Rick"); 正如您所期望的那样! 附带说明一下,有趣的是,在此示例中,路由 URL 是作为参数传递给 onVisit 事件的,而不是直接在处理程序内部引用(就像我们对 currentUser.name 属性所做的那样)。 这是一个重要的区别,因为这意味着我们将在效果运行时记录路由 URL。 如果我们在事件处理程序中使用了 logAnalytics("visit_page", route.url, currentUser.name),我们将始终记录路由 URL 的最新值。 在这种特殊情况下,它没有太大区别,因为效果中的代码是同步的。 但如果 onVisit 已被调用以响应异步方法,则传递给函数的路由 URL 的值将是效果运行时的值,它可能不再是最新的 route.url。 如果您喜欢您阅读的内容,请随时关注我以获取更多信息! 关注七爪网,获取更多APP/小程序/网站源码资源!