React内部是如何实现Cache方法的?

开发 前端
React组件经常render,如果在id不变的情况下,由于User组件render导致不断发起请求,显然是不合理的。这种情况下就需要cache方法。当id不变时,即使User组件反复render,fetch(id)都返回同一个值。本文来聊聊cache的源码实现。

大家好,我卡颂。

前几天写的一篇介绍use这个新hook的文章中聊到React原生实现了一个缓存函数的方法 —— cache。

对于如下代码,被cache包裹的函数,当多次调用时,如果传参不变,会始终返回缓存值:

const cacheFn = cache(fn);
cacheFn(1, 2, 3);
// 不会执行fn,直接返回缓存值
cacheFn(1, 2, 3);

React内为什么需要cache方法呢?考虑如下组件:

const fetch = cache(fetchUserData);

function User({id}) {
const {name} = use(fetch(id));

return <p>{name}</p>;
}

User组件会根据用户id请求用户数据,并渲染用户名。

如果id改变,那么fetch方法重新发起请求是正常逻辑。

但是,React组件经常render,如果在id不变的情况下,由于User组件render导致不断发起请求,显然是不合理的。

所以,这种情况下就需要cache方法。当id不变时,即使User组件反复render,fetch(id)都返回同一个值。

本文来聊聊cache的源码实现。

分析实现思路

整个方法实现一共有64行代码,首先我们来分析下实现要点。

如果参数不变,则使用缓存的值。这意味着我们需要处理:

参数的顺序

举个例子,当参数顺序变了,不使用缓存值:

const cacheFn = cache(fn);
cacheFn(1, 2, 3);
// 不使用缓存值
cacheFn(3, 2, 1);

区别处理引用类型、原始类型参数

举个例子,当同一位置的参数传递了同一个引用类型值,则返回缓存值:

const cacheFn = cache(fn);
const obj = {};
cacheFn(1, obj, 3);
// 返回缓存值
cacheFn(1, obj, 3);

当同一位置的参数传递了不同引用类型值,则不返回缓存值:

const cacheFn = cache(fn);
const obj = {};
cacheFn(1, obj, 3);
// 不返回缓存值
cacheFn(1, {}, 3);

缓存的垃圾回收

缓存数据时,要注意「缓存失效但是引用的数据没有释放」造成的内存泄漏问题。

所以,对于引用类型数据,可以使用WeakMap保存。

对于原始类型数据,可以使用Map保存。

WeakMap与Map的区别在于 —— 在WeakMap中,key到他对应的value是弱引用。这意味着当没有其他数据引用这个key时,他可以被垃圾回收。而在Map中,key到value是强引用,即使没有其他数据引用这个key,他也不会被垃圾回收。

实现原理

本文不会介绍具体的代码实现(大段贴代码让人看起来头疼)。

我会用示例图讲解实现原理。了解原理后,如果你对实现细节感兴趣,可以参考:

  • cache的源码实现PR[1]
  • cache的在线示例[2]

对于如下代码:

const cacheFn = cache(fn);
const obj = {};
cacheFn(1, obj, 3);

cacheFn的每个传参,对应cache内部的一个cacheNode节点:

// CacheNode构造函数
function createCacheNode<T>(): CacheNode<T> {
return {
s: UNTERMINATED,
v: undefined,
o: null,
p: null
};
}

字段的意义如下:

  • s:cacheNode的缓存状态,有 未中止/中止/发生错误 3种状态。
  • v:cacheNode缓存的值。
  • o:缓存的引用类型值。
  • p:缓存的原始类型值。

上述cacheFn执行后会生成如下cacheNode链式结构:

图片

让我们看看这个链式结构如何解决文章开篇提到的3个问题。

如何解决参数的顺序?

可以看到,上图中最后一个cacheNode节点的状态(cacheNode.s)为「中止」。

如果后续执行cacheFn传入相同的参数,则会复用缓存的cacheNode节点。

如果所有传参都相同,那么会复用完整的cacheNode链,此时最后一个cacheNode节点为「中止」状态,则不需要重新执行cacheFn方法计算返回值,而是直接返回缓存的值(cacheNode.v)。

如果后续执行cacheFn,传入新的参数,则前后的cacheNode链不会一致。

比如:

// 第一次
cacheFn(1, obj, 3);
// 第二次
cacheFn(1, 3, obj);

则第二次生成的cacheNode链中,第二个节点就与之前不同(之前obj,之后3),则后续cacheNode节点也不会相同。

图片

通过这种链式结构,保证了只有当所有参数保持一致,才能返回缓存的值。否则将重新执行函数,并缓存新的返回值与cacheNode链。

如何处理引用类型值

可以从图中发现,对于引用类型参数(比如示例中的obj),对应一个weakMap节点。

这不仅意味着当没有其他数据引用他时,这个cacheNode节点能够释放内存,同时也意味着这个cacheNode之后的cacheNode链会断掉,他们占用的内存也会释放。

而原始类型值不存在这样的问题,从图中可以发现,原始类型值对应一个map节点。

总结

cache方法是React内部实现,未来会暴露给开发者使用的缓存方法,可以缓存任意函数。

当多次执行并传递相同的参数给cache包裹的函数时,后续执行会返回缓存的值。

这是为了应对「某些函数需要在React组件多次render间返回稳定的值」的场景。

比如:对于相同的传参,请求数据的函数返回同一个promise。

cache的实现方式是 —— 基于传参,构造一条cacheNode链,传参的稳定对应了链表的稳定,并最终对应了返回值的稳定。

参考资料

[1]cache的源码实现PR:https://github.com/sebmarkbage/react/blob/ecdf734d1aa73d9f5f09f5a8e7fa5685f5f1bd29/packages/react/src/ReactCache.js。

[2]cache的在线示例:https://codesandbox.io/s/amazing-leaf-viq4q7?file=/src/cache.js。

责任编辑:姜华 来源: 魔术师卡颂
相关推荐

2021-08-19 16:56:37

Python内存开发

2023-11-23 19:30:35

Python编程语言

2020-02-12 15:08:41

KVM内部运作

2023-10-07 08:41:42

JavaJVM

2022-08-22 16:23:11

React特性

2023-05-05 18:38:33

多级缓存Caffeine开发

2022-04-14 09:01:39

React源码Flow

2023-04-06 09:41:00

React 组件重渲染

2022-07-27 07:32:28

Debug版本arthas

2019-10-08 11:10:18

React自动保存前端

2022-12-23 08:34:30

HookReact

2022-05-15 22:08:58

ReactHookdebounce

2022-03-11 10:23:02

React性能优化

2021-09-17 12:50:10

MySQL数据库ACID

2020-10-21 08:38:47

React源码

2022-12-07 11:21:30

Reactdiff

2024-11-05 15:02:41

2023-05-31 07:29:46

2023-12-05 15:58:06

React开发

2022-05-16 08:22:37

零拷贝Netty
点赞
收藏

51CTO技术栈公众号