从理念到LRU算法实现,起底未来React异步开发方式
欢迎加入人类高质量前端框架研究群,带飞
大家好,我卡颂。
React
源码内部在实现不同模块时用到了多种算法与数据机构(比如调度器使用了小顶堆)。
今天要聊的是数据缓存相关的LRU
算法。内容包含四方面:
- 介绍一个
React
特性 - 这个特性和
LRU
算法的关系 LRU
算法的原理React
中LRU
的实现
文章图片
一切的起点:Suspense 在
React
16.6引入了Suspense
和React.lazy
,用来分割组件代码。对于如下代码:
import A from './A';
import B from './B';
function App() {
return ()
}
经由打包工具打包后生成:
- chunk.js(包含
A、B、App
组件代码)
B
组件不是必需的,可以将其代码分割出去。只需要做如下修改:// 之前
import B from './B';
// 之后
const B = React.lazy(() => import('./B'));
经由打包工具打包后生成:
- chunk.js(包含
A、App
组件代码) - b.js(包含
B
组件代码)
B
组件代码会在首屏渲染时以jsonp
的形式被请求,请求返回后再渲染。为了在
B
请求返回之前显示占位符,需要使用Suspense
:// 之前,省略其余代码
return ()
// 之后,省略其余代码
return ()
B
请求返回前会渲染loading.。.
作为占位符。可见,
Suspense
的作用是:在异步内容返回前,显示占位符(fallback属性),返回后显示内容再观察下使用
Suspense
后组件返回的JSX
结构,会发现一个很厉害的细节:return ()
从这段
JSX
中完全看不出组件B
是异步渲染的!同步和异步的区别在于:
- 同步:开始 -> 结果
- 异步:开始 -> 中间态 -> 结果
Suspense
可以将包裹在其中的子组件的中间态逻辑收敛到自己身上来处理(即Suspense
的fallback
属性),所以子组件不需要区分同步、异步。文章图片
那么,能不能将
Suspense
的能力从React.lazy
(异步请求组件代码)推广到所有异步操作呢?答案是可以的。
resource的大作为
React
仓库是个monorepo
,包含多个库(比如react
、react-dom
),其中有个和Suspense
结合的缓存库 —— react-cache
,让我们看看他的用处。假设我们有个请求用户数据的方法
fetchUser
:const fetchUser = (id) => {
return fetch(`xxx/user/${id}`).then(
res => res.json()
)
};
经由
react-cache
的createResource
方法包裹,他就成为一个resource
(资源):import {unstable_createResource as createResource} from 'react-cache';
const userResource = createResource(fetchUser);
resource
配合Suspense
就能以同步的方式编写异步请求数据的逻辑:function User({ userID }) {
const data = https://www.it610.com/article/userResource.read(userID);
return (name: {data.name}
age: {data.age}
)
}
可以看到,
userResource.read
完全是同步写法,其内部会调用fetchUser
。文章图片
背后的逻辑是:
- 首次调用
userResource.read
,会创建一个promise
(即fetchUser
的返回值) throw promise
React
内部catch promise
后,离User
组件最近的祖先Suspense
组件渲染fallback
promise resolve
后,User
组件重新render
- 此时再调用
userResource.read
会返回resolve
的结果(即fetchUser
请求的数据),使用该数据继续render
userResource.read
可能会调用2次,即:- 第一次发送请求、返回
promise
- 第二次返回请求到的数据
userResource
内部需要缓存该promise
的值,缓存的key
就是userID
:const data = https://www.it610.com/article/userResource.read(userID);
由于
userID
是User
组件的props
,所以当User
组件接收不同的userID
时,userResource
内部需要缓存不同userID
对应的promise
。如果切换100个
userID
,就会缓存100个promise
。显然我们需要一个缓存清理算法,否则缓存占用会越来越多,直至溢出。文章图片
react-cache
使用的缓存清理算法就是LRU
算法。LRU原理
LRU
(Least recently used,最近最少使用)算法的核心思想是:如果数据最近被访问过,那么将来被访问的几率也更高所以,越常被使用的数据权重越高。当需要清理数据时,总是清理最不常使用的数据。
react-cache中LRU的实现
react-cache
的实现包括两部分:- 数据的存取
- LRU算法实现
每个通过
createResource
创建的resource
都有一个对应map
,其中:- 该
map
的key
为resource.read(key)
执行时传入的key
- 该
map
的value
为resource.read(key)
执行后返回的promise
userResource
例子中,createResource
执行后会创建map
:const userResource = createResource(fetchUser);
userResource.read
首次执行后会在该map
中设置一条userID
为key
,promise
为value
的数据(被称为一个entry
):const data = https://www.it610.com/article/userResource.read(userID);
要获取某个
entry
,需要知道两样东西:entry
对应的key
entry
所属的resource
react-cache
使用双向环状链表实现LRU
算法,包含三个操作:插入、更新、删除。插入操作 首次执行
userResource.read(userID)
,得到entry0
(简称n0
),他会和自己形成环状链表:文章图片
此时
first
(代表最高权重)指向n0
。改变
userID props
后,执行userResource.read(userID)
,得到entry1
(简称n1
):文章图片
此时
n0
与n1
形成环状链表,first
指向n1
。如果再插入
n2
,则如下所示:文章图片
可以看到,每当加入一个新
entry
,first
总是指向他,暗含了LRU
中新的总是高权重的思想。更新操作 每当访问一个
entry
时,由于他被使用,他的权重会被更新为最高。对于如下
n0 n1 n2
,其中n2
权重最高(first
指向他):文章图片
当再次访问
n1
时,即调用如下函数时:userResource.read(n1对应userID);
n1
会被赋予最高权重:文章图片
删除操作 当缓存数量超过设置的上限时,
react-cache
会清除权重较低的缓存。对于如下
n0 n1 n2
,其中n2
权重最高(first
指向他):文章图片
如果缓存最大限制为1(即只缓存一个
entry
),则会迭代清理first.previous
,直到缓存数量为1。即首先清理
n0
:文章图片
接着清理
n1
:文章图片
每次清理后也会将
map
中对应的entry
删掉。完整LRU实现见 react-cache LRU总结 除了
React.lazy
、react-cache
能结合Suspense
,只要发挥想象力,任何异步流程都可以收敛到Suspense
中,比如React Server Compontnt
、流式SSR
。随着底层
React18
在年底稳定,相信未来这种同步写法的开发模式会逐渐成为主流。不管未来
React
开发出多少新奇玩意儿,底层永远是这些基础算法与数据结构。【从理念到LRU算法实现,起底未来React异步开发方式】真是朴素无华且枯燥......
推荐阅读
- 2018-02-06第三天|2018-02-06第三天 不能再了,反思到位就差改变
- 一个小故事,我的思考。
- Docker应用:容器间通信与Mariadb数据库主从复制
- 第三节|第三节 快乐和幸福(12)
- 你到家了吗
- 一个人的碎碎念
- 遇到一哭二闹三打滚的孩子,怎么办┃山伯教育
- 死结。
- 我从来不做坏事
- 赢在人生六项精进二阶Day3复盘