缓存记忆是一种能大幅提升你代码速度的通用技术,它先把一个耗时操作的结果缓存起来,当下次再去调用这个耗时操作的时候就不用再浪费时间计算直接给你结果。
基于这种定义,我们提炼出几个使用缓存记忆的标准,根据这些标准我们再考虑要不要在代码里加上缓存记忆。
1, 缓存应该主要用于加速那些开销巨大,耗时的函数。
2,记忆主要针对非首次调用,如果你在相同的条件下反复的调用相同的函数,那么是时候考虑把它记住了。
3,因为这个缓存的结果是放在内存里面,如果调用方法的条件变化无常,那么就没必要使用了。
下面牛刀小试,看看缓存记忆的威力。
class MyObject {
constructor(data) {
this.data = data;
this.data[this.data.length - 2] = { value: 'Non-empty' };
}
firstNonEmptyItem() {
return this.data.find(v => !!v.value);
}
firstNonEmptyItemMemo() {
if (!this.firstNonEmpty)
this.firstNonEmpty = this.data.find(v => !!v.value);
return this.firstNonEmpty;
}
}
const myObject = new MyObject(Array(2000).fill({ value: null }));
for (let i = 0; i < 100; i ++)
myObject.firstNonEmptyItem(); // ~4000ms
for (let i = 0; i < 100; i ++)
myObject.firstNonEmptyItemMemo(); // ~70ms
上面的代码通过JS的类实现了缓存记忆。一个长度2000的数组,倒数第2个元素是一个非空的元素,找到这个非空元素是一个开销较大的操作。
在实际的打码操作中是没有调用100次的操作,但是仅仅是加了一个缓存记忆,这段代码执行的速度得到了肉眼可见的提高。
上面的这个例子是缓存记忆Class中的耗时函数,下面我们直接对函数进行缓存,借助proxy,劫持函数的调用方法来实现缓存记忆。
const memoize = fn => new Proxy(fn, {
cache: new Map(),
apply (target, thisArg, argsList) {
let cacheKey = argsList.toString();
if(!this.cache.has(cacheKey))
this.cache.set(cacheKey, target.apply(thisArg, argsList));
return this.cache.get(cacheKey);
}
});
const fibonacci = n => (n <= 1 ? 1 : fibonacci(n - 1) + fibonacci(n - 2));
const memoizedFibonacci = memoize(fibonacci);
for (let i = 0; i < 100; i ++)
fibonacci(30); // ~5000ms
for (let i = 0; i < 100; i ++)
memoizedFibonacci(30); // ~50ms
在参数相同的情况下,如果缓存记忆的结果在map中,当函数被调用直接返回map中记忆的结果。
这样的操作让人很容易联想到框架中类似的应用,例如React Hooks的useCallback和useMemo.
useCallback
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);
返回一个 memoized 回调函数。
把内联回调函数及依赖项数组作为参数传入 useCallback
,它将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新。当你把回调函数传递给经过优化的并使用引用相等性去避免非必要渲染(例如 shouldComponentUpdate
)的子组件时,它将非常有用。
下面是源代码
// mount阶段就是获取到传入的回调函数和依赖数组,保存到hook的memorizedState中,然后返回回调函数。
function mountCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
hook.memoizedState = [callback, nextDeps];
return callback;
}
// update阶段
function updateCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
const hook = updateWorkInProgressHook();
// 从hook的memorizedState中获取上次保存的值[callback, deps],
const nextDeps = deps === undefined ? null : deps;
const prevState = hook.memoizedState;
if (prevState !== null) {
if (nextDeps !== null) {
const prevDeps: Array<mixed> | null = prevState[1];
// 比较新的deps和之前的deps是否相等
if (areHookInputsEqual(nextDeps, prevDeps)) {
// 如果相等,返回memorized的callback
return prevState[0];
}
}
}
// 如果deps发生变化,更新hook的memorizedState,并返回最新的callback
hook.memoizedState = [callback, nextDeps];
return callback;
}
useMemo
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
返回一个 memoized 值。
把“创建”函数和依赖项数组作为参数传入 useMemo
,它仅会在某个依赖项改变时才重新计算 memoized 值。这种优化有助于避免在每次渲染时都进行高开销的计算。
// mount阶段, 执行创建函数获得返回值
// 保存到hook的memorizedState中[nextValue, nextDeps]
function mountMemo<T>(
nextCreate: () => T,
deps: Array<mixed> | void | null,
): T {
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
const nextValue = nextCreate();
hook.memoizedState = [nextValue, nextDeps];
return nextValue;
}
// update阶段
function updateMemo<T>(
nextCreate: () => T, // 注意这里有返回值
deps: Array<mixed> | void | null,
): T {
const hook = updateWorkInProgressHook();
// 获取新的deps
const nextDeps = deps === undefined ? null : deps;
// 从memorizedState中获得上次保存的值
const prevState = hook.memoizedState;
if (prevState !== null) {
if (nextDeps !== null) {
// 比较新deps和旧deps是否相等,如果两者相等,返回旧的创建函数的返回值
const prevDeps: Array<mixed> | null = prevState[1];
if (areHookInputsEqual(nextDeps, prevDeps)) {
return prevState[0];
}
}
}
// 如果deps发生改变,hook中保存新的返回值和deps,并返回新的创建函数的返回值
const nextValue = nextCreate();
hook.memoizedState = [nextValue, nextDeps];
return nextValue;
}
在实际的使用场景中,根据我们要缓存值或者缓存函数可以灵活使用。useMemo还可以拿来缓存组件,相当于Class component的purecomponent。