函数缓存

什么是函数缓存

为了讲明白这个概念,假设你在开发一个天气app。开始你不知道怎么作,正好有一个npm包里有一个getChanceOfRain的方法能够调用:javascript

import { getChangeOfRain } from 'magic-weather-calculator';

function showWeatherReport() {
  let result = getChangeOfRain();    // 这里调用
  console.log('The change of rain tomorrow is: ', result);
}
复制代码

只是这样会遇到一个问题。不管你作什么,只要调用这个方法就会消耗100毫秒。因此,若是某个用户疯狂点击“显示天气”按钮,每次点击app都会有一段时间没有响应。java

showWeatherReport(); // 触发计算
showWeatherReport(); // 触发计算
showWeatherReport(); // 触发计算
复制代码

这很不理性。在实际开发中,若是你已经知道结果了,那么你不会一次一次的计算结果。重用上次的结果才是上佳选择。这就是函数缓存。函数缓存也就是缓存函数的结算结果,这样就不须要一次一次的调用函数git

在下面的例子里,咱们会调用memoizedGetChangeOfRain()。在这个方法里咱们会检查一下是否已经有结果了,而不会每次都调用getChangeOfRain()方法:github

import { getChangeOfRain } from 'magic-weather-calculator';

let isCalculated = false;
let lastResult;

// 添加这个方法
function momoizedGetChangeOfRain() {
  if (isCalculated) {
    // 不须要在计算一次
    return lastResult;
  }
  
  // 第一次运行时计算
  let result = getChangeOfRain();
  
  lastResult = result;
  isCalculated = true;
  
  return result;
}

function showWeatherReport() {
  let result = momoizedGetChangeOfRain();
  console.log('The chance of rain tomottow is:', result);
}

复制代码

屡次调用showWeatherReport()只会在第一次作计算,其余都是返回第一次计算的结果。算法

showWeatherReport(); // (!) 计算
showWeatherReport(); // 直接返回结果
showWeatherReport(); // Uses the calculated result
showWeatherReport(); // Uses the calculated result
复制代码

这就是函数缓存。当咱们说一个函数被缓存了,不是说在javascript语言上作了什么。而是当咱们知道结果不变的状况下避免没必要要的调用。npm

函数缓存和参数

通常的函数缓存模式:浏览器

  1. 检查是否存在一个结果
  2. 若是是,则返回这个结果
  3. 若是没有,计算结果并保存在之后返回

然而,实际开发中须要考虑某些状况。好比:getChangeOfRain()方法接收一个城市参数:缓存

function showWeatherReport(city) {
  let result = getChanceOfRain(city); // Pass the city
  console.log("The chance of rain tomorrow is:", result);
}
复制代码

若是只是简单的像以前同样缓存这个函数,就会产生一个bug:安全

showWeatherReport('Tokyo');  // (!) Triggers the calculation
showWeatherReport('London'); // Uses the calculated answer
复制代码

发现了么?东京和伦敦的天气是很不同的,因此咱们不能直接使用以前的计算结果。也就是说咱们使用函数缓存的时候必需要考虑参数markdown

方法1:保存上一次的结果

最简单的方法就是缓存结果和这个结果依赖的参数。也就是这样:

import { getChanceOfRain } from 'magic-weather-calculator';

let lastCity;
let lastResult;

function memoizedGetChanceOfRain(city) {
  if (city === lastCity) { // 检查城市!
    // 城市相同返回上次的结果
    return lastResult;
  }
  
  // 第一次计算,或者参数变了则执行计算
  let result = getChanceOfRain(city);
  
  // 保留参数和结果.
  lastCity = city;
  lastResult = result;
  return result;
}

function showWeatherReport(city) {
  // 参数传递给缓存的参数
  let result = memoizedGetChanceOfRain(city);
  console.log("The chance of rain tomorrow is:", result);
}
复制代码

注意这个例子和第一个例子的些许不一样。再也不是直接返回上次的计算结果,而是比较city === lastCity。若是中途城市发生了变化就要从新计算结果。

showWeatherReport('Tokyo');  // (!) 计算
showWeatherReport('Tokyo');  // 使用缓存结果
showWeatherReport('Tokyo');  // 使用缓存结果
showWeatherReport('London'); // (!) 从新计算
showWeatherReport('London'); // 使用缓存结果
复制代码

