非同步程式設計解決方案全集—promise、generator+co、async+await
傳統的解決程式碼單執行緒執行的方案是 回撥函式和事件
。這是個解決問題的方案,但是會造成回撥地獄。
非同步程式設計是優化程式碼邏輯提高程式碼易讀性的關鍵。
目前通用的非同步程式設計方法有三種:
- Promise
- generator+co
- async+await
這三種方法我都經常在用,但是對它們的原理卻一知半解。於是想炒個冷飯從頭到尾理一遍,梳理一下它們之間的關係。
二、Promise
2.1 原理
Promise物件是一個建構函式,用來生成Promise例項。
Promise物件代表一個非同步操作,有三種狀態: pending
(進行中)、 fulfilled
(已成功)和 rejected
(已失敗)。
Promise函式的兩個引數分別是 resolve
和 reject
。它們是Promise中定義的兩個函式,在執行自定義函式時返回。
resolve
函式將Promise物件的狀態從 pending
變為 resolved
, reject
將Promise物件的狀態從 pending
變為 rejected
Promise的原型鏈上定義了then方法,提供兩個回撥函式分別捕獲resolve、reject返回的值。
2.2 靜態方法
方法 | 描述 |
---|---|
Promise.resolve(promise); | 返回 promise(僅當 promise.constructor == Promise 時) |
Promise.resolve(thenable); | 從 thenable 中生成一個新 promise。thenable 是具有 then() 方法的類似於 promise 的物件。 |
Promise.resolve(obj); | 在此情況下,生成一個 promise 並在執行時返回 obj。 |
Promise.reject(obj); | 生成一個 promise 並在拒絕時返回 obj。為保持一致和除錯之目的(例如堆疊追蹤), obj 應為 instanceof Error。 |
Promise.all(array); | 生成一個 promise,該 promise 在陣列中各項執行時執行,在任意一項拒絕時拒絕。 |
Promise.race(array); | 生成一個 Promise,該 Promise 在任意項執行時執行,或在任意項拒絕時拒絕,以最先發生的為準。 |
sample 1
let p1 = new Promise((resolve,reject)=>{ console.log('hello') setTimeout(function () { reject('1212') },1000) }) p1.then(data=> { console.log('success'+data) },err=>{ console.log('err'+err) }) p1.then(data=> { console.log('success'+data) },err=>{ console.log('err'+err) }) 複製程式碼
terminal:
hello err1212 err1212 複製程式碼
sample 1 中新建了一個Promise例項,定時1S後使用reject方法,將Promise例項的狀態從pending變成rejected,觸發then的err捕捉回撥函式。
在sample 1 中呼叫then方法,並不會馬上執行回撥。是等待例項中狀態改變後才會執行。這一點和 釋出訂閱
模式很類似。
sample 2
let fs = require('fs') let event = { arr:[], result:[], on(fn){ this.arr.push(fn) }, emit(data){ this.result.push(data) this.arr.forEach(fn=>fn(this.result)) } } event.on(function (data) { if(data.length === 2){ console.log(data) } }) fs.readFile('1.txt','utf8',function (err,data) { event.emit(data) }) fs.readFile('2.txt','utf8',function (err,data) { event.emit(data) }) 複製程式碼
smaple2 中將結果data放入暫存陣列中,在執行接聽函式的時候返回。
2.3 簡寫Promise原始碼
通過之前的例子和對釋出訂閱模式的理解,我們可以大概寫出Promise例項的基本功能:
code 1:
function Promise(executor) { let self = this self.value = undefined self.reason = undefined self.status = 'pending' self.onResovedCallbacks = [] self.onRejectedCallbacks = [] function resolve(data) { if(self.status === 'pending'){ self.value = data self.status = 'resolved' self.onResovedCallbacks.forEach(fn=>fn()) } } function reject(reason) { if(self.status === 'pending') { self.reason = reason self.status = 'reject' self.onRejectedCallbacks.forEach(fn=>fn()) } } //如果函式執行時發生異常 try{ executor(resolve,reject) }catch (e){ reject(e) } } Promise.prototype.then = function (onFulfilled,onRejected) { let self = this if(self.status === 'pending'){ self.onResovedCallbacks.push(()=>{ onFulfilled(self.value) }) self.onRejectedCallbacks.push(()=>{ onRejected(self.reason) }) }else if(self.status === 'resolved'){ onFulfilled(self.value) }else if(self.status === 'reject'){ onRejected(self.reason) } } module.exports = Promise 複製程式碼
- 函式內部變數
- status:儲存Promise的狀態
- onResovedCallbacks:儲存Promise pending狀態下成功回撥函式
- onRejectedCallbacks:儲存Promise pending狀態下失敗回撥函式
- resolve函式
- reject函式
- Promise.prototype.then
- 根據例項狀態執行響應的回撥
- status == pending使用釋出訂閱模式儲存回撥函式。
2.4 Promise用法簡述
- 如果一個promise執行完後,返回的還是一個Promise物件,會把這個promise的執行結果,傳遞給下一個then中。
let fs = require('fs') function read(filePath,encoding) { return new Promise((resolve,reject)=>{ fs.readFile(filePath,encoding,(err,data)=> { if(err) reject(err) resolve(data) }) }) } read('1.txt','utf8').then( f1=>read(f1,'utf8') // 1 ).then( data=> console.log('resolved:',comments) err=> console.log('rejected: ',err) ) 複製程式碼
- 如果then中返回的不是promise,是一個普通值,會將這個普通值作為下一個then的返回結果。
...... read('1.txt','utf8').then( f1=>read(f1,'utf8') ).then( return 123 //2 ).then( data=> console.log('resolved:',comments) err=> console.log('rejected: ',err) ) 複製程式碼
- 如果當前then中失敗了會走下一個then的失敗。
...... read('1.txt','utf8').then( f1=>read(f1,'utf8') ).then( return 123 ).then( throw new Error('出錯') //3 ).then( data=> console.log('resolved:',comments) err=> console.log('rejected: ',err) ) 複製程式碼
- 如果返回的是undefined不管當前是失敗還是成功,都會走下一次成功。
- catch是錯誤沒有處理的情況下會走。
- then中可以不寫。
...... read('1.txt','utf8').then( f1=>read(f1,'utf8') ).then( return 123 ).then( throw new Error('出錯') ).then() //6 .then( data=> console.log('resolved:',comments) err=> console.log('rejected: ',err) ) 複製程式碼
這些用法中最重要的是promise的then鏈式呼叫。 可以大致猜到,舊Promise的then方法返回的是一個新的Promise物件。
參考Promises/A+規範,可以完善手寫的Promise原始碼使其支援promise的靜態方法和呼叫規則。
code 2:
function Promise(executor) { let self = this self.value = undefined self.reason = undefined self.status = 'pending' self.onResovedCallbacks = [] self.onRejectedCallbacks = [] function resolve(value) { if (self.status === 'pending') { self.value = value self.status = 'resolved' self.onResovedCallbacks.forEach(fn=>fn()) } } function reject(reason) { if (self.status === 'pending') { self.reason = reason self.status = 'rejected' self.onRejectedCallbacks.forEach(fn=>fn()) } } //如果函式執行時發生異常 try { executor(resolve, reject) } catch (e) { reject(e) } } function resolvePromise(promise2, x, resolve, reject) { //If promise and x refer to the same object, reject promise with a TypeError as the reason. if (promise2 === x) { return reject(new TypeError('chaining cycle')) } let called //2.3.3.Otherwise, if x is an object or function, if (x !== null && (typeof x == 'object' || typeof x === 'function')) { try { let then = x.then //2.3.3.3.If then is a function, call it with x as this, first argument resolvePromise, and second argument rejectPromise, where: //2.3.3.3.3.If both resolvePromise and rejectPromise are called, or multiple calls to the same argument are made, the first call takes precedence, and any further calls are ignored. if (typeof then === 'function') { then.call(x, y=> { if (called) return; called = true; //遞迴直到解析成普通值為止 //2.3.3.1.If/when resolvePromise is called with a value y, run [[Resolve]](promise, y). resolvePromise(promise2, y, resolve, reject) }, err=> { if (called) return; called = true; reject(err) }) } else { resolve(x) } } catch (e) { if (called) return; called = true; //2.3.3.3.If retrieving the property x.then results in a thrown exception e, reject promise with e as the reason. reject(e) } } else { //If x is not an object or function, fulfill promise with x. resolve(x) } } //then呼叫的時候 都是非同步呼叫 (原生的then的成功或者失敗 是一個微任務) Promise.prototype.then = function (onFulfilled, onRejected) { //成功和失敗的函式 是可選引數 onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : val=>val; onRejected = typeof onRejected === 'function' ? onRejected : (e)=> {throw e}; let self = this let promise2; promise2 = new Promise((resolve, reject)=> { if (self.status === 'resolved') { setTimeout(()=> { try { let x = onFulfilled(self.value) resolvePromise(promise2, x, resolve, reject) } catch (e) { reject(e) } }, 0) } else if (self.status === 'rejected') { setTimeout(()=> { try { let x = onRejected(self.reason) resolvePromise(promise2, x, resolve, reject) } catch (e) { reject(e) } }, 0) } else if (self.status === 'pending') { self.onResovedCallbacks.push(()=> { setTimeout(()=> { try { let x = onFulfilled(self.value) resolvePromise(promise2, x, resolve, reject) } catch (e) { //當執行成功回撥的時候,可能會出現異常,那就用這個異常作為promise2的錯誤結果 reject(e) } }, 0) }) self.onRejectedCallbacks.push(()=> { setTimeout(()=> { try { let x = onRejected(self.reason) resolvePromise(promise2, x, resolve, reject) } catch (e) { reject(e) } }, 0) }) } }) return promise2 } //setTimeout (規範要求) Promise.reject = function (reason) { return new Promise((resolve,reject)=>{ reject(reason) }) } Promise.resolve = function (value) { return new Promise((resolve,reject)=>{ resolve(value) }) } Promise.prototype.catch = function (onReject) { return this.then(null,onReject) } Promise.defer = Promise.deferred = function () { let dfd = {} dfd.promise = new Promise((resolve, reject)=> { dfd.resolve = resolve dfd.reject = reject }) return dfd; } module.exports = Promise 複製程式碼
- 為了支援then的鏈式呼叫,Promise.then.prototype中返回一個新的Promise物件
return p2 = new Promise() 複製程式碼
2.增加resolvePromise方法,處理舊Promise的回撥函式的結果x,根據x的型別,分別呼叫新promise物件的resolve/reject方法。
- 是普通值用resolve方法返回
- 是函式或者物件就繼續用resolvePromise方法迭代(解決回撥函式是Promise物件)
- 出錯就用reject方法返回
三、bluebird
1: NodeJS 中的 fs.readFile 方法的基本使用方式
const fs = require('fs'),path = require('path'); fs.readFile(path.join(__dirname, '1.txt'), 'utf-8', (err, data) => { if (err) { console.error(err); } else { console.log(data); } }); 複製程式碼
2:使用Promise封裝
let fs = require('fs') function read(filePath, encoding) { return new Promise((resolve, reject)=> { fs.readFile(filePath, encoding, (err, data)=> { if (err) reject(err) resolve(data) }) }) } read('1.txt', 'utf8').then( data=> data) 複製程式碼
把fs.readFile方法用Promise封裝一下就能使用Promise api。但是每次手動封裝比較麻煩,bluebird可以幫我們簡化這個步驟。
3:在 NodeJS 環境中,通過 const bluebird = require('bluebird') 就可以開始使用 Bluebird 提供的 Promise 物件。
Promise.promisify 將單個方法轉換成Promise物件。
const bluebird = require('bluebird') let read = bluebird.promisify(fs.readFile) read('1.txt', 'utf-8').then(data=> { console.log('data promisify', data) }) 複製程式碼
使用 bluebird.promisify
方法,就能將fs.readFile直接封裝成一個promise物件,它的原理很簡單,return new Promise 是它的核心:
function promisify(fn) { return function () { return new Promise((resolve, reject)=> { fn(...arguments, function (err, data) { if (err) reject(err) resolve(data) }) }) } } 複製程式碼
4.使用 Promise.promisifyAll 把一個物件的所有方法都自動轉換成使用 Promise。
const bluebird = require('bluebird'), fs = require('fs'), path = require('path'); Promise.promisifyAll(fs); fs.readFileAsync(path.join(__dirname, 'sample.txt'), 'utf-8') .then(data => console.log(data)) .catch(err => console.error(err)); 複製程式碼
promisifyAll核心是遍歷物件,生成些新建立方法的名稱在已有方法的名稱後加上"Async"字尾。
function promisifyAll(obj) { Object.keys(obj).forEach(key=>{ if(typeof obj[key] === 'function'){ obj[key+'Async'] = promisify(obj[key]) } }) } 複製程式碼
四、generator+co
4.1 簡介
generator函式最大的特點是可以用 yield
暫停執行,為了區別普通函式在函式名前加*號。
function *say() { let a = yield "test1" let b = yield "test2" } let it = say(); console.log(1, it.next()) //1 { value: 'test1', done: false } console.log(2, it.next()) //2 { value: 'test2', done: false } console.log(3, it.next()) //3 { value: undefined, done: true } 複製程式碼
執行say()方法返回的是指標物件,不會返回函式執行結果。it 就是iterator 迭代器
需要呼叫指標物件的next()方法,讓函式指標不斷移動並返回一個物件。({value:xxx,done:xxx})
value是yield後面的值,done表示函式是否執行完成。
我們可以用generator函式實現結果的產出,但是也需要它支援輸入。
generator函式的執行順序如下:

使用it.next()執行函式,結果並不會返回給定義的變數a。next方法可以接受引數,這是向 Generator 函式體內輸入資料。 第二個next的時候傳入引數,就能被變數a接收到。
terminal 返回:
1 { value: 'test1', done: false } aaa 2 { value: 'test2', done: false } bbb 3 { value: undefined, done: true } 複製程式碼
4.2 使用
example:使用generator非同步執行函式,使函式的返回作為下一個函式的入參執行。
let bluebird = require('bluebird') let fs = require('fs') let read = bluebird.promisify(fs.readFile) function *r() { let r1 = yield read('1.txt', 'utf-8') console.log('r1',r1); // r1 2.txt let r2 = yield read(r1, 'utf-8') console.log('r2',r2); // r2 3.txt let r3 = yield read(r2, 'utf-8') console.log('r3',r3); // r3 hello return r3 } 複製程式碼
拿讀取檔案的例子:使用bluebird將fs.readFile變成promise物件,將讀取到的檔案內容作為入參傳入下一個要執行的函式。
突然發現,要拿到結果會是個複雜的過程,但還是硬著頭皮下下去:
const it_r = r() it_r.next().value.then(d1=>{ return it_r.next(d1).value }).then(d2=>{ return it_r.next(d2).value }).then(d3=>{ return it_r.next(d3).value }).then(data=>{ console.log(data) // hello }) 複製程式碼
it.next().value 返回的是一個promise,使用then方法,拿到它成功回撥的值,並傳入下一個next。
這樣能成功拿到我們要的值,但是太麻煩了。於是就有了generator+co的組合!
安裝:
$ npm install co 複製程式碼
使用:
co(r()).then(data=> { console.log(data) }) 複製程式碼
co會迭代執行it.next()方法,直到done的布林值為true就返回generator函式的執行結果。
大致執行程式碼如下:
function co(it) { return new Promise((resolve, reject)=> { function next(data) { let {value, done} = it.next(data) if(done){ resolve(value) }else{ value.then(data=> { next(data) },reject) } } next() }) } 複製程式碼
五、async+await
async 函式是Generator 函式的語法糖。
比Generator函式用起來簡單
- 可以讓程式碼像同步
- 可以try+catch
- 可以使用promise api
async function r() { try{ let r1 = await read('1.txt','utf8') let r2 = await read(r1,'utf8') let r3 = await read(r2,'utf8') return r3 }catch(e){ console.log('e',e) } } r().then(data=> { console.log(data) },err=>{ console.log('err',err) }) 複製程式碼
async 函式返回一個 Promise 物件,可以使用 then 方法添加回調函式。遇到await就會先返回,等待函式執行。
參考
-
ofollow,noindex">使用 bluebird 實現更強大的 Promise