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

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

时间:2023-03-12 08:06:30 科技观察

大家好,我是Kason。在前几天写的一篇介绍use新钩子的文章中,讲到了React是如何原生实现一个缓存功能的——cache。对于下面的代码,多次调用cache包裹的函数时,如果传递的参数不变,则一直返回cache的值:constcacheFn=cache(fn);cacheFn(1,2,3);//不会执行fn,直接返回缓存值cacheFn(1,2,3);为什么React需要cache方法?考虑以下组件:constfetch=cache(fetchUserData);functionUser({id}){const{name}=use(fetch(id));return

{name}

;}User组件会根据Userid请求用户数据,并渲染用户名。如果id改变了,fetch方法重新发起请求是正常逻辑。然而,React组件经常被渲染。在id不变的情况下,由于User组件的渲染而不断发起请求,显然是不合理的。因此,在这种情况下,缓存方法是必需的。当id不变时,即使重复渲染User组件,fetch(id)返回相同的值。这篇文章会讲缓存的源码实现。实现思路分析整个方法实现共有64行代码。首先分析一下实现的要点。如果参数未更改,则使用缓存的值。这意味着我们需要处理:参数的顺序例如,当参数的顺序发生变化时,不使用缓存值:constcacheFn=cache(fn);缓存Fn(1,2,3);//不使用缓存值cacheFn(3,2,1);区分引用类型和原始类型参数。例如,当相同位置的参数传递相同的引用类型值时,返回缓存值:constcacheFn=cache(fn);constobj={};cacheFn(1,obj,3);//返回缓存值cacheFn(1,obj,3);当同一位置的参数传递不同的引用类型值时,不会返回缓存值:constcacheFn=cache(fn);常量对象={};cacheFn(1,obj,3);//不返回缓存值cacheFn(1,{},3);缓存垃圾回收缓存数据时,注意“缓存失效但引用数据未释放”导致内存泄漏。所以,对于引用类型的数据,可以使用WeakMap来保存。对于原始类型的数据,可以使用Map来保存。WeakMap和Map的区别在于——在WeakMap中,其对应值的键是弱引用。这意味着当没有其他数据引用该键时,它可以被垃圾回收。在Map中,keytovalue是强引用,即使没有其他数据引用这个key,也不会被垃圾回收。实现原理本文不介绍具体的代码实现(一大段代码贴出来让人看着头疼)。我将通过示例图来解释实现原理。了解原理后,如果对实现细节感兴趣,可以参考:缓存源码实现PR[1]缓存在线示例[2]如下代码:constcacheFn=cache(fn);constobj={};cacheFn(1,obj,3);cacheFn的每个传参都对应缓存内部的一个cacheNode节点://CacheNode构造函数createCacheNode():CacheNode{return{s:UNTERMINATED,v:undefined,o:null,p:null};}字段含义如下:s:cacheNode的缓存状态,有三种状态:未挂起/中止/发生错误。v:cacheNode缓存的值。o:缓存的引用类型值。p:缓存的原始类型值。上面的cacheFn执行后,会生成如下的cacheNode链式结构:让我们看看这个链式结构是如何解决文章开头提到的三个问题的。如何固定参数的顺序?可以看到上图中最后一个cacheNode节点(cacheNode.s)的状态是“Suspended”。如果在后续执行中将相同的参数传递给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链条会被打断,它们占用的内存也会被释放。但是,原始类型值不存在这样的问题。从图中可以看出,原来的type值对应的是一个map节点。总结一下,cache方法是React的内部实现,以后会暴露给开发者,可以缓存任意函数。当多次执行并将相同的参数传递给缓存包装函数时,后续执行将返回缓存值。这是为了应对“某些函数需要在React组件的多次渲染之间返回稳定值”的场景。例如:对于同一个参数,请求数据的函数返回同一个promise。缓存的实现是——根据传参构建一个cacheNode链。传递参数的稳定性对应链表的稳定性,最终对应返回值的稳定性。参考资料[1]缓存PR的源码实现:https://github.com/sebmarkbage/react/blob/ecdf734d1aa73d9f5f09f5a8e7fa5685f5f1bd29/packages/react/src/ReactCache.js。[2]缓存在线示例:https://codesandbox.io/s/amazing-leaf-viq4q7?file=/src/cache.js。