这样虽然修改了第一个例子的bug,可是也不老是最好的解决办法。若是每次调用参数都不同,上面的解决方法就没什么用处了。

showWeatherReport('Tokyo');  // (!) 执行计算
showWeatherReport('London'); // (!) 执行计算
showWeatherReport('Tokyo');  // (!) 执行计算
showWeatherReport('London'); // (!) 执行计算
showWeatherReport('Tokyo');  // (!) 执行计算
复制代码

不管什么时候使用函数缓存都要检查下是否是真的有帮助!

方法2:保留多个结果

另外一件咱们能够作的就是保留多个结果。虽然咱们能够为每一个参数都定义一个变量,好比:lastTokyoResult, lastLondonResult等。使用Map看起来是一个更好的方法。

let resultsPerCity = new Map();

function memoizedGetChangeOfRain(city) {
  if (resultsPerCity.has(city)) {
    // 返回已经存在的结果
    return resultsPerCity.get(city);
  }
  
  // 第一次获取城市数据
  let result = getChangeOfRain(city);
  
  // 保留整个城市的数据
  resultsPerCity.set(city, result);
  
  return result;
}

function showWeatherReport(city) {
  let result = memoizedGetChangeOfRain(city);
  console.log('The chance of rain tomorrow is:', result);
}
复制代码

整个方法和适合咱们的用例。由于它只会在第一次获取城市数据的时候计算。使用相同的城市获取数据的时候都会返回已经保存在Map里的数据。

showWeatherReport('Tokyo');  // (!) 执行计算
showWeatherReport('London'); // (!) 执行计算
showWeatherReport('Tokyo');  // 使用缓存结果
showWeatherReport('London'); // 使用缓存结果
showWeatherReport('Tokyo');  // 使用缓存结果
showWeatherReport('Paris');  // (!) 执行计算
复制代码

然而这样的方法也不是没有缺点。尤为在咱们城市参数不断增长的状况下,咱们保存在Map里的数据会不断增长。

因此,这个方法在得到性能提高的同时在无节制的消耗内存。在最坏的状况下会形成浏览器tab的崩溃。

其余方法

在“只保存上一个结果”和“保存所有结果”之间还有不少其余的办法。好比,保存最近使用的最后N个结果,也就是我么熟知的LRU,或者“最近最少使用”缓存。这些都是在Map以外添加其余逻辑的方法。你也能够删除某些时间以后删掉过去的数据,就如同浏览器在缓存过时以后会把他们删掉同样。若是参数是一个对象(不是上例所示的字符串),咱们可使用WeakMap来代替Map。现代一点的浏览器都支持。使用WeakMap的好处是在做为key的对象不存在的时候会把键值对都删除。函数缓存是一个很是灵活的技术,你能够根据具体状况使用不一样的策略。

函数缓存和函数纯度

咱们知道函数缓存不老是安全的。

假设getChangeOfRain()方法不接受一个城市做为参数,而是直接接收用户输入:

function getChangeOfRain() {
  // 显示输入框
  let city = prompt('Where do you live?');
  
  // 其余代码
}

// 咱们的代码
function showWeatherReport() {
  let result = getChangeOfRain();
  console.log('The chance of rain tomorrow is:', result);
}
复制代码

每次调用showWeatherReport()方法都会出现一个输入框。咱们能够输入不一样的城市,在console里看到不一样的结果。可是若是缓存了getChanceOfRain()方法,咱们只会看到一个输入框!无法输入一个不一样的城市。

因此函数缓存只有在那个函数是纯函数的状况下才是安全的。也就是说:只读取参数,不和外界交互。一个纯函数,调用一次或者使用以前的缓存结果都是无所谓的。

这也是为何在一个复杂的算法里,把仅仅计算的代码和作什么的代码分离的缘由。纯计算的方法能够安全的缓存来避免屡次调用。而那些什么的方法无法作相同的处理。

// 若是这个方法值作计算的话,那么能够被称为纯函数
// 对它使用函数缓存是安全的。
function getChanceOfRain(city) {
  // ...计算代码...
}

// 这个方法要显示输入框给用户,因此不是纯函数
function showWeatherReport() {
  // 这里显示输入框
  let city = prompt('Where do you live?');
  let result = getChanceOfRain(city);
  console.log("The chance of rain tomorrow is:", result);
}
复制代码

