喂!别再要求一切了:记忆化简易指南
喂,别再费力地调用函数去获取你两分钟前刚获取的数据了!你会问怎么做?其实很简单,当然是用记忆化啦。
定义
记忆化是动态规划中的一种优化技术,它涉及将昂贵的函数调用的值存储在内存中,以便当您需要再次检索这些值时,您可以更快地完成!
目标
- 了解记忆的基本概念。
- 识别何时应该使用记忆法。
- 识别何时不应该使用记忆法。
先决条件
虽然不是必需的,但如果你已经了解以下知识,那么本文将会更容易理解:
概述
记忆化是一种缓存形式,它涉及将函数的返回值存储在内存中。当函数被调用时,会检查缓存对象,判断传入的输入值是否已经存在。如果存在,则返回缓存的结果。如果不存在,则进行繁重的计算,并将返回值也存储在缓存中,以便下次需要时更快地获取。
让我们看一个基本的例子...
基本示例
1. 让我们创建一个闭包
我们使用闭包来封装缓存对象,并将其初始化为一个空对象。我们还添加了一个函数来检查缓存并执行繁重的工作。
const memoizeFn = () => {
// our cache object
let cache = {};
return (input) => {
// the contents of the function which will be doing the heavy work
}
}
2. 让我们在闭包中创建函数
在这个例子中,我们将使用一个使输入加倍的函数,这显然不是一个要求很高的函数,但它适用于这个例子。
const memoizeFn = () => {
let cache = {};
return (input) => {
const result = input * 2;
return result;
}
}
3. 现在,是时候记住了
我们真正需要做的就是在内部函数中添加一个 if..else 条件,以查看缓存中是否存在该值。
const memoizeFn = () => {
let cache = {};
return (input) => {
// lets log our cache here so we can see what is stored
// when we call our function
console.log(cache);
// have we got the result of this input already from a previous call?
if (cache[input]) {
// nice, we do! No need for any heavy computation here!
return cache[input];
} else {
// it’s not in our cache!
const result = input * 2;
// store the result in the cache so next time it is called with this input
// we can retrieve it from our cache
cache[input] = result;
return result;
}
}
}
从上面的例子中可以看到,我们有一个闭包 memoizeFn,它用一个空对象初始化缓存,并返回一个计算量很大的纯函数,该函数接受一个数字作为输入。该输入用作缓存对象中的键。每次调用该函数时,都会检查缓存,看看是否已经有与输入对应的结果。
4. 让我们看看实际效果
// this invokes the first function and initialises our cache object
const doubleInput = memoizeFn();
doubleInput(10); // console log = {}
doubleInput(20); // console log = {10: 20}
// 10 is in our cache. No heavy computation needed
doubleInput(10); // console log = {10: 20, 20: 40}
memoizeFn被调用并赋值给doubleInput变量,该变量现在可以在调用时访问缓存对象了。首先,我们传入值10 调用doubleInput函数,此时缓存对象为空,因此需要进行将该数字翻倍的繁重计算。接下来,我们将 20 作为输入,由于缓存中不存在 20,因此该值需要经过函数的繁重计算部分。最后,我们再次将 10 传递给函数,检查缓存对象是否存在键为10的值,如果存在,则从缓存中检索该值!
那么,在现实世界中我会在哪里使用它呢?
让我们看一个更实际的例子。假设你正在创建一个 SPA 社交媒体平台,用户可以拥有一个好友列表,当用户点击其中一位好友时,它会返回该用户的个人资料。我们需要调用一个 API 来返回与该个人资料相关的数据,对吗?没错。但是,如果用户在浏览网站时返回到之前访问过的个人资料,我们是否需要再次调用该 API?我们可以,或者可以使用 memoization 机制。方法如下:
const memoizeUser = () => {
let cache = {};
return async (userId) => {
if (cache[userId]) {
return cache[userId];
}
// it's not in our cache, we need to hit the API
// this could take a little while...
const data = await fetch(`https://myapi.com/users/{userId}`);
const user = await data.json();
cache[userId] = user;
return user;
}
}
这是我们的函数,它看起来与第一个例子非常相似。接下来,让我们看看如何使用它。
// get access to the cache
const getUser = memoizeUser();
// add a click event listener to a button which gets a user’s profile
// this button will have an id of the users id that it accesses
document.querySelector('#getUserButton').addEventListener('click', async (e) => {
const userId = e.target.id;
// have we visited this user before?
const userData = await getUser(userId);
// rest of function which returns users profile using the
// userData retrieved above
});
当用户个人资料被点击时,我们会从按钮获取用户 ID,然后调用getUser函数返回用户数据。这将触发 API,除非我们之前访问过该用户个人资料时缓存了相应的数据。在这种情况下,无需调用服务器,我们可以直接从缓存中获取数据。
很简单吧?这涵盖了记忆的基础知识。
是时候更上一层楼了
如果你想真正聪明一点,你甚至可以将繁重的计算函数传递给闭包本身,它可以接受可变数量的参数。
const memoize = (fn) => {
let cache = {};
return (...args) => {
// as this now takes variable arguments, we want to create a unique key
// you would need to define this hash function yourself
const key = hash(args);
if (!cache[key]) {
cache[key] = fn(...args);
}
return cache[key];
}
}
// some functions we can pass to memoize
const add = (num1, num2) => num1 + num2;
const subtract = (num1, num2) => num1 - num2;
// these 2 will have different cache objects thanks to closures
const add2Numbers = memoize(add);
const subtract2Numbers = memoize(subtract);
const result1 = add2Numbers(10, 20);
const result2 = add2Numbers(20, 30);
const result3 = subtract2Numbers(10, 5);
很酷吧?我们可以定义这个 memoize 包装器,并向其传递多个函数,每个函数接受可变数量的参数。
一些应该做的和不应该做的事
记忆什么时候有用?
- 从 API 检索固定数据时。
- 当执行对于给定输入可能定期重复发生的苛刻计算时。
何时不使用记忆
- 从数据定期变化的 API 检索数据时。
- 简单的函数调用。
总结
- 记忆化是一种缓存形式,用于存储所需函数的结果。
- 这是一种简单的技术,可以轻松地实现到现有的代码库中以提高性能。
- 在处理固定数据 API 和经常发生的繁重计算功能时,记忆化很有用。