当前位置:首页 > 科技  > 软件

有点东西啊!一个被小瞧的冷门Hook 补全了 React 19 异步优秀实践的最后一环

来源: 责编: 时间:2024-06-19 15:36:53 93观看
导读先预警一下,完全消化本文内容有点难。useDeferredValue 解决真实场景问题的案例。useDeferredValue 基础知识。复杂案例渲染过程分析。useDeferredValue 底层执行原理分析。重新分析取消请求案例。全文共 5104 字,阅读

ZtP28资讯网——每日最新资讯28at.com

先预警一下,完全消化本文内容有点难。ZtP28资讯网——每日最新资讯28at.com

  • useDeferredValue 解决真实场景问题的案例。
  • useDeferredValue 基础知识。
  • 复杂案例渲染过程分析。
  • useDeferredValue 底层执行原理分析。
  • 重新分析取消请求案例。

全文共 5104 字,阅读需要花费 10 分钟。ZtP28资讯网——每日最新资讯28at.com

useDeferredValue,一个出了很久,但是我几乎没咋在实践中用到过的超冷门 hook。它有多冷门呢,我之前甚至都觉得没必要介绍它。ZtP28资讯网——每日最新资讯28at.com

直到前几天,一个粉丝给了我重要的思路,我才认识到它的威力,逐渐深入了解之后发现它简直就是一个宝藏 hook,说它是为了 Suspense 量身订做的都不为过。ZtP28资讯网——每日最新资讯28at.com

ZtP28资讯网——每日最新资讯28at.com

此时,我使用 useTransition 勉强实现了该功能。主要代码如下:ZtP28资讯网——每日最新资讯28at.com

