正文之前,我们先阐明几个概念:

  • 允诺(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 的诡异现象。