2017年五月底Node.js 8正式發(fā)布,帶來了 很多新特性 侦铜。本文討論下util.promisify()這個(gè)方法。
Promise
介紹promisify之前,首先來看下Promise這個(gè)API君纫,因?yàn)閡til.promisify()這個(gè)方法就是把原來的異步回調(diào)方法改成支持 Promise 的方法并返回一個(gè)Promise實(shí)例。ES2015(ES6)加入了 Promise芹彬,可以直接使用蓄髓。Promise沒有新的語法元素,即使在不支持原生Promise的環(huán)境里也可以使用舒帮,比如 Q 或者 Bluebird会喝,甚至 jQuery ,在小程序里有效玩郊。
ES2017 增加了 await/async 語法肢执,但請注意, await 后面必須跟Promise實(shí)例才能實(shí)現(xiàn)異步瓦宜。
由于歷史原因蔚万,js中存在大量異步回調(diào),回調(diào)層數(shù)多的話就形成了“地獄回調(diào)”临庇,不僅代碼丑陋反璃,也很難維護(hù)。針對這種現(xiàn)象假夺,開發(fā)社區(qū)總結(jié)出來一套名為 Promise/A+ 的解決方案淮蜈。大體上來說,這套方案通過使用 “Promise 回調(diào)實(shí)例”包裹原先的回調(diào)函數(shù)已卷,可以將原先復(fù)雜的嵌套展開梧田、鋪平,從而降低開發(fā)和維護(hù)的難度和成本1。下面來自文獻(xiàn)1的代碼非常簡單清晰裁眯、一目了然:
new Promise( (resolve, reject) => { // 構(gòu)建一個(gè) Promise 實(shí)例
someAsyncFunction( (err, result) => { // 調(diào)用原來的異步函數(shù)
if (err) { // 發(fā)生錯(cuò)誤鹉梨,進(jìn)入錯(cuò)誤處理模式
return reject(err);
}
resolve(result); // 一切正常,進(jìn)入隊(duì)列的下一環(huán)節(jié)
});
})
.then( result => { // 下一環(huán)節(jié)
return doSomething(result);
})
.then( result2 => { // 又下一環(huán)節(jié)
return doSomething2(result2);
})
... // 各種中間環(huán)節(jié)
.catch( err => { // 錯(cuò)誤處理
console.log(err);
});
ES6規(guī)定穿稳,Promise對象是一個(gè)構(gòu)造函數(shù)存皂,用來生成Promise實(shí)例。下面代碼創(chuàng)造了一個(gè)Promise實(shí)例逢艘。
var promise = new Promise(function(resolve, reject) {
// ... some code
if (/* 異步操作成功 */){
resolve(value);
} else {
reject(error);
}
});
假設(shè)一個(gè)事件eventFor2Seconds操作耗時(shí)2秒旦袋,那么我們可以使用Promise這樣寫:
function eventFor2Seconds(x) {//eventFor2Seconds方法返回一個(gè)Promise實(shí)例代表一個(gè)異步操作
return new Promise(resolve => {
setTimeout(() => {
resolve(x);
}, 2000);
});
}
async function doSomething() {
var x = await eventFor2Seconds(10);//需要使用await實(shí)現(xiàn)異步
console.log(x); // 2秒后打印 10
}
doSomething();
Promise有以下幾個(gè)特點(diǎn)2:
- Promise對象有三種狀態(tài):Pending(進(jìn)行中)、Resolved(已完成它改,又稱 Fulfilled)和Rejected(已失敯淘小)且對象的狀態(tài)不受外界影響。只有異步操作的結(jié)果央拖,可以決定哪一種狀態(tài)祭阀,任何其他操作都無法改變這個(gè)狀態(tài)。這也是Promise這個(gè)名字的由來爬泥,它的英語意思就是“承諾”柬讨,表示其他手段無法改變。
- 一旦狀態(tài)生成袍啡,就不會(huì)再變踩官,任何時(shí)候都得到這個(gè)狀態(tài)。Promise對象只有兩種可能的狀態(tài)改變方式:從Pending變?yōu)镽esolved和從Pending變?yōu)镽ejected境输。只要這兩種情況發(fā)生蔗牡,狀態(tài)就凝固了,不會(huì)再變了嗅剖,會(huì)一直保持這個(gè)結(jié)果辩越。就算改變已經(jīng)發(fā)生了,你再對Promise對象添加回調(diào)函數(shù)信粮,也會(huì)立即得到這個(gè)結(jié)果黔攒。這與事件(Event)完全不同,事件的特點(diǎn)是强缘,如果你錯(cuò)過了它督惰,再去監(jiān)聽,是得不到結(jié)果的旅掂。
- Promise所有API都返回當(dāng)前實(shí)例(就是builder設(shè)計(jì)模式)赏胚,因此可以采用連續(xù)的then/catch鏈?zhǔn)讲僮鱽韺懟卣{(diào)。
- resolve方法會(huì)使之后的連續(xù)then執(zhí)行(不寫then也沒事)商虐,reject方法會(huì)使之后的catch執(zhí)行(如果不寫catch會(huì)出現(xiàn)異常觉阅,因此catch必須寫)崖疤。
- 可以在then中return出數(shù)據(jù),并且這個(gè)數(shù)據(jù)會(huì)以參數(shù)的形式傳入下一個(gè)then典勇。
- Promise 對象的錯(cuò)誤具有“冒泡”性質(zhì)劫哼,會(huì)一直向后傳遞,直到被捕獲為止割笙。也就是說沦偎,錯(cuò)誤總是會(huì)被下一個(gè)catch語句捕獲。
Promise對象提供統(tǒng)一的接口咳蔚,包括resolve,reject搔驼,then谈火,catch,all舌涨,race糯耍,使得異步流程控制更加方便。
Promise.all()
Promise.all方法用于將多個(gè)Promise實(shí)例囊嘉,包裝成一個(gè)新的Promise實(shí)例温技。var p = Promise.all([p1, p2, p3]);
all()接受數(shù)組作為參數(shù)。p1,p2,p3都是Promise的實(shí)例對象扭粱,p要變成Resolved狀態(tài)需要p1舵鳞,p2,p3狀態(tài)都是Resolved琢蛤,如果p1,p2,p3至少有一個(gè)狀態(tài)是Rejected蜓堕,p的狀態(tài)就變成Rejected(很像&&符號(hào)鏈接)
Promise.race()
var p = Promise.race( [p1,p2,p3] )
上面代碼中,只要p1博其、p2套才、p3之中有一個(gè)實(shí)例首先改變狀態(tài),p的狀態(tài)就跟著改變慕淡。那個(gè)首先改變的Promise 實(shí)例的返回值背伴,就傳遞給p的回調(diào)函數(shù)(很像||符號(hào)操作)
var p1 = new Promise(function(resolve, reject) {
setTimeout(resolve, 500, "one");
});
var p2 = new Promise(function(resolve, reject) {
setTimeout(resolve, 100, "two");
});
Promise.race([p1, p2]).then(function(value) {
console.log(value); // "two"
// Both resolve, but p2 is faster
});
Promise resolve()
resolve可以將將現(xiàn)有對象轉(zhuǎn)為Promise對象。
Promise.resolve('fa')
// 等價(jià)于
new Promise(resolve => resolve('fa'))
Promise reject()
Promise.reject(reason)方法也會(huì)返回一個(gè)新的 Promise 實(shí)例峰髓,該實(shí)例的狀態(tài)為rejected傻寂。
Promise.reject('foo')
// 等價(jià)于
new Promise(reject => reject('foo'))
util.promisify
Promise的用法差不多就這些了,下面來看下util.promisify的用法儿普。我的理解是util.promisify只是返回一個(gè)Promise實(shí)例來方便異步操作崎逃。比如要延遲一段時(shí)間執(zhí)行代碼,我們可以這樣:
let { promisify } = require('util')
const sleep = promisify(setTimeout)
async function fuc() {
console.log('before')
await sleep(2000)
console.log('after')
}
fuc()
上面的代碼結(jié)合while(1)死循環(huán)可以實(shí)現(xiàn)一個(gè)簡單的定時(shí)器功能眉孩。
大家都知道nodejs的fs庫有讀文件的API个绍,結(jié)合util.promisify使用鏈?zhǔn)讲僮鞔娴鬲z回調(diào):
const util = require('util');
const fs = require('fs');
const stat = util.promisify(fs.stat);
stat('.')
.then((stats) => {
// Do something with `stats`
})
.catch((error) => {
// Handle the error.
});
在綁定的函數(shù)的參數(shù)列表中的最后會(huì)多出一個(gè)參數(shù)勒葱,這個(gè)參數(shù)是函數(shù)而且包含兩個(gè)參數(shù)為 (err, result),前面是可能的錯(cuò)誤巴柿,后面是正常的結(jié)果凛虽。這個(gè)多出來的參數(shù)是promisify在綁定的時(shí)候強(qiáng)制添加的作為默認(rèn)的回調(diào)函數(shù),這個(gè)默認(rèn)的回調(diào)函數(shù)源碼如下:
(err, ...values) => {
if (err) {
promiseReject(promise, err);
} else if (argumentNames !== undefined && values.length > 1) {
const obj = {};
for (var i = 0; i < argumentNames.length; i++)
obj[argumentNames[i]] = values[i];
promiseResolve(promise, obj);
} else {
promiseResolve(promise, values[0]);
}
}
然后再看一個(gè)例子:
function paramObj(params, callback) {//假設(shè)業(yè)務(wù)需求這個(gè)函數(shù)需要傳入一個(gè)對象參數(shù)params才是正常的广恢,否則就表示異常
console.log('params', params)
console.log('callback', callback)
if (typeof params != 'object') {
params(JSON.stringify({ "code": "1", "msg": "params null" }), null)//拋出異常凯旋,原因是params null
} else {
callback(null, params)//promisify會(huì)添加一個(gè)自己風(fēng)格的類型為function的參數(shù)
}
}
async function test() {//async會(huì)返回一個(gè)Promise實(shí)例
var data = await promisify(paramObj).bind(paramObj)()//不傳入一個(gè)對象的參數(shù)的話會(huì)拋出異常
console.log('inner data: ', data)
return data
}
test()
.then(data => {
console.log('outer data:', data)
})
.catch(err => {
console.log('outer err:', err)
})