export default function Index() {  const [api, setApi] = useState(postApi)  const [isPending, startTransition] = useTransition()  function __inputChange() {    startTransition(() => {      api.cancel()      setApi(postApi())    })  }  ....
<Suspense fallback={<div>loading...</div>}>  <List api={api} isPending={isPending} /></Suspense>
const List = ({api, isPending}) => {  const posts = use(api)    return (    <ul className='_04_list' style={{opacity: isPending ? 0.5 : 1}}>      {posts.map((post) => (        <div key={post.id} className='_04_item'>          <h2>{post.title}</h2>          <p>{post.body}</p>        </div>      ))}    </ul>  )}

useTransition 能够阻止 Suspense 在请求发生时,渲染 fallback 中的 Loading 组件,并且,isPending 也能表示请求正在发生,因此,我把 isPending 传入到子组件中,那么我们就可以在子组件中自定义请求状态。ZtP28资讯网——每日最新资讯28at.com

这基本达到了我想要的交互效果。ZtP28资讯网——每日最新资讯28at.com

但是一个严重的问题是,我每次输入,都会发送一个请求,当我快速输入时,我希望通过取消上一次还没完成的请求的方式来优化交互效果。useTransition 并不支持我这样做。ZtP28资讯网——每日最新资讯28at.com

核心原因是因为 useTransition 的任务会排队依次执行,当我想要在下一个任务开始时,取消上一个请求时,上一个任务已经执行完了。因此 api.cancel() 虽然成功执行了,但是并起不到取消请求的效果,它执行时,已经没有未完成的请求了。ZtP28资讯网——每日最新资讯28at.com

useTransition 无法取消请求。我思考了很久,也没摸索出来一个合适的方案。因此之前我只能使用防抖来做这个优化。ZtP28资讯网——每日最新资讯28at.com

const [api, setApi] = useState(postApi)const [isPending, startTransition] = useTransition()const timer = useRef(null)function __inputChange() {  clearTimeout(timer.current)  timer.current = setTimeout(() => {    startTransition(() => {      api.cancel()      setApi(postApi())    })  }, 300)  }...

但是很显然,这不是很优雅,因为防抖实际上和 useTransition 有类似的作用,用了防抖之后,useTransition 在这里的存在就变得有点尴尬了。ZtP28资讯网——每日最新资讯28at.com

意外之喜的是,有大佬级别的粉丝在评论区给我提供了一个非常优雅的解决思路。那就是利用 useDeferredValue。ZtP28资讯网——每日最新资讯28at.com

ZtP28资讯网——每日最新资讯28at.com

肃然起敬!!!!ZtP28资讯网——每日最新资讯28at.com

在保证了代码优雅的情况之下,轻松实现了我理想中的效果。useDeferredValue 直接补齐了 React 19 异步开发中,最佳实践的最后一块短板!ZtP28资讯网——每日最新资讯28at.com

代码就这么几行,但是要理解 useDeferredValue,可能就要花点时间了。我们一起来学习一下。ZtP28资讯网——每日最新资讯28at.com

二、useDeferredValue 基础

useDeferredValue 是一个可以推迟 UI 更新的 hook。这句话理解起来有点困难。需要我稍微给各位道友解读一下。ZtP28资讯网——每日最新资讯28at.com

在正常情况下,一个 state 的变化,会导致 UI 发生变化。例如下面这个案例。ZtP28资讯网——每日最新资讯28at.com

function Index() {  const [counter, setCounter] = useState(0)  function __clickHanler() {    setCounter(counter + 1)  }  return (    <div>      <div id='tips'>基础案例,state 递增</div>      <button onClick={__clickHanler}>counter++</button>      <div className="counter">counter: {counter}</div>      <div className="counter">counter: {counter}</div>    </div>  )}

这里需要注意的是,状态 counter 被两个元素使用,因此,这两个元素的更改,实际上是一个任务。他们必定会同时响应 counter 的变化。ZtP28资讯网——每日最新资讯28at.com

但是这个时候,我们可以利用 useDeferredValue,把他们拆分成两个任务。ZtP28资讯网——每日最新资讯28at.com

function Index() {  const [counter, setCounter] = useState(0)  const deferred = useDeferredValue(counter)  function __clickHanler() {    setCounter(counter + 1)  }  return (    <div>      <div id='tips'>基础案例,state 递增</div>      <button onClick={__clickHanler}>counter++</button>      <div className="counter">        counter: {counter}      </div>      <div className="counter">        counter: {deferred}      </div>    </div>  )}

注意看,我们使用 counter 作为 useDeferredValue 的初始值,并将其返回值替换第二个元素。ZtP28资讯网——每日最新资讯28at.com

const deferred = useDeferredValue(counter)
<div className="counter">  counter: {deferred}</div>

此时,第二个元素的更新,就不再与第一个元素同步。它更新的优先级被降低。这个时候它的执行在理论上是可以被更高的优先级插队和中断的。ZtP28资讯网——每日最新资讯28at.com

但是由于渲染都太短了,我们肉眼无法区分出来两个任务已经被分开了,因此我们把第二个元素重构成一个子组件,并模拟成一个耗时组件。此时我们就能明显看出区别来。ZtP28资讯网——每日最新资讯28at.com

<Expensive counter={deferred} />
const Expensive = ({counter}) => {  const start = performance.now()  while (performance.now() - start < 200) {}  return (    <div className="counter">Deferred: {counter}</div>  )}

演示效果如下。ZtP28资讯网——每日最新资讯28at.com

ZtP28资讯网——每日最新资讯28at.com

因此,我们可以利用 useDeferredValue 推迟 UI 的更新。将对应任务的优先级降低,使其可以被插队与中断。ZtP28资讯网——每日最新资讯28at.com

三、复杂案例分析

在这里,我们要更加清楚的理解任务和渲染任务,才能对案例的分析更加的精准。以上一个例子的 Expensive 组件为例。ZtP28资讯网——每日最新资讯28at.com

状态变化时,diff 会发生,Expensive 函数本身作为 diff 过程的一部分,它必定也会执行,但是这里我们注意,它对应的渲染任务,却是可以被阻止执行的。ZtP28资讯网——每日最新资讯28at.com

例如在上面的例子中,当我快速点击按钮递增时,Expensive 组件不会依次递增。效果如下:ZtP28资讯网——每日最新资讯28at.com

ZtP28资讯网——每日最新资讯28at.com

我们发现,Expensive 组件的渲染直接从 0 变成了 7。ZtP28资讯网——每日最新资讯28at.com

这是因为作为一个耗时任务,又被标记了低优先级,因此它的渲染任务不停的被优先级更高的 counter 中断并放弃。因此直接从 0 变成了 7。ZtP28资讯网——每日最新资讯28at.com

但是此时我们也发现另外一个情况,那就是 counter 直接对应的高优先级执行也没有那么流畅,这是为什么呢?其实很简单,因为在我们的模拟案例中,并没有把耗时定位在渲染上。这可能和实践情况会不太一样。我们把耗时写在了 Expensive 函数里,而这个函数每次都会执行,它的执行阻塞了渲染。ZtP28资讯网——每日最新资讯28at.com

const Expensive = ({counter}) => {  const start = performance.now()  while (performance.now() - start < 200) {}  return (    <div className="counter">Deferred: {counter}</div>  )}

ZtP28资讯网——每日最新资讯28at.com

所以这里我们一定要区分开渲染任务和 Expensive 函数,他们是不同的,UI 渲染是一个异步任务,而 Expensive 函数是同步执行的。useDeferredValue 推迟的是 UI 渲染任务。因此,我们需要特别注意的是,不要在同步逻辑上执行过多的耗时任务。ZtP28资讯网——每日最新资讯28at.com

但是我们可以通过任务拆分的方式,把执行耗时时间分散到更多的子组件中去,这样 React 就可以利用任务中断的机制,在不阻塞渲染的情况下,中断低优先级的任务。ZtP28资讯网——每日最新资讯28at.com

借用官网的一个复杂案例来跟大家演示。ZtP28资讯网——每日最新资讯28at.com

function SlowList({ text }) {  // Log once. The actual slowdown is inside SlowItem.  console.log('[ARTIFICIALLY SLOW] Rendering 250 <SlowItem />');  let items = [];  for (let i = 0; i < 250; i++) {    items.push(<SlowItem key={i} text={text} />);  }  return (    <ul className="items">      {items}    </ul>  );}function SlowItem({ text }) {  let startTime = performance.now();  while (performance.now() - startTime < 1) {    // Do nothing for 1 ms per item to emulate extremely slow code  }  return (    <li className="item">      Text: {text}    </li>  )}

此时我们注意观察,不要错漏这个细节。slowList 中包含了 250 个子组件。每个子组件都渲染 1ms,那么整个组件渲染就需要耗时至少 250ms。ZtP28资讯网——每日最新资讯28at.com

在父组件中,我们把 deferred 传递给 SlowList。ZtP28资讯网——每日最新资讯28at.com

<SlowList text={deferred} />

那么此时表示,slowList 的任务是低优先级。counter 对应的任务可以中断它的执行。当我快速点击时,执行效果如下。ZtP28资讯网——每日最新资讯28at.com

ZtP28资讯网——每日最新资讯28at.com

此时一个很明显的区别就是,counter 的 UI 变化变得更加流畅了。这是因为耗时被拆分到了多个子组件中,React 就有机会中断这些函数的执行,并执行优先级更高的任务,以确保高优先级任务的流畅。ZtP28资讯网——每日最新资讯28at.com

如果你没有使用 React Compiler,你需要使用 memo 手动缓存 SlowList。ZtP28资讯网——每日最新资讯28at.com

const SlowList = memo(function SlowList({ text }) {  // ...});

useDefferdValue 会首先使用旧值传递给组件。ZtP28资讯网——每日最新资讯28at.com

<SlowList text={deferred} />

因此,当 counter 发生变化时,deferred 依然是旧值,那么此时,如果我们使用 memo 包裹,SlowList 的 props 就没有发生变化,我们可以跳过此次针对 SlowList 的更新。ZtP28资讯网——每日最新资讯28at.com

这跟 React 的性能优化策略有关。ZtP28资讯网——每日最新资讯28at.com

四、运行原理

看了上面两个例子,肯定还是有一部分人会觉得很懵,不要急,接下来我们把运行原理分析一下,整个情况就清晰了。ZtP28资讯网——每日最新资讯28at.com

useDeferredValue 会尝试将 UI 任务更新两次。ZtP28资讯网——每日最新资讯28at.com

第一次,会给子组件传递旧值。此时 SlowList 接收到的 props 会与上一次完全相同。如果结合了 React.memo,那么该组件就不会重新渲染。该组件可以重复使用之前的渲染结果。ZtP28资讯网——每日最新资讯28at.com

ZtP28资讯网——每日最新资讯28at.com

Compiler 编译之后不需要 memo。ZtP28资讯网——每日最新资讯28at.com

此时,高优先级的任务渲染会发生,渲染完成之后,将会开始第二次渲染。此时,将会传入刚才更新之后的新值。对于 SlowList 而言,props 发生了变化,整个组件会重新渲染。ZtP28资讯网——每日最新资讯28at.com

我们通常会将已经非常明确的耗时任务标记为 deferred,因此,这些任务都被视为低优先级。当重要的高优先级更新已经完成,低优先级任务在第二次渲染时尝试更新...ZtP28资讯网——每日最新资讯28at.com

在它第二次更新的过程中,如果又有新的高优先级任务进来,那么 React 就会中断并放弃第二次更新,去执行高优先级的任务。ZtP28资讯网——每日最新资讯28at.com

iZtP28资讯网——每日最新资讯28at.com

注意:是中断,并放弃这次更新,所以表现出来的结果就是,中间会漏掉许多任务的执行。ZtP28资讯网——每日最新资讯28at.com

这样的运行机制有一个非常重要的好处。ZtP28资讯网——每日最新资讯28at.com

那就是,如果你的电脑性能足够强悍,那么第二次的更新可能会快速完成,高优先级的任务来不及中断,那么我们的页面响应就是非常理想的。ZtP28资讯网——每日最新资讯28at.com

但是如果我们的电脑性能比较差,第二次更新还没完成,新的高优先级任务又来了,那么就可以通过中断的方式,降级处理,保证重要 UI 的流畅,放弃低优先级任务。ZtP28资讯网——每日最新资讯28at.com

ZtP28资讯网——每日最新资讯28at.com

在不同性能的设备上,有不同的反应,这个是跟防抖、节流的最重要的区别。ZtP28资讯网——每日最新资讯28at.com

五、重新分析取消请求案例

那我们回过头来,分析一下最开始的那个案例,重新看一眼代码ZtP28资讯网——每日最新资讯28at.com

export default function Index() {  const [api, setApi] = useState(postApi)  const deferred = useDeferredValue(api)  function __inputChange(e) {    api.cancel()    setApi(postApi())  }  ...
<Suspense fallback={<div>loading...</div>}>  <List api={deferred} isPending={api !== deferred} /></Suspense>

这里我们将 api 做为 state,当 api 被重新赋值时,List 会经历两次更新。ZtP28资讯网——每日最新资讯28at.com

首先点击事件触发,请求立即发生。api 被改变。触发组件更新。ZtP28资讯网——每日最新资讯28at.com

第一次更新时,deferred 使用旧值传参,此时对于 List 而言,api 没有发生变化。因此,利用这个机制,我们可以阻止 Suspense 直接渲染成 fallback。ZtP28资讯网——每日最新资讯28at.com

在 Suspense 包裹之下,只有当接口请求成功之后,deferred 的第二次更新才会发生,因此,在这个过程中,如果我们快速进行第二次点击,可以直接取消上一次请求,让第二次更新来不及执行。此时新的请求发生。ZtP28资讯网——每日最新资讯28at.com

ZtP28资讯网——每日最新资讯28at.com

这里要结合 Suspense 的执行机制来理解。ZtP28资讯网——每日最新资讯28at.com

六、总结

这种场景的最佳实践代码非常的简洁和优雅。写起来也很舒服,性能也非常强悍。但是理解起来会比较困难。因此想要做到灵活运用,还需要多多消化。ZtP28资讯网——每日最新资讯28at.com

但是,等你彻底掌握它之后,你就会发现 React 19 在异步交互上真的太优雅了。这样的开发体验,是依赖 useEffect 完全比不了的。ZtP28资讯网——每日最新资讯28at.com

后续的分享中,我将会继续为大家分享 React Action 的设计核心思维与具体使用。ZtP28资讯网——每日最新资讯28at.com

本文链接:http://www.28at.com/showinfo-26-94859-0.html有点东西啊!一个被小瞧的冷门Hook 补全了 React 19 异步优秀实践的最后一环

声明:本网页内容旨在传播知识,若有侵权等问题请及时与本网联系,我们将在第一时间删除处理。邮件:2376512515@qq.com

上一篇: Python编程新境界,代码逻辑分离指南!

下一篇: 百度二面,有点小激动!附面试题

标签:
  • 热门焦点
  • 帅气纯真少年!日本最帅初中生选美冠军出炉

    帅气纯真少年!日本最帅初中生选美冠军出炉

    日本第一帅哥初一生选美大赛冠军现已正式出炉,冠军是来自千叶县的宗田悠良。日本一直热衷于各种选美大赛,从&ldquo;最美JK&rdquo;起到&ldquo;最美女星&r
  • 跑分安卓第一!Redmi K60至尊版8月发布!卢伟冰:目标年度性能之王

    跑分安卓第一!Redmi K60至尊版8月发布!卢伟冰:目标年度性能之王

    8月5日消息,Redmi K60至尊版将于8月发布,在此前举行的战略发布会上,官方该机将搭载搭载天玑9200+处理器,安兔兔V10跑分超177万分,是目前安卓阵营最高的分数
  • 十个可以手动编写的 JavaScript 数组 API

    十个可以手动编写的 JavaScript 数组 API

    JavaScript 中有很多API,使用得当,会很方便,省力不少。 你知道它的原理吗? 今天这篇文章,我们将对它们进行一次小总结。现在开始吧。1.forEach()forEach()用于遍历数组接收一参
  • 一篇聊聊Go错误封装机制

    一篇聊聊Go错误封装机制

    %w 是用于错误包装(Error Wrapping)的格式化动词。它是用于 fmt.Errorf 和 fmt.Sprintf 函数中的一个特殊格式化动词,用于将一个错误(或其他可打印的值)包装在一个新的错误中。使
  • JavaScript学习 -AES加密算法

    JavaScript学习 -AES加密算法

    引言在当今数字化时代,前端应用程序扮演着重要角色,用户的敏感数据经常在前端进行加密和解密操作。然而,这样的操作在网络传输和存储中可能会受到恶意攻击的威胁。为了确保数据
  • 小红书1周涨粉49W+,我总结了小白可以用的N条涨粉笔记

    小红书1周涨粉49W+,我总结了小白可以用的N条涨粉笔记

    作者:黄河懂运营一条性教育视频,被54万人&ldquo;珍藏&rdquo;是什么体验?最近,情感博主@公主是用鲜花做的,火了!仅仅凭借一条视频,光小红书就有超过128万人,为她疯狂点赞!更疯狂的是,这
  • 自律,给不了Keep自由!

    自律,给不了Keep自由!

    来源 | 互联网品牌官作者 | 李大为编排 | 又耳 审核 | 谷晓辉自律能不能给用户自由暂时不好说,但大概率不能给Keep自由。近日,全球最大的在线健身平台Keep正式登陆港交所,努力
  • 新电商三兄弟,“抖快红”成团!

    新电商三兄弟,“抖快红”成团!

    来源:价值研究所作 者:Hernanderz 随着内容电商的概念兴起,抖音、快手、小红书组成的&ldquo;新电商三兄弟&rdquo;成为业内一股不可忽视的势力,给阿里、京东、拼多多带去了巨大压
  • 苹果MacBook Pro 2021测试:仍不支持平滑滚动

    苹果MacBook Pro 2021测试:仍不支持平滑滚动

    据10月30日9to5 Mac 消息报道,苹果新的 14 英寸和 16 英寸 MacBook Pro 2021 上市后获得了不错的评价,亮点包括行业领先的性能,令人印象深刻的电池续航,精美丰
Top