【第2723期】高階 Promise 模式:Promise快取

語言: CN / TW / HK

前言

Promise 還可以這樣。今日前端早讀課文章由 @ConardLi 翻譯,公號:code 祕密花園授權。

正文從這開始~~

在本文中,我們將介紹常見的快取實現在併發條件下存在的問題。然後我們將介紹如何修復它,並且在此過程中簡化程式碼。

我們將通過介紹 基於 Singleton Promise 模式 的 Promise Memoization 模式來做到這一點。

一個例子:快取非同步請求結果

下面是一個簡單的 API 客戶端:

const getUserById = async (userId: string): Promise<User> => {
   const user = await request.get(`http://users-service/${userId}`);
   return user;
 };

非常簡單。

但是,如果要關注效能,該怎麼辦?users-service 解析使用者詳細資訊可能很慢,也許我們經常使用相同的使用者 ID 集來呼叫此方法。

我們可能要新增快取,該怎麼做?

簡單的解決方案

const usersCache = new Map<string, User>();

 const getUserById = async (userId: string): Promise<User> => {
   if (!usersCache.has(userId)) {
     const user = await request.get(`http://users-service/${userId}`);
     usersCache.set(userId, user);
   }

   return usersCache.get(userId);
 };

這非常簡單:在從 users-service 中解析了使用者詳細資訊之後將結果填充到記憶體中的快取中。

併發場景

上面的程式碼,它將在以下情況下進行重複的網路呼叫:

await Promise.all([
   getUserById('user1'),
   getUserById('user1')
 ]);

問題在於直到第一個呼叫解決後,我們才分配快取。但是,等等,如何在獲得結果之前填充快取?

如果我們快取結果的 Promise 而不是結果本身,該怎麼辦?程式碼如下:

const userPromisesCache = new Map<string, Promise<User>>();

 const getUserById = (userId: string): Promise<User> => {
   if (!userPromisesCache.has(userId)) {
     const userPromise = request.get(`http://users-service/v1/${userId}`);
     userPromisesCache.set(userId, userPromise);
   }

   return userPromisesCache.get(userId)!;
 };

非常相似,但是我們沒有 await 發出網路請求,而是將其 Promise 放入快取中,然後將其返回給呼叫方。

注意,我們不需要宣告我們的方法 async ,因為它不再呼叫 await 。我們的方法簽名雖然沒有改變我們仍然返回一個 promise ,但是我們是同步進行的。

這樣可以解決併發條件,無論時間如何,當我們對進行多次呼叫時,只會觸發一個網路請求 getUserById('user1') 。這是因為所有後續呼叫者都收到與第一個相同的 Promise 單例。

Promise 快取

從另一個角度看,我們的最後一個快取實現實際上只是在記憶 getUserById!給定我們已經看到的輸入後,我們只返回儲存的結果(恰好是一個 Promise)。

因此,記住我們的非同步方法可以使我們在沒有競爭條件的情況下進行快取。

藉助 lodash,我們可以將最後一個解決方案簡化為:

import _ from 'lodash';

 const getUserById = _.memoize(async (userId: string): Promise<User> => {
   const user = await request.get(`http://users-service/${userId}`);
   return user;
 });

我們採用了原始的無快取實現,並放入了 _.memoize 包裝器,非常簡潔。

錯誤處理

對於 API 客戶端,你應考慮操作可能失敗的可能性。如果我們的記憶體實現已快取了被拒絕的 Promise ,則所有將來的呼叫都將以同樣的失敗 Promise 被拒絕!

幸運的是,memoizee (http://www.npmjs.com/package/memoizee) 庫支援此功能。我們的最後一個示例變為:

import memoize from 'memoizee';

 const getUserById = memoize(async (userId: string): Promise<User> => {
   const user = await request.get(`http://users-service/${userId}`);
   return user;
 }, { promise: true});

關於本文
譯者:@ConardLi
譯文:http://mp.weixin.qq.com/s/sDyQh06eGrjdqAcV8XTKoQ
作者:@Jon Mellman
原文:http://www.jonmellman.com/posts/promise-memoization

關於【Promise】相關推薦,歡迎讀者自薦投稿,前端早讀課等你來

【第2720期】高階非同步模式 - Promise 單例

【第1956期】JavaScript視覺化:Promise和Async/Await