当前位置: 首页 > 科技观察

从概念到实现LRU算法,未来的React异步开发方式

时间:2023-03-12 20:32:43 科技观察

大家好,我是Kason。React源代码在实现不同的模块时使用了多种算法和数据结构(例如,调度程序使用一个小的顶堆)。今天我们就来聊聊数据缓存相关的LRU算法。内容包括四个方面:介绍一个React的特性这个特性和LRU算法的关系LRU算法的原理React中LRU的实现可以说是从入门到实现都涵盖了,所以内容非常多,建议喜欢的慢慢收藏。一切的起点:Suspense在React16.6中引入了Suspense和React.lazy来拆分组件代码。对于如下代码:importAfrom'./A';importBfrom'./B';functionApp(){return(

>
)}被打包工具打包后,生成:块。js(包括A、B、App组件代码)首屏渲染,如果不需要组件B,可以拆分出它的代码。只需进行以下修改://beforeimportBfrom'./B';//afterconstB=React.lazy(()=>import('./B'));被打包工具打包后生成:chunk.js(包含A,App组件代码)b.js(包含B组件代码)这样首屏时会以jsonp的形式请求B组件代码渲染,然后在请求返回后渲染。为了在B请求返回前显示占位符,需要使用Suspense://before,省略其余代码return(
)//after,省略其余代码在B请求返回之前呈现。.
作为占位符。可以看出Suspense的作用是:在异步内容返回前,显示占位符(fallback属性),返回后显示内容,再观察组件使用Suspense后返回的JSX结构,会发现一个非常强大的细节:return(}>
)从这个JSX,完全不可能看出组件B是异步渲染的!同步和异步的区别是:同步:开始->结果异步:开始->中间状态->结果Suspense可以将其包裹的子组件的中间状态逻辑收敛到自身forprocessing(即Suspense的fallback属性),所以子组件不需要区分同步和异步。那么,Suspense的能力是否可以从React.lazy(异步请求组件代码)扩展到所有的异步操作呢?答案是肯定的。resource最大的一点就是React仓库是一个monorepo,里面包含了多个库(比如react,react-dom),其中有一个结合了Suspense的缓存库——react-cache,让我们看看它的用处。假设我们有一个fetchUser方法来请求用户数据:constfetchUser=(id)=>{returnfetch(`xxx/user/${id}`).then(res=>res.json())};通过react-cache的createResource方法封装,他就变成了一个resource(资源):import{unstable_createResourceascreateResource}from'react-cache';constuserResource=createResource(fetchUser);resource可以用Suspense同步写异步请求数据的逻辑:functionUser({userID}){constdata=userResource.read(userID);return(

name:{data.name}

age:{data.age}

)}可以看到,userResource.read是完全同步的,内部会调用fetchUser。其背后的逻辑是:第一次调用userResource.read会创建一个promise(即fetchUser的返回值)throwpromiseReact内部的catchpromise后,最接近User组件祖先的Suspense组件渲染fallbackpromiseresolve,此时User组件重新渲染再次调用userResource.read会返回resolve的结果(即fetchUser请求的数据),并使用这个数据继续渲染。从步骤1和步骤5可以看出,对于一个请求,userResource.read可能会被调用两次,即:第一次发送请求,第二次返回promise并返回请求的数据,所以promise的value需要缓存在userResource里面,缓存的key是userID:constdata=userResource.read(userID);由于userID是User组件的props,当User组件接收到不同的userID时,userResource需要缓存不同userID对应的promise。如果切换了100个userID,将缓存100个promises。显然我们需要一个缓存清理算法,否则缓存会越占越多,直到溢出。react-cache使用的缓存清理算法是LRU算法。LRU原理LRU(Leastrecentlyused,最近最少使用)算法的核心思想是:如果数据最近被访问过,那么以后被访问的概率也更高。因此,越频繁使用的数据,权重越高。当需要清理数据时,始终清理最不常用的数据。react-cache中LRU的实现react-cache的实现包括两部分:数据访问LRU算法实现数据访问createResource创建的每个资源都有对应的map,其中:map的key是resource.read(key)的key执行时传入的map的值是resource.read(key)执行后返回的promise。在我们的userResource示例中,将在执行createResource后创建映射:constuserResource=createResource(fetchUser);userResource.read第一次执行后会在map中设置一条以userID为key,promise为value的数据(称为entry):constdata=userResource.read(userID);获取一个entry需要知道两件事:entry对应的keyentry所属的resourceLRU算法react-cache使用“双向循环链表”实现LRU算法,包括三个操作:insert、更新和删除。insert操作第一次执行userResource.read(userID)得到entry0(简称n0),会和自己形成一个循环链表:此时first(代表权重最高)指向n0。改变userIDprops后,执行userResource.read(userID)得到entry1(简称n1):此时n0和n1组成一个循环链表,先指向n1。如果再插入n2,会是这样:可以看出,每当有新的条目加入时,first总是指向他,隐含了LRU中权重总是高的新思想。更新操作每当一个条目被访问时,它的权重被更新为最高的,因为它被使用了。对于后面的n0n1n2,其中n2的权重最高(先指向他):当再次访问n1时,即调用如下函数时:userResource.read(n1对应userID);n1将被赋予最高的权重:当缓存数量超过设置上限时,react-cache将清除权重较低的缓存。对于后面的n0n1n2,其中n2的权重最高(first指向他):如果最大缓存限制为1(即只缓存一个条目),first.previous会迭代清理,直到缓存数为1,即先清理n0:再清理n1:每次清理后,map中对应的entry也会被删除。完整的LRU实现见react-cacheLRUsummary。除了React.lazy和react-cache可以和Suspense结合之外,只要发挥你的想象力,任何异步流程都可以收敛到Suspense,比如ReactServerCompontnt和流式SSR。随着年底React18底层的稳定,相信这种同步开发模式在未来会逐渐成为主流。无论React以后发展出多少新奇的东西,底层永远都是这些基本的算法和数据结构。它是如此简单和无聊......