当前位置: 首页 > Web前端 > JavaScript

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

时间:2023-03-27 12:01:37 JavaScript

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

)}打包工具后生成:chunk.js(包括A、B、App组件代码)首屏渲染,如果不需要组件B,可以将其代码拆分出来。只需进行以下修改://beforeimportBfrom'./B';//afterconstB=React.lazy(()=>import('./B'));打包工具打包后生成:chunk.js(包括A和App组件代码)b.js(包括B组件代码)这样??首屏渲染时会以jsonp的形式请求B组件代码,并将在请求返回后呈现。为了在B请求返回之前显示占位符,需要使用Suspense://之前,省略其余代码返回(
)//之后,省略其余代码return(}>
)
loading将在B请求返回之前呈现。.
作为占位符。可以看出Suspense的作用是:在异步内容返回前,显示占位符(fallback属性),返回后显示内容,再观察组件使用Suspense后返回的JSX结构,会发现一个非常强大的细节:return(
}>
)从这个JSX,它是完全看不出组件B是异步渲染的!同步和异步的区别是:同步:开始->结果异步:开始->中间状态->结果Suspense可以将其包裹的子组件的中间状态逻辑收敛到自己处理(即Suspense的fallback属性),因此子组件不需要区分同步和异步。那么,Suspense的能力是否可以从React.lazy(异步请求组件代码)扩展到所有的异步操作呢?答案是肯定的。资源大的是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可以用SuspenseLogic同步写入异步请求数据: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可能会被调用两次,即:第一次发送Request和returnpromise,第二次返回请求的数据,所以promise的值需要缓存在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)的执行时传入的keymap的值是resource.read(key)执行后返回的promise在我们的userResource例子中,createResource会在执行后创建一个map:constuserResource=createResource(fetchUser);userResource.read第一次执行后会在map中设置一条以userID为key,promise为value的数据(称为entry):constdata=userResource.read(userID);获取一个entry需要知道两件事:entrycorrespondencekeyentry所属的resourceLRU算法实现react-cache使用双向循环链表来实现LRU算法,包括insert、update、delete三种操作。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以后发展出多少新奇的东西,底层永远都是这些基本的算法和数据结构。它是如此简单和无聊......