如今能够安全的对getChanceOfRain()作函数缓存。--由于它接受city做为参数,而不是弹出一个输入框。换句话说,它是纯函数。

每次调用showWeatherReport()仍是会看到输入框。可是获得结果以后对应的计算是能够避免的。

重用函数缓存

若是你要缓存不少个方法,为每一个方法写一次缓存有点重复劳动。这个是能够自动化的,一个方法就能够搞定。

咱们用第一个例子来演示:

let isCalculated = false;
let lastResult;

function memoizedGetChanceOfRain() {
  if (isCalculated) {
    return lastResult;
  }
  
  let result = getChanceOfRain();
  lastResult = result;
  isCalculated = true;
  
  return result;
}
复制代码

以后咱们把这些步骤都放在一个叫作memoize的方法里:

function memoize() {
  let isCalculated = false;
  let lastResult;
  
  function memoizedGetChanceOfRain() {
    if (isCalculated) {
      return lastResult;
    }
    
    let result = getChanceOfRain();
    lastResult = result;
    isCalculated = true;
    return result;
  }
}
复制代码

咱们要让这个方法更加有用,不只仅是计算下雨的几率。因此咱们要添加一个方法参数,就叫作fn

function memoize(fn) { // 声明fn参数
  let isCalculated = false;
  let lastResult;
  
  function memoizedGetChanceOfRain() {
    if (isCalculated) {
      return lastResult;
    }
    let result = fn(); // 调用传入的方法参数
    lastResult = result;
    isCalculated = true;
    return result;
  }
}
复制代码

最后把memoizedGetChanceOfRain()重命名为memoizedFn并返回:

function memoize(fn) {
  let isCalculated = false;
  let lastResult;
  
  return function memoizedFn() {
    if (isCalculated) {
      return lastResult;
    }
    
    let result = fn();
    lastResult = result;
    isCalculated = true;
    return result;
  }
}
复制代码

咱们获得了一个能够重用的缓存函数。

如今咱们最开始的例子能够改为:

import { getChanceOfRain } from 'magic-weather-calculator';

let memoizedGetChanceOfRain = memoize(getChanceOfRain);

function showWeatherReport() {
  let result = memoizedGetChanceOfRain();
  console.log('The chance of rain tomorrow is:', result);
}
复制代码

isCalculatedlastResult还在,可是在memoize方法内。也就是说他们是闭包的一部分了。咱们能够在任何地方使用memoize方法了,每次都独立缓存。

import { getChanceOfRain, getNextEarthquake, getCosmicRaysProbability } from 'magic-weather-calculator';

let momoizedGetChanceOfRain = memoize(getChanceOfRain);
let memoizedGetNextEarthquake = memoize(getNextEarthquake);
let memoizedGetCosmicRaysProbability = memoize(getCosmicRaysProbability);
复制代码

这里memoize的目的是生成方法的缓存版本。这样咱们就不要每次都写那么多重复代码了。

回顾

如今咱们能够快速的回顾一下。函数缓存是一个让你的程序运行加快的方法。若是有一段代码只作计算(纯函数)那么这段代码就能够经过函数缓存避免为同一个结果而执行没有必要的重复计算。

咱们能够缓存最后的N个结果,也能够是所有的结果。这些须要你根据实际的状况作取舍。

你本身实现memoize方法并不困难,同事也有一些包帮你作这件事情。这里有Lodash的实现。

最核心的部分基本都是这样的:

import { getChanceOfRain } from 'magic-weather-calculator';

function showWeatherReport() {
  let result = getChanceOfRain();
  console.log('The chance of rain tomorrow is:', result);
}
复制代码

会变成:

import { getChanceOfRain } from 'magic-weather-calculator';

let isCalculated = false;
let lastResult;

function memoizedGetChanceOfRain() {
  if (isCalculated) {
    return lastResult;
  }
  let result = getChanceOfRain();
  lastResult = result;
  isCalculated = true;
  return result;
}

function showWeatherReport() {
  let result = memoizedGetChanceOfRain();
  console.log("The chance of rain tomorrow is:", result);
}
复制代码

合理的使用函数缓存会带来实际的性能提高。固然,要当心可能带来的复杂度和潜在的bug。

备注

原文在这里:whatthefork.is/memoization

相关文章
相关标签/搜索