本篇文章,以模拟从『Hacker News API[1]』获取热门文章为例,通过一步步地代码优化和封装,阐述 React
异步获取数据的进化历程(useEffect
-> useReducer
-> custom hook
-> Suspense
)。
虽然 React 16.6 新增了 Suspense
组件用于异步获取数据,但我们相信,截止到 react 18 新特性发布之前,异步获取数据最常见的方法仍是使用 useEffect
,我们称这种方法为“fetch-on-render”(渲染之后获取数据),因为数据的获取是发生在组件被渲染到屏幕之后。
本小节内容将以实现关键词搜索热门文章的需求为例,阐述通过 React Hook 异步获取数据的实践过程。
首先,我们使用useState
创建data
来管理数据的本地状态,当组件第一次渲染时,从useState()
拿到data
的初始值[],当我们调用setData(val)
,React 会再次渲染组件,这一次data
的值为val
。
我们在useEffect
这个钩子中使用axios
[2]来异步请求数据,并将返回的结果存储到data
中。但是,当您运行这个程序时,您会发现,useEffect
在组件挂载时触发,在组件更新时也会频繁触发,导致一次又一次的请求相同的数据。我们只想在组件挂载时请求一次数据,因此我们给useEffect
的传递一个空数组([])作为第二个参数。这就告诉 React 你的 effect 不依赖于 props
或 state
中的任何值,所以它永远都不需要重复执行。
这时,已经能够正常获取数据了,但是useEffect
会报以下错误:Effect callbacks are synchronous to prevent race conditions
。问题的原因是请求结果返回的顺序不能保证一致,比如我将App
这个组件做为子组件,当接收关键词不同时,会重新渲染组件,那么先请求关键词为react
的文章,然后请求关键词为vue
的文章,但关键词为vue
的请求更先返回。请求更早但返回更晚的情况会错误地覆盖状态值data
(此时关键词是vue
,但显示的是后返回react
的结果),这就叫做竞态(race conditions
),具体可以点此查看**示例代码**[3]。
如果你对“竞态”问题还很迷惑,那一定是因为你对useEffect
了解得不够透彻。请先记住这句话,effects 会在每次渲染后运行,并且概念上它是组件输出的一部分,可以“看到”属于某次特定渲染的 props 和 state。 仍然以刚才那个例子为例,假设第一次渲染的时候search
(存储关键词的state
)是react
,第二次渲染的时候是search
(存储关键词的state
)是vue
。你可能会认为发生了下面的这些事:
react
的 UI。react
的 effect。 - 从这之后开始搜索关键词vue
react
的 effect。vue
的 UI。vue
的 effect。你可能会认为清除过程“看到”的是旧的 props 因为它是在重新渲染之前运行的,新的 effect“看到”的是新的 props 因为它是在重新渲染之后运行的。其实这个认知时错误的,React 只会在浏览器绘制后运行 effects。这使得你的应用更流畅因为大多数 effects 并不会阻塞屏幕的更新。Effect 的清除同样被延迟了。上一次的 effect 会在重新渲染后被清除,真正的执行顺序如下:
react
的 UI。react
的 effect。 - 从这之后开始搜索关键词vue
vue
的 UI。react
的 effect。vue
的 effect。你可能会好奇:如果清除上一次的 effect 发生在search
变成vue
之后,那它为什么还能“看到”旧的react
?这是因为组件内的每一个函数(包括事件处理函数,effects,定时器或者 API 调用等等)会捕获定义它们的那次渲染中的props
和state
。 这正是为什么 React 能做到在绘制后立即处理 effects — 并且默认情况下使你的应用运行更流畅。但这么处理有利有弊,就导致了异步情况引发的“竞态”问题。
useEffect
并没有解决这个问题,而是通过提示警告你不要使用异步函数。当然,可以对useEffect
做如下改造:
如上图所示,将异步请求函数封装在useEffect
中,同时使用didCancel
这个布尔值来让我们的数据获取逻辑知道组件的状态(安装/卸载)。如果组件卸载/更新了,则应设置该标志为true
,以防止组件卸载/更新后异步请求结果返回设置组件状态导致出现竞态的问题。以刚才那个例子为例,先请求关键词为react
的文章,在请求结果还没返回的时候,关键词变更为vue
,组件即将重新渲染,组件执行清除函数,将didCancel
置为true
,此时,react
请求的结果返回,并不会执行setData
,从而避免竞态的问题,可点击查看**示例代码**[4]。
接下来,我们增加关键词搜索功能,添加input
供用户任意输入,添加button
确认用户要检索的关键词,并设置query
和search
两个状态值,同时对请求 Url 拼接上查询参数。由于请求依赖search
这个状态值,所以在useEffect
的依赖数组中,诚实地添加上search
(不添加只会在组件挂载时执行一次),这样,如果search
发生改变,就会重新请求数据。
至此,我们已经实现了关键词搜索热门文章的功能,可在此沙箱[5]中查看代码示例。
从使用 UseEffect 异步获取数据[6]示例中能明显感受到,当用户搜索新的关键词直至响应时,查询结果的展示有明显的“滞后”效果,为了提升用户体验,一般都会加个加载中的状态提示。如下图代码所示,添加isLoading
状态值,请求发起时将isLoading
设为true
,接收到请求结果后将isLoading
设置为false
。UI 展示通过二元组进行判断,当isLoading
设为true
时,展示加载中动画,相反,则展示请求结果。
网络环境错综复杂,当一个请求发起时,可能会遇到丢包、超时、服务器宕机、请求出错等各种情况,对于一个高鲁棒性的组件而言,添加错误处理机制是必不可少的。如下图代码所示,添加isError
状态值,请求发起时将isError
设为false
,请求结果出错被catch
捕获则将isError
设置为true
。UI 展示通过三元组进行判断,当isError
设为true
时,展示错误提示,否则如果当isLoading
设为true
时,展示加载中动画,相反,则展示请求结果。
至此,我们已经实现了异步获取数据的各种状态处理方案,你可在此沙箱[7]查看示例代码。发送请求时到接收请求结果期间,会有一个加载动画。当输入script
等不安全的关键词,会报错。
在上面的 Demo 中,我们使用data
、isLoading
、isError
三个状态值来标记页面的展示状态(通过三元组判断逻辑),在数据请求加载中、成功、失败等不同状态下都会进行一系列的setState
,降低可读性的同时,useEffect
重新执行时机变得更加难以预料。这时,可以使用useReducer
进行解耦,组件层面只需要发出action
,无需知道如何更新状态(reducer
负责更新状态)。另一方面,useEffect
再也不需要依赖状态值,而是依赖dispatch
,能够避免useEffect
、useMemo
、useCallback
需要频繁重执行的问题,同时也提高了性能。
如下图所示,使用useReducer
返回一个状态对象state
和一个改变状态对象的函数dispatch
,通过dispatch
发出action
去改变状态值,组件层面是无需关心状态值如何改变,这一切都封装在dataFetchReducer
函数中。第 11 行dispatch({ type: "LOADING" })
表示将状态对象中的isLoading
设置为加载中,第 17 行dispatch({ type: "SUCCESS", payload: result.data.hits })
表示将状态对象中的data
设置为请求获取得到的文章列表数据,第 20 行dispatch({ type: "FAIL" })
表示将状态对象中的isError
设置为状态错误。
接下来,我们关注dataFetchReducer
函数的实现,type
不同时,进行对应的操作。具体可以点击此沙箱[8]查看代码示例。
useAsyncFn
Hook至此,我们已经使用useReducer
进行解耦,将状态管理从组件中分离出来,组件层面只需要通过dispatch
发出action
去改变状态值,从某种程度上而言,已经具有一定的可复用性。但是,异步获取数据是前后端通信的“桥梁”,几乎所有的业务模块都会使用,尽管每个业务模块都可以复用同一套dataFetchReducer
方法,但仍然需要在数据的获取过程中通过dispatch
发出不同的action
去改变状态值。其实对于业务开发人员而言,仅需要关心向哪个 URL 发起请求,已经请求得到的结果是什么,并不需要关心状态值是如何变化的。
因此,我们可以封装一个自定义 hook 来实现这个功能,这里将这个 hook 命名为useAsyncFn
。思考一下,useAsyncFn
这个 hook 的入参和出参该如何设计?首先明确该 hook 的目的是实现异步获取数据过程中的状态管理,也就是对dispatch
发出action
去改变状态值这些操作进行二次封装。那么入参必然有异步函数Fn
,异步函数Fn
所依赖的数组deps
,以及初始状态值initialState
。出参则是状态对象state
,以及对Fn
进行状态处理后的回调函数callback
。
useAsyncFn
函数的入参和出参如上所示,接下来讲述每个参数的类型定义,首先入参异步函数Fn
是返回Promise
对象的函数,可定义为type FunctionReturningPromise = (...args: any[]) = Promiseany;
,由于出参callback
的类型依赖异步函数Fn
,所以可以将其使用泛型T
代替。异步函数Fn
所依赖的数组deps
类型同 hook 中的依赖数组一样,使用react
暴露的DependencyList
定义。接下来是初始状态值initialState
,通过前面章节的描述,它应该包含有data
、isLoading
、error
三个状态值,类型定义为AsyncState
,由于data
这个状态值的类型依赖异步函数Fn
返回的结果,所以可用泛型A
表示,AsyncState
类型存在如下图所示四种状态,第一种状态是初始化,isLoading
的值未知;第二种状态时加载中;第三种状态是返回异常结果,此时isLoading
值为false
,error
也有对应的赋值;第四种状态是返回成功结果,此时isLoading
值为false
,data
也有对应的赋值;由于泛型A
需要有明确的定义,所以将AsyncState
二次封装成StateFromFunctionReturningPromise
,具体定义可参照下图:
入参和出参讨论完毕,接下来我们讲述useAsyncFn
函数的内部实现,如下图所示: 定义一个state
(useState
hook)来管理状态,定义callback
(useCallback
hook)作为出参供外部调用,该callBack
是否执行,依赖数组deps
。callback
函数就是对状态管理的二次封装,第 10 行执行setState
将状态设置为加载中,第 11 行执行异步函数Fn
,并等待返回结果。如果返回成功结果,那么跳转到第 13 行执行setState
将取消加载中状态并将返回结果赋值给data
。如果返回异常结果,那么跳转到第 17 行执行setState
将取消加载中状态并将抛出的异常赋值给error
。
至此,你是否感觉到好像缺了点什么?没错,就是前面章节提到的竞态问题,解决方法就是设置一个布尔值判断组件是否挂载,等组件卸载/更新后才返回的结果无效,不更改状态。该方法其实也可以封装成一个自定义 hook,这里将其命名为useMountedState
。如下图所示,使用useRef
定义mountedRef
实时追踪组件的状态,在useEffect
执行时,将值设置为true
,表示当前组件已经挂载。在组件卸载/更新时,将值设置为false
。同时在第三行使用useCallback
定义一个回调函数可以实时获得mountedRef
的值,并将该回调函数返回供外部使用。
接下来讲一下useMountedState
如何解决组件(挂载/卸载)引起的异步获取数据的竞态问题。如下图所示,使用useMountedState
定义一个“看不见”的状态值实时追踪组件的状态,并将回调函数返回给变量isMounted
,执行isMonted
函数可以判断当前组件是否还处于挂载中,只有组件仍然处于挂载中,才会对error
或者data
赋值(分别对应 14、19 行代码)。
处理完上面的竞态问题,你是否觉得万事无忧了。那么我想请你思考一下,当你反复调用对外暴露的callback
函数时,会发生什么?没错,还是竞态问题。
如何解决这种竞态问题呢?这就需要对业务形态有一个正确的认知,当反复调用对外暴露的callback
函数时,其实用户想要获得的只是最新的一次结果。基于这个认知的前提,你可以使用useRef
定义一个变量lastCallId
实时追踪当前调用函数的id
,每调用一次,就累加一。只有最新一次调用函数才能对error
或者data
赋值(分别对应 16、21 行代码)。
useAsyncFn
Hook 应用自此,我们已经实现useAsyncFn
这个自定义 hook,接下来我们用它来改造我们之前的 Demo,如下图所示,这里不做赘述。
上一小节,我们已经通过封装自定义 Hook 将数据获取和组件渲染进行解耦,但仍然存在一些不可避免的问题:
1 . 丑陋的三元表达式:如下图所示,加载和错误状态是通过渲染中的三元组定义的,从而使代码不必要地复杂化。我们不是描述了一个渲染函数,我们描述了三个。
2 . Fetch-on-render
导致的瀑布问题:先开始渲染组件,每个完成渲染的组件都可能在它们的 effects 或者生命周期函数中获取数据,拿到数据后对状态进行赋值,触发更新,就经常导致“瀑布”问题。
这时,React 16.6 新增的 Suspense
组件派上用场了,让你可以“等待”任何内容(图像、脚本或者其他异步的操作)加载,并且可以直接指定一个加载的界面。
通常的想法是,Suspense
允许组件“悬停”它们的渲染。例如,如果需要从外部来源加载额外数据,一旦所有依赖的资源(数据或资源文件)都存在了,React 将重新尝试渲染组件。
为了实现上面描述的功能,React 使用Promises
。组件可以在其render
方法中抛出Promise
,React 捕获抛出的Promise
并在组件树上查找最接近的Suspense
组件,它充当一种边界。Suspense
组件接受一个组件作为fallback
属性,当其子树中的任何子项被挂起时,都会呈现该元素。
React 还会跟踪抛出的Promise
。一旦Promise
被resolve
了,就会再次渲染组件。这假定由于Promise
被resolve
,被"悬停"的组件现在已经获取了能够正确渲染所需的所有信息。为此,我们使用某种形式的缓存来存储数据,在每次渲染时,我们通过这个缓存来确定数据是否已经可用(然后它只是从变量中读取它), 在这种情况下它会触发fetch
,并抛出Promise
的结果来让 React 捕获。
Suspense
的核心概念与错误边界[9]非常相似,错误边界在 React 16 中引入,允许在应用程序内的任何位置捕获未捕获的异常,然后在组件树中展示跟错误信息相关的组件。以同样的方式,Suspense
组件从其子节点捕获任何抛出的Promises
,不同之处在于对于Suspense
我们不必使自定义组件充当边界,Suspense
组件就是那个边界!
我们知道,无论是什么异常,JavaScript
都能捕获,React 就是利用了这个语言特性,捕获了所有生命周期函数,render
函数,以及事件回调中的任何错误,封装进一个特殊的生命周期里:ComponentDidCatch
。实际上,Suspense
就是依赖ComponentDidCatch
实现的。
如下图所示,是一个简单版本的Suspense
实现,通过ComponentDidCatch
捕获异常,只不过这个异常是个Promise
,当Promise
处于pending
状态时,渲染fallback
组件,当Promise
处于resolved
状态时,渲染children
组件。
如下图所示,业务组件无需关心数据的获取情况,将其封装在fetchData
中,第 5 行定义一个resource
存储这个支持Suspense
的特殊对象。第 6-8 行当搜索关键词发生变化时,重新触发fetchData
,并对resource
赋值。第 19-28 行对文章列表组件ArticleList
进行封装,第 21 行已经在尝试加载文章列表信息,但是此时可能请求还未加载完毕。如果未加载完毕就会跑出一个Ptomise
被包裹的Suspense
组件捕获,而渲染fallback
传入的组件。React 还会跟踪抛出的Promise
。一旦Promise
被resolve
了,就会渲染组件ArticleList
组件。
接下来,我们谈谈fetchData
的具体实现,第 1-11 行的fetchArticleData
函数发起请求,并返回一个Promise
对象。第 13-18 行的fetchData
是所有请求的封装(假设业务功能丰富,还有其他请求),第 11 行同步执行fetchArticleData
函数,articlePromise
的值是这个Promise
对象,然后通过wrapPromise
函数进行封装,将其返回给业务层。
wrapPromise
函数的实现如下图所示,接收Promise
对象作为参数,由于我们是同步执行fetchArticleData
函数,所以当前的状态必为pending
,第 4 行设置suspender
存储Promise
对象执行后扭转对应的状态。最后返回一个read
方法,当状态为pending
时,抛出这个suspender
对象给业务层,这个会被Suspense
组件的ComponentDidCatch
生命周期捕获,并渲染渲染fallback
传入的组件。当状态为error
时,抛出异常,Suspense
组件并不处理这个异常,而是继续向外抛出。当状态为success
时,返回结果给业务层进行渲染。
每当使用 Promises
,大概率我们会用 catch()
来做错误处理。但当我们用 Suspense
时,我们不等待 Promises
就直接开始渲染,这时 catch()
就不适用了。这种情况下,错误处理该怎么进行呢?
在 Suspense
中,获取数据时抛出的错误和组件渲染时的报错处理方式一样——你可以在需要的层级渲染一个错误边界[10]组件来“捕捉”层级下面的所有的报错信息。如下图所示,定义ErrorBoundary
这个类,当抛出错误后,使用 static getDerivedStateFromError()
更新state
用户判断渲染备用 UI ,使用 componentDidCatch()
打印错误信息。第 11-20 行,当state
中的hasError
为true
时,渲染备用 UI,否则渲染children
组件。
你可以在此沙箱[11]中查看示例代码。
从长远来看,Suspense
是为数据请求而生的,会允许第三方库通过一些方式告诉 React 暂停渲染直到某些异步事物(任何东西:代码,数据,图片)准备就绪。根据 React 18 发布预告,将包括对 React 服务器端渲染 (SSR) 性能的架构改进,这里的优化思路就用到Suspense
。当Suspense
逐渐地覆盖到更多的数据请求使用场景,我觉得useEffect
会退居幕后作为一个强大的工具,用于同步props
和state
到某些副作用。不过在那之前,自定义的 Hooks 比如这儿提到的是复用数据请求逻辑很好的方式。
本文由哈喽比特于3年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/rXhNNDzj6CtZi6ILmHx56Q
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。