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