正文之前,我們先闡明幾個概念:

  • 允諾(Promise):想必不需要解釋。這個概念貌似缺乏一個約定俗成的中文翻譯,這裏就現造一個罷。
  • 允諾式(Promisive):基於允諾的異步編程方式,包括允諾鏈和異步函數等;一個對立面是回調式。
  • 允諾化(Promisify):將非允諾式的程序或 API 轉化爲允諾式。

引子

   允諾(Promise)是現代異步編程的基礎與核心,而 NodeJS 的設計早於允諾。 因此,其大量的異步 API 皆爲回調式。不進行允諾化(Promisify)改造,我們就無法方便地使用。 理想情況是官方修改這些 API 的行爲,當沒有回調函數傳入時,返回一個允諾。 然而這取決於官方的態度,由於這將破壞向後兼容,他們自然是要周全考慮,不可能輕易改動。 作爲普通用戶,我們只有自己動手了。

   一個方案是將需要用的 API 進行再次封裝,鑑於異步 API 龐大的數量, 我們只能用到哪些封裝哪些。但這並不令人滿意。當我們用到新的 API 又得重新封裝; 並且,我們被迫要額外記憶封裝後的另一套 API;每個項目,每個開發者,又有不同的封裝方案。

   最好是能用一個精簡的方式構造一個通用的解決方案。比如如下的方案:

定義

function Do( api, ...args )
{
	return new Promise(
		( resolve, reject, )=> api(
			...args,
			( err, response, )=> {
				if( err )
					reject( err, );
				else
					resolve( response, );
			},
		),
	);
}

function Promisify( module, )
{
	const cache= {};
	
	return new Proxy(
		module,
		{
			get( target, key, receiver, ){
				if(!( Reflect.has( target, key, ) ))
					return undefined;
				
				return cache[key] || (
					cache[key]= ( ...args )=> Do(
						Reflect.get( target, key, receiver, ),
						...args,
					)
				);
			},
		},
	);
}

用法:

// Do
const fs= require( 'fs', );


Do( fs.readFile, '/path/to/file', 'utf-8', ).then( /* ... */ );

await Do( fs.readdir, '/path/to/directory', );


// Promisify
const pfs= Promisify( fs );


pfs.readFile( '/path/to/file', 'utf-8', ).then( /* ... */ );

await pfs.readdir( '/path/to/directory', );

設計思路

   分析 NodeJS 的異步 API 規範爲:不論接受幾個參數,最後一個參數爲回調函數, 並且回調函數所接受的第一個參數統一爲錯誤,第二個參數爲接口的響應。 據此設計通用的 Do 函數。

Do 函數的用法略顯彆扭。如果我們只使用某個模塊中的異步函數,就可將整個模塊允諾化, 提供更順手的 API。可以使用一個原模塊的代理(Proxy)對象,攔截接口的訪問, 並返回封裝層,通過 Do 函數訪問接口。這裏對封裝層作了緩存,目的是使每一次訪問都獲取到同一個函數, 避免出現 pfs.readFile !== pfs.readFile 的詭異現象。