1 引言
函数缓存是重要概念,实质上就是用空间(缓存存储)换工夫(跳过计算过程)。
对于无副作用的纯函数,在适合的场景应用函数缓存是十分必要的,让咱们跟着 https://whatthefork.is/memoiz… 这篇文章深刻了解一下函数缓存吧!
2 概述
假如又一个获取天气的函数 getChanceOfRain
,每次调用都要花 100ms 计算:
import {getChanceOfRain} from "magic-weather-calculator";
function showWeatherReport() {let result = getChanceOfRain(); // Let the magic happen
console.log("The chance of rain tomorrow is:", result);
}
showWeatherReport(); // (!) Triggers the calculation
showWeatherReport(); // (!) Triggers the calculation
showWeatherReport(); // (!) Triggers the calculation
很显然这样太节约计算资源了,当曾经计算过一次天气后,就没有必要再算一次了,咱们冀望的是后续调用能够间接拿上一次后果的缓存,这样能够节俭大量计算。因而咱们能够做一个 memoizedGetChanceOfRain
函数缓存计算结果:
import {getChanceOfRain} from "magic-weather-calculator";
let isCalculated = false;
let lastResult;
// We added this function!
function memoizedGetChanceOfRain() {if (isCalculated) {
// No need to calculate it again.
return lastResult;
}
// Gotta calculate it for the first time.
let result = getChanceOfRain();
// Remember it for the next time.
lastResult = result;
isCalculated = true;
return result;
}
function showWeatherReport() {
// Use the memoized function instead of the original function.
let result = memoizedGetChanceOfRain();
console.log("The chance of rain tomorrow is:", result);
}
在每次调用时判断优先用缓存,如果没有缓存则调用原始函数并记录缓存。这样当咱们屡次调用时,除了第一次之外都会立刻从缓存中返回后果:
showWeatherReport(); // (!) Triggers the calculation
showWeatherReport(); // Uses the calculated result
showWeatherReport(); // Uses the calculated result
showWeatherReport(); // Uses the calculated result
然而对于有参数的场景就不实用了,因为缓存并没有思考参数:
function showWeatherReport(city) {let result = getChanceOfRain(city); // Pass the city
console.log("The chance of rain tomorrow is:", result);
}
showWeatherReport("Tokyo"); // (!) Triggers the calculation
showWeatherReport("London"); // Uses the calculated answer
因为参数可能性很多,所以有三种解决方案:
1. 仅缓存最初一次后果
仅缓存最初一次后果是最节俭存储空间的,而且不会有计算错误,但带来的问题就是当参数变动时缓存会立刻生效:
import {getChanceOfRain} from "magic-weather-calculator";
let lastCity;
let lastResult;
function memoizedGetChanceOfRain(city) {if (city === lastCity) {
// Notice this check!
// Same parameters, so we can reuse the last result.
return lastResult;
}
// Either we're called for the first time,
// or we're called with different parameters.
// We have to perform the calculation.
let result = getChanceOfRain(city);
// Remember both the parameters and the result.
lastCity = city;
lastResult = result;
return result;
}
function showWeatherReport(city) {
// Pass the parameters to the memoized function.
let result = memoizedGetChanceOfRain(city);
console.log("The chance of rain tomorrow is:", result);
}
showWeatherReport("Tokyo"); // (!) Triggers the calculation
showWeatherReport("Tokyo"); // Uses the calculated result
showWeatherReport("Tokyo"); // Uses the calculated result
showWeatherReport("London"); // (!) Triggers the calculation
showWeatherReport("London"); // Uses the calculated result
在极其状况下等同于没有缓存:
showWeatherReport("Tokyo"); // (!) Triggers the calculation
showWeatherReport("London"); // (!) Triggers the calculation
showWeatherReport("Tokyo"); // (!) Triggers the calculation
showWeatherReport("London"); // (!) Triggers the calculation
showWeatherReport("Tokyo"); // (!) Triggers the calculation
2. 缓存所有后果
第二种计划是缓存所有后果,应用 Map 存储缓存即可:
// Remember the last result *for every city*.
let resultsPerCity = new Map();
function memoizedGetChanceOfRain(city) {if (resultsPerCity.has(city)) {
// We already have a result for this city.
return resultsPerCity.get(city);
}
// We're called for the first time for this city.
let result = getChanceOfRain(city);
// Remember the result for this city.
resultsPerCity.set(city, result);
return result;
}
function showWeatherReport(city) {
// Pass the parameters to the memoized function.
let result = memoizedGetChanceOfRain(city);
console.log("The chance of rain tomorrow is:", result);
}
showWeatherReport("Tokyo"); // (!) Triggers the calculation
showWeatherReport("London"); // (!) Triggers the calculation
showWeatherReport("Tokyo"); // Uses the calculated result
showWeatherReport("London"); // Uses the calculated result
showWeatherReport("Tokyo"); // Uses the calculated result
showWeatherReport("Paris"); // (!) Triggers the calculation
这么做带来的弊病就是内存溢出,当可能参数过多时会导致内存无限度的上涨,最坏的状况就是触发浏览器限度或者页面解体。
3. 其余缓存策略
介于只缓存最初一项与缓存所有项之间还有这其余抉择,比方 LRU(least recently used)只保留最小化最近应用的缓存,或者为了不便浏览器回收,应用 WeakMap 代替 Map。
最初提到了函数缓存的一个坑,必须是纯函数。比方上面的 CASE:
// Inside the magical npm package
function getChanceOfRain() {
// Show the input box!
let city = prompt("Where do you live?");
// ... calculation ...
}
// Our code
function showWeatherReport() {let result = getChanceOfRain();
console.log("The chance of rain tomorrow is:", result);
}
getChanceOfRain
每次会由用户输出一些数据返回后果,导致缓存谬误,起因是“函数入参一部分由用户输出”就是副作用,咱们不能对有副作用的函数进行缓存。
这有时候也是拆分函数的意义,将一个有副作用函数的无副作用局部合成进去,这样就能部分做函数缓存了:
// If this function only calculates things,
// we would call it "pure".
// It is safe to memoize this function.
function getChanceOfRain(city) {// ... calculation ...}
// This function is "impure" because
// it shows a prompt to the user.
function showWeatherReport() {
// The prompt is now here
let city = prompt("Where do you live?");
let result = getChanceOfRain(city);
console.log("The chance of rain tomorrow is:", result);
}
最初,咱们能够将缓存函数形象为高阶函数:
function memoize(fn) {
let isCalculated = false;
let lastResult;
return function memoizedFn() {
// Return the generated function!
if (isCalculated) {return lastResult;}
let result = fn();
lastResult = result;
isCalculated = true;
return result;
};
}
这样生成新的缓存函数就不便啦:
let memoizedGetChanceOfRain = memoize(getChanceOfRain);
let memoizedGetNextEarthquake = memoize(getNextEarthquake);
let memoizedGetCosmicRaysProbability = memoize(getCosmicRaysProbability);
isCalculated
与 lastResult
都存储在 memoize
函数生成的闭包内,内部无法访问。
3 精读
通用高阶函数实现函数缓存
原文的例子还是比较简单,没有思考函数多个参数如何解决,上面咱们剖析一下 Lodash memoize
函数源码:
function memoize(func, resolver) {
if (
typeof func != "function" ||
(resolver != null && typeof resolver != "function")
) {throw new TypeError(FUNC_ERROR_TEXT);
}
var memoized = function () {
var args = arguments,
key = resolver ? resolver.apply(this, args) : args[0],
cache = memoized.cache;
if (cache.has(key)) {return cache.get(key);
}
var result = func.apply(this, args);
memoized.cache = cache.set(key, result) || cache;
return result;
};
memoized.cache = new (memoize.Cache || MapCache)();
return memoized;
}
原文有提到缓存策略多种多样,而 Lodash 将缓存策略简化为 key 交给用户本人治理,看这段代码:
key = resolver ? resolver.apply(this, args) : args[0];
也就是缓存的 key 默认是执行函数时第一个参数,也能够通过 resolver
拿到参数解决成新的缓存 key。
在执行函数时也传入了参数 func.apply(this, args)
。
最初 cache
也不再应用默认的 Map,而是容许用户自定义 lodash.memoize.Cache
自行设置,比方设置为 WeakMap:
_.memoize.Cache = WeakMap;
什么时候不适宜用缓存
以下两种状况不适宜用缓存:
- 不常常执行的函数。
- 自身执行速度较快的函数。
对于不常常执行的函数,自身就不须要利用缓存晋升执行效率,而缓存反而会长期占用内存。对于自身执行速度较快的函数,其实大部分简略计算速度都很快,应用缓存后对速度没有显著的晋升,同时如果计算结果比拟大,反而会占用存储资源。
对于援用的变动尤其重要,比方如下例子:
function addName(obj, name){
return {
...obj,
name:
}
}
为 obj
增加一个 key,自身执行速度是十分快的,但增加缓存后会带来两个害处:
- 如果
obj
十分大,会在闭包存储残缺obj
构造,内存占用加倍。 - 如果
obj
通过 mutable 形式批改了,则一般缓存函数还会返回原先后果(因为对象援用没有变),造成谬误。
如果要强行进行对象深比照,尽管会避免出现边界问题,但性能反而会大幅降落。
4 总结
函数缓存十分有用,但并不是所有场景都实用,因而千万不要极其的将所有函数都增加缓存,仅限于计算耗时、可能反复利用屡次,且是纯函数的。
探讨地址是:精读《函数缓存》· Issue #261 · dt-fe/weekly
如果你想参加探讨,请 点击这里,每周都有新的主题,周末或周一公布。前端精读 – 帮你筛选靠谱的内容。
关注 前端精读微信公众号
版权申明:自在转载 - 非商用 - 非衍生 - 放弃署名(创意共享 3.0 许可证)
本文应用 mdnice 排版