本文的例子用 JavaScript 語法給出,但願讀者至少有使用過 Promise 的經驗,若是用過 async/await 則更好,對於客戶端的開發者,我相信語法不是閱讀的瓶頸,思惟纔是,所以也能夠了解一下異步編程模型的演變過程。javascript
CPS 的全稱是 (Continuation-Passing Style),這個名詞聽上去比較高大上(背後涉及到不少數學方面的東西),實際上若是隻是想了解什麼是 CPS 的話,並非太難。java
咱們看下面這段代碼,你確定會以爲太簡單了:python
function sum(a, b) {
return a + b;
}
int a = sum(1, 2); // 第一行業務代碼
console.log(a); // 第二行業務代碼複製代碼
隱藏在這兩行代碼背後的是串行編程的思想,也就是說第一行代碼執行出結果之後纔會執行第二行代碼。編程
可若是 sum
這個函數耗時比較久怎麼辦呢,通常咱們不會選擇等待它執行完,而是提供一個回調,在執行完耗時操做之後再執行回調,同時避免阻塞主線程:數組
function asum(a, b, callback) {
const r = a + b;
setTimeout(function () {
callback(r);
}, 0);
}
asum(1, 2, r => console.log(r));複製代碼
因而,業務方不用等待 asum
的返回結果了,如今它只要提供一個回調函數。這種寫法就叫作 CPS。promise
CPS 能夠總結爲一個很重要的思想: 「我不用等執行結果,我先假設結果已經有了,而後描述一下如何利用這個結果,至於調用的時機,由結果提供方負責管理」。babel
扯了這麼多 CPS,其實我想說的是,不少介紹 Promise 的文章上來就談 CPS,更有甚者直接聊起了 CPS 的背後數學模型。實際上 CPS 對異步編程沒什麼卵用,主要是它的概念太廣泛,太容易理解了,我敢打賭幾乎全部的開發者都或多或少的用過 CPS。網絡
畢竟回調函調每一個人都用過,只不過你不必定知道這是 CPS 而已。好比隨便舉一個 AFNetworking 中的例子:數據結構
NSURLSessionDataTask *dataTask = [manager dataTaskWithRequest:request completionHandler:^(NSURLResponse *response, id responseObject, NSError *error) {
if (error) {
NSLog(@"Error: %@", error);
} else {
NSLog(@"%@ %@", response, responseObject);
}
}];複製代碼
寫過 JavaScript 的人應該都接觸過 Promise,首先明確一個概念,Promise 是一些列規範的總稱,現有的規範有 Promise/A、Promise/B、Promise/A+ 等等,每一個規範都有本身的實現,固然也能夠本身提供一個實現,只要能知足規範中的描述便可。閉包
寫過 Promise 或者 RAC/RxSwift 的讀者估計對一長串 then
方法記憶深入,不知道你們是否思考過,爲何會設計這種鏈式寫法呢?
我固然不想聽到什麼「方法調用之後還返回本身」這種廢話,要能反覆調用 then 方法必然要返回同一個類的對象啊。。。要想搞清楚爲何要這麼設計,或者爲何能夠這麼設計,咱們先來看看傳統的 CPS(基於回調) 寫法如何處理嵌套的異步事件。
若是我須要請求第一個接口,而且用這個接口返回的數據請求下一個接口,那代碼看起來大概是這樣的:
request(url1, parms1, response => {
// 處理 response
request(url2, params2, response => {
// 處理第二個接口的數據
})
})複製代碼
上述代碼用僞代碼寫起來看上去還能接受,不過能夠參考 OC 的繁瑣代碼,試想一個雙層嵌套就已經如此麻煩, 三層嵌套該怎麼寫是好呢?
咱們抽象一下上面的邏輯,CPS 的含義是不直接等待異步數據返回,而是傳入一個回調函數來處理將來的數據。換句話講:
回調事件是一個普通事件,內部可能還會發起一個異步事件
這種世界觀的好處在於,經過事件的嵌套造成了一套遞歸模型,理論上可以解決任意多層的嵌套。固然缺點也是顯而易見的,語義上的嵌套最終致使了代碼上的嵌套,影響了可讀性和可維護性。
這種嵌套模型能夠用下面這幅圖來表示:
能夠看到圖中只有兩種圖形,橢圓形表示通常性事件(回調也是一個事件),而圓角矩形表示一個異步過程,當執行完之後,就會接着執行它鏈接着的事件。
固然,咱們是有辦法解決嵌套問題的,俗話說得好:
任何計算機問題均可以經過添加一箇中間層來解決
而 Promise 的本質則是下面這幅圖:
能夠看到,咱們引入了新的 Promise 層,一個 Promise 內部封裝了異步過程,和異步過程結束之後的回調。若是這個回調的內部能夠生成一個新的 Promise。因而嵌套模型就變成了鏈式模型,這也是爲何咱們常常能看到 then
方法的調用鏈。
須要強調的是,即便你用了 Promise,也能夠在回調函數中直接執行異步過程,這樣就回到了嵌套模型。因此 Promise 的精髓實際上在於回調函數中返回一個新的 Promise 對象。
數據結構學得好的讀者看到上面這幅圖應該會想到鏈表。不過一個 Promise 內部能夠持有多個新的 Promise,因此採用的不是鏈表結構而是有些相似於多叉樹。簡化版的 Promise 定義以下:
function Promise(resolver) {
this.state = PENDING;
this.value = void 0;
this.queue = []; // 持有接下來要執行的 promise
if (resolver !== INTERNAL) {
safelyResolveThen(this, resolver);
}
}複製代碼
對一個 Promise 對象調用 then
方法,其實是判斷 Promise 的狀態是否仍是 PENDING
,若是是的話就生成一個新的 Promise 保存在數組中。不然直接執行 then 方法參數中 block。
當一個 Promise 內部執行完之後,好比說是進入了 FULLFILLED
狀態,就會遍歷本身持有的全部的 Promise 並告訴他們也去執行 resolve
方法,進入 REJECTED
狀態也是同理。
若是可以理解這層思想,你就能夠理解爲何有先後關係順序的幾個異步事件能夠用 then
這種同步寫法串聯了。由於調用 then
其實是預先保留了一個回調,只有當上一個 Promise 結束之後纔會通知到下一個 Promise。
關於 Promise 的實現原理,這篇文章不想描述太多,感興趣的讀者能夠參考 深刻 Promise(一)——Promise 實現詳解,讀完之後能夠看一下做者的後續文章中的四個題目,檢驗一下是否真的理解了: 深刻 Promise(二)——進擊的 Promise。
這裏我只想強調一下幾個容易理解錯的地方。首先,Promise 會接受一個函數做爲本身的參數,也就是下面代碼中的 fucntion (resolve, reject){ /* do something */ }
:
var p = new Promise(function (resolve, reject) {
resolve('hello');
});
console.log(ppppp);
// 打印出 Promise { 'hello' } 而不是 Promise { 'pedding' }
// 證實 Promise 已經在建立時就決議複製代碼
在建立 Promise 時,這個參數函數就會被執行, 執行這個函數須要兩個參數 resoleve
和 reject
,它並非經過 then
方法提供而是由 Promise 在內部本身提供,換句話說這兩個參數是已知的。
所以若是按照上述代碼來寫, 在建立 Promise 時就會馬上調用 resolve('hello')
,而後把狀態標記爲 FULLFILLED
而且讓內部的 value
值爲 "hello"
。這樣後來執行 then
的時候會判斷到 Promise 已經決議,直接把 value
的值放到 then 的閉包中,並且這個過程是異步執行(參考文章中 immediate 的使用)。
有的文章會談到 Promise 的錯誤處理,實際上這裏沒有什麼高深的學問或者黑科技。若是在 Promise 內部調用 setTimeout
異步的拋出錯誤,外面仍是接不到。
Promise 處理錯誤的原則是提供了一個 reject
回調,而且用 reject
方法來代替拋出錯誤的作法。這樣作至關於約定了一套錯誤協議,把錯誤直接轉嫁到業務方的邏輯中。
另外一個須要重點理解的是 then
方法提供的閉包中,返回的內容,由於這纔是鏈式模型的核心。
在 Promise 內部的 doResolve
方法中會有如下關鍵判斷:
var then = getThen(value);
if (then) {
safelyResolveThen(self, then);
} else {
self.state = FULFILLED;
self.value = value;
self.queue.forEach(function (queueItem) {
queueItem.callFulfilled(value);
});
}複製代碼
所以若是這裏的 value 不是基本類型,就會從新走一遍 safelyResolveThen
,至關於從新解一遍 Promise 了。
因此正確的異步嵌套邏輯應該是:
var p = new Promise(function (resolve, reject) {
resolve('hello');
})
p.then(value => {
console.log(value);
return new Promise(function (resolve, reject) {
resolve('world')
});
}).then(value => {
console.log(value);
});
// 第一行打印出 hello
// 第二行打印出 world複製代碼
咱們先看一個 Python 中的例子,如何打印斐波那契數列的前五個元素:
def fab(max):
n, a, b = 0, 0, 1
while n < max:
print b
a, b = b, a + b
n = n + 1複製代碼
得益於 Python 簡潔的語法,函數實現僅用了六行代碼:
不過缺點在於, 每次調用函數都會打印全部數字,不能實現按需打印:
for n in fab(5):
print n複製代碼
咱們先不考慮爲何 fab(5)
能放在 in
關鍵字後面,至少能分次打印就意味着咱們須要一個對象,內部保存上一次的結果,這樣才能正確的生成下一個值。
感興趣的讀者能夠用對象來實現一下上述需求, 而且對比一下引入對象後帶來的複雜度增長。一種既不增長複雜度,也能保留上下文的技術是使用生成器,只須要修改一個單詞便可:
def fab(max):
n, a, b = 0, 0, 1
while n < max:
yield b #原來是 print b
a, b = b, a + b
n = n + 1複製代碼
yield
關鍵字的含義是 當外界調用 next 方法時生成器內部開始執行,直到遇到 yield 關鍵字,此時把 yield 後面的值傳遞出去做爲 next() 的結果,而後繼續執行函數,直到再次遇到 yield 方法時暫停。
上面舉 Python 的例子是由於生成器在 Python 中最爲簡單,最好理解。在 JavaScript 中,生成器的概念稍微複雜一點,主要涉及兩個變化。
next()
方法能夠傳遞參數,在生成器內部表現爲 yield 的返回值。舉個例子:
function* generator(count) {
console.log(count);
const result = yield 100
console.log(result + count);
}
const g = generator(2); // 什麼都不輸出
console.log(g.next().value); // 第一次打印 2,隨後打印 100
g.next(9); // 打印 11複製代碼
逐行解釋一下:
generator
時,生成器並無執行,因此什麼都沒有輸出。g.next
時,函數開始執行,打印 2
,遇到 yield,拿到了 yield 生成的內容,也就是 100,傳遞給 next()
的調用結果,因此第二行打印 100。next()
方法,生成器內部恢復執行,因爲 next()
方法傳入參數 9,因此 result
的值是 9,第三行打印 11。可見 JavaScript 中的生成器經過 yield value
和next(value)
實現了值的內外雙向傳遞。
我不知道 Generator 在 JavaScript 和 Python 中的實現原理,然而用 Objective-C 確實能夠模擬出來。考慮到生成器內部 運行 -> 等待 -> 恢復運行 的特色,信號量是最佳的實現方案。
yield
實際上就是信號量的 wait
方法,而 next()
實際上就是信號量的 signal
方法。固然還要處理好數據的交互問題。總的來講思路仍是比較清晰的。
咱們先舉一個例子,看一下 Promise 的使用,每次調用函數 p()
都會生成一個新的 Promise 對象,內部的操做是把參數加一併返回,不妨把函數 p 想象成某個耗時操做。
function p(t) {
return new Promise(function (resolve, reject) {
setTimeout(function () {
resolve(t + 1);
}, t);
});
}複製代碼
假設我須要反覆的、線性的執行這個耗時操做,代碼將是這樣的:
p(0).then( r => {
console.log(r);
return p(r);
}).then( r => {
console.log(r);
return p(r);
}).then( r => {
console.log(r);
return p(r);
});複製代碼
可見咱們調用三次 then
方法,執行了三次加一操做,所以會有三行輸出,分別是 一、二、3。
文章的一開頭就說了,代碼老是線性執行, 遇到異步操做不會進行等待,而是直接設置好回調函數並繼續向後執行。
實際上,若是藉助於 Generator 暫停、恢復的特性,咱們能夠用同步的方式來寫異步代碼。好比咱們先定義一個生成器 linear()
表示內部將要線性執行異步代碼:
function* linear() {
const r1 = yield p(0);
console.log(r1);
const r2 = yield p(r1);
console.log(r2);
const r3 = yield p(r2);
console.log(r3);
}複製代碼
咱們看到 yield 的值是一個 Promise 對象,爲了拿到這個對象,須要調用 g.next().value
。所以爲了讓第一個輸出打印來,代碼是這樣的:
g.next().value.then(value => { // 實際上是 Promise.then 的模式
// 正如上一節 Generator 的例子中所述,第一個 next 會啓動 Generator,而且卡在第一個 yield 上
// 爲了讓程序向後執行,還須要再調用一次 next,其中的參數 0 會賦值給 r1。
g.next(0).value.then()
})複製代碼
如何模擬完整的三個 Promise 調用呢,這要求咱們的代碼不斷向內迭代,同時用一個值保存上一次的結果:
let t = 0;
var g = linear();
g.next().value.then(value => {
t = value;
g.next(t).value.then(value => {
t = value;
g.next(t).value.then(value => {
t = value;
g.next(t)
})
})
})複製代碼
這種寫法的運行結果和以前用 then
語法的運行結果徹底一致。
有的讀者可能會想問,這種寫法徹底沒有看到好處啊,反而像是回退到了最初的模式,各類嵌套不利於代碼閱讀和理解。
然而仔細觀察這段代碼就會發現,嵌套邏輯中更多的是架構邏輯而非業務邏輯,業務邏輯都放在 Promise 內部實現了,所以這裏的複雜代碼其實是能夠作精簡的,它是一個結構高度一致的遞歸模型。
咱們注意到 g.next().value.then
的內部其實是重複了外面的調用過程,如何描述這樣的遞歸呢,有一個小技巧,只要在最外層包一個函數,而後遞歸執行函數就行:
// 遞歸必然要有能夠遞歸的函數,所以咱們在外面包裝一層函數
function recursive() {
g.next(t).value.then(value => {
t = value;
return value;
}).then( result => recursive())
}
recursive();複製代碼
然而有一個問題在於,咱們必須在 recursive()
函數外面建立生成器 g
,不然放在函數內部就會致使遞歸建立新的。所以咱們能夠加一個內部函數處理核心的遞歸問題,而外部函數處理生成器和臨時變量的建立:
function recursive(generator) {
let t; // 臨時變量,用來存儲
var g = linear(); // 建立整個遞歸過程當中惟一的生成器
function _recursive() {
g.next(t).value.then(value => {
t = value;
return value;
}).then(() => _recursive())
}
_recursive();
}
recursive(linear);複製代碼
能夠看到這個 recursive
函數徹底與業務無關,對於任何生成器函數,好比說叫 g,均可以經過 recursive(g)
來進行調用。
這也就經過實際例子簡單的證實了即便是異步事件也能夠採用同步寫法。
須要註明的是,這並非 async/await 語法的真正實現,這種寫法的問題在於,await 外面的每一層函數都要標註爲 async,然而沒辦法把每個函數都轉換成生成器,而後調用 recursive()
感興趣的同窗能夠了解一下 babel 轉換先後的代碼。
標記了 async 的函數返回結果老是一個 Promise 對象,若是函數內部拋出了異常,就會調用 reject 方法並攜帶異常信息。不然就會把函數返回值做爲 resolve 函數的參數調用。
理解了這一點之後,咱們會發現 async/await 實際上是異步操做的向外轉移。
好比說 p 是一個 Promise 對象,咱們可能會這樣寫:
async function test() {
var value = await p;
console.log('value = ' + value);
return value;
}
test().then(value => console.log(value));複製代碼
咱們必定程度上能夠把 test
當作生成器來看:
所以咱們發現異步操做並無消失,也不可能消失,只是從 await
的地方轉移到了外面的 async
函數上。若是這個函數的返回值有用,那麼外部還得使用 await
進行等待,而且把方法標記爲 async
。
因此我的建議在使用 await
關鍵字的時候,首先應該判斷對異步操做的依賴狀況,好比如下場景就很是合適:
async sendRequest(url) {
const response = await fetch(url); // 異步請求網絡
const result = await asyncStore(response); // 獲得結果後異步存儲數據
}複製代碼
考慮到 await
會阻塞執行,若是某個 Promise 後面的代碼任然須要執行(好比存儲、統計、日誌等),則不建議盲目使用 await
:
async function test() {
var s = await fetch(url);
console.log('這裏輸出不了啊');
}複製代碼