Async javascript is much more fun when you memoize.
当你进行 memoize 时,Async javascript 会变得更有趣。
Here’s something resembling an interview question. The question isn’t very well defined, but it serves point… so bear with me.
这里有一个类似面试的问题。这个问题的定义不是很清楚,但它是有意义的......所以请耐心听我说。
Let’s take the following interface for making soup (mmmmm):
我们来看看下面的煲汤界面(嗯嗯):
async function getSoupRecipe(<soupType>)
async function hireSoupChef(<soupRecipe:requiredSkills>)
async function buySoupPan()async function makeSoup(<soupChef>, <soupRecipe>, <soupPan>)
The rules are: 规则是
- Provide me with some code for making my favorite soup.
为我提供一些制作我最喜欢的汤的代码。 - To make soup you need a recipe, a chef, and a soup pan.
做汤需要食谱、厨师和汤锅。 - The above code is a loose interface for a few methods which should get you what you need to make the soup
上面的代码是几个方法的松散接口,应该可以满足制作汤的需要 - Mess around with the interface, if it makes for better code
如果能编写出更好的代码,就在界面上做文章
OK, so let’s get started and churn out some of these methods.
好了,让我们开始使用这些方法吧。
async function getSoupRecipe(soupType) {
return await http.get(`/api/soup/${soupType}`);
}
async function buySoupPan() {
return await http.get(`/api/soupPan`);
}
async function hireSoupChef(requiredSkills) {
return await http.post(`/api/soupChef/hire`, {
requiredSkills: requiredSkills
});
}
async function makeSoup(soupChef, soupRecipe, soupPan) {
return await http.post(`api/makeSoup`, {
soupRecipe, soupPan, soupChef
});
}
Great, that was straightforward… but oh crap, how difficult is it to actually call these, if I want some soup..?! Let’s make it easier and add another method to orchestrate all of this:
很好,这很简单......但糟糕的是,如果我想喝汤,实际调用这些方法有多难?让我们简单一点,再添加一个方法来协调这一切:
async function makeSoupFromType(soupType) {
let soupRecipe = await getSoupRecipe(soupType);
let soupPan = await buySoupPan();
let soupChef = await hireSoupChef(soupRecipe.requiredSkills);
return await makeSoup(soupChef, soupRecipe, soupPan);
}
OK, great, so we’re done? Not quite.
好了,很好,我们说完了吗?还没呢
This code works, but it’s just going to cycle through every step one by one. We’re really hungry for our soup, so let’s try to make it a bit faster. Surely there’s stuff we can do in parallel?
这段代码是有效的,但它会一个步骤一个步骤地循环。我们真的很想喝我们的汤,所以让我们试着让它更快一点。我们肯定能并行处理一些事情吧?
As it happens, yes! The soup pan doesn’t actually have any dependencies on anything else being ready first — so I can buy it in parallel while I’m fetching the recipe:
事实上,是的!汤锅实际上并不依赖于其他任何东西先准备好,所以我可以在买食谱的同时买汤锅:
async function makeSoupFromType(soupType) {
let [soupRecipe, soupPan] = await Promise.all([
getSoupRecipe(soupType),
buySoupPan()
]); let soupChef = await hireSoupChef(soupRecipe.requiredSkills);
return await makeSoup(soupChef, soupRecipe, soupPan);
}
Great, this is a bit faster, but … now my code is pretty tightly coupled with what-happens-when. I’m making things way more imperative, by explicitly stating ‘these two methods must run in parallel’ and ‘this method must run afterwards’. Gee, I hope I don’t ever have to maintain this code in future.
很好,这样速度快了一些,但是......现在我的代码与 "什么时候发生什么事情 "的耦合非常紧密。通过明确说明 "这两个方法必须并行运行 "和 "这个方法必须在之后运行",我让事情变得更加势在必行。天哪,我希望以后再也不用维护这些代码了。
Oh wait, yes I do, because I didn’t even get it right. Hiring my soup chef really only needs a recipe, but before I even start the hiring process, I’m waiting for the soup pan, and that could take days…
哦,等等,我是这么想的,因为我甚至都没弄对。雇我的汤厨师其实只需要一份食谱,但在我开始雇人之前,我还在等汤锅,而这可能需要好几天......
I’m still going to be waiting way longer than I need to get my soup. Ho hum. OK then, let’s take this to the logical extreme and try to fix the code again:
我还是要等比我需要的时间更长的时间才能喝到汤。哼。好吧,让我们把这个问题推向逻辑的极致,再次尝试修复代码:
async function makeSoupFromType(soupType) {
let soupRecipePromise = getSoupRecipe(soupType);
async function hireSoupChefWithSoupRecipe(_soupRecipePromise) {
let soupRecipe = await _soupRecipePromise;
return await hireSoupChef(soupRecipe.requiredSkills);
}
let [ soupRecipe, soupPan, soupChef ] = await Promise.all([
soupRecipePromise,
buySoupPan(),
hireSoupChefWithSoupRecipe(soupRecipePromise)
]);
return await makeSoup(soupChef, soupRecipe, soupPan);
}
Well, cool, it’s as fast as it can possibly be… but that’s really about the only good thing that can be said about this code.
好吧,很酷,它已经快到了极致......但这确实是这个代码唯一的优点了。
- Since I don’t want to get my soup recipe twice, I’m having to explicitly cache the promise for it. Before now, I didn’t even have to really think about promises, now I have one hanging around in my code where it’s not welcome.
由于我不想两次获得汤配方,所以我必须明确地缓存promise。在此之前,我甚至都不需要真正考虑promises,而现在我的代码中却有一个不受欢迎的promises。 - At this point, to exacerbate the problem I had before, my code really cares what order things happen in… I literally had to define a new inner function to ensure the only thing blocking hiring my soup chef was retrieving the recipe.
在这一点上,我之前遇到的问题更加严重了,我的代码真的很在意事情发生的顺序......我真的不得不定义一个新的内部函数,以确保唯一阻碍我的汤厨师的事情就是检索配方。 - This method takes a lot of effort to read and understand, and probably even more to maintain and change in future. What if someone absent-mindedly adds an `await` to that cached promise, thinking I’ve missed it? Everything gets slow again…
这种方法需要花费大量精力去阅读和理解,将来的维护和更改可能会花费更多精力。如果有人心不在焉地给缓存的promise添加了一个 "等待",以为我错过了,那该怎么办?一切又会变得缓慢起来......
So how do we fix this? Well, consider the problem we have in the above method. We needed to get a soup recipe, but we didn’t want to fetch it twice, so we had to manually cache it ourselves.
那么,我们该如何解决这个问题呢?好吧,考虑一下我们在上述方法中遇到的问题。我们需要获取一份汤食谱,但又不想获取两次,所以只能自己手动缓存。
There’s a better way. Let’s take the functional approach, and use memoization instead:
还有更好的办法。让我们采用功能性方法,用 memoization 来代替:
function memoize(method) {
let cache = {};
return async function() {
let args = JSON.stringify(arguments);
cache[args] = cache[args] || method.apply(this, arguments);
return cache[args];
};
}
This will turn any async function into a memoized one — that is, if it’s called twice with the same arguments, it will return the same cached value the second time.
这将把任何异步函数变成 memoized 函数,也就是说,如果使用相同的参数调用该函数两次,它将在第二次返回相同的缓存值。
Notice, by the way, how we’re not using `await` in our memoize function— the thing we’re actually caching is the promise returned by the method, not the final value. This means that we don’t even need to wait for the async method to return anything, before we cache it’s future value. Awesome!
顺便注意一下,我们在 memoize 函数中没有使用 `await`- 我们实际缓存的是方法返回的promise,而不是最终值。这意味着在缓存其未来值之前,我们甚至不需要等待异步方法返回任何内容。太棒了
Now let’s re-implement our original methods using this… and let’s change around that interface a little, for good measure. What if everything just takes `soupType` as a parameter?
现在,让我们用它来重新实现我们原来的方法......为了方便起见,让我们稍微改变一下接口。如果所有方法都只将 `soupType` 作为参数呢?
let getSoupRecipe = memoize(async function(soupType) {
return await http.get(`/api/soup/${soupType}`);
});
let buySoupPan = memoize(async function() {
return await http.get(`/api/soupPan`);
});
let hireSoupChef = memoize(async function(soupType) {
let soupRecipe = await getSoupRecipe(soupType)
return await http.post(`/api/soupChef/hire`, {
requiredSkills: soupRecipe.requiredSkills
});
});
let makeSoup = memoize(async function(soupType) {
let [ soupRecipe, soupPan, soupChef ] = await Promise.all([
getSoupRecipe(soupType), buySoupPan(), hireSoupChef(soupType)
]);
return await http.post(`api/makeSoup`, {
soupRecipe, soupPan, soupChef
});
});
Cool! Now I can safely call any of these methods, at any time, and be guaranteed that I won’t double-invoke any of my http apis. Remember that we’re caching/memoizing promises? And promises can only resolve a single time? That means after the first call, I’ll always get a cached result, even if I call one of these methods again before it has returned for the first time.
太酷了现在,我可以在任何时候安全地调用这些方法中的任何一个,并保证不会重复调用任何 http apis。还记得我们正在缓存/记忆promises吗?promises只能解析一次?这意味着在第一次调用之后,即使我在第一次返回之前再次调用其中一个方法,我也将始终得到一个缓存结果。
All of these methods now only require `soupType` as a parameter, making them super easy to integrate and call elsewhere in my codebase. In fact, I don’t even need to define a `makeSoupFromType` method any more, to chain everything together: `makeSoup` already does the job for me.
现在,所有这些方法都只需要将 `soupType` 作为参数,这使得它们可以非常容易地集成到我的代码库中,并在其他地方调用。事实上,我甚至不需要再定义一个 `makeSoupFromType` 方法,就能把所有东西串联起来:makeSoup` 已经帮我完成了这项工作。
Moreover, there’s now no more explicit code defining what order these functions need to run in. They fetch their own dependencies, and are automatically run in the fastest possible order.
此外,现在不再需要明确的代码来定义这些函数的运行顺序。它们会获取自己的依赖关系,并自动以最快的顺序运行。
I don’t even have to think through the problem any more… I can just look forward to my soup. Woot.
我甚至不用再思考问题了......我只需期待着喝汤。太棒了