我在第一話中介紹了異步的概念、事件循環、以及JS編程中可能的3種異步狀況(用戶交互、I/O、定時器)。在編寫異步操做代碼時,最直接、也是每一個JSer最早接觸的寫法必定是回調函數(callback),好比下面這位段代碼:html
ajax('www.someurl.com', function(res) { doSomething(); ... });
Ajax請求是一種I/O操做,每每須要較長時間來完成,爲了避免阻塞單線程的JS程序,故設計爲異步操做。此處,將一個匿名函數做爲參數傳給ajax,意思是「這個匿名函數先放你那兒,但暫不執行,須在收到response以後,再回過頭來調用這個函數」,所以這個匿名函數也被稱爲「回調」。這樣的寫法相信每一個JSer都再熟悉不過了,但仔細想一想,這種寫法可能有什麼問題?程序員
問題就出在「控制反轉」。ajax
匿名函數的代碼,完徹底全是我寫的。可是,這段代碼什麼時候被調用、調用幾回、調用時傳入什麼參數……等等,我卻沒法掌握;而原本是被我所調用的ajax函數,竟冠冕堂皇地接管了個人代碼,回調的控制權旁落到了寫ajax函數的那傢伙手裏——控制被反轉了。編程
不少狀況下,「那傢伙」是個很是可信的機構或公司(好比Google的Chrome團隊)、或是比你我牛得多的天才程序員,所以能夠放心地把回調交給他。但也有不少狀況下,事情並不是如此:假如你在開發一個電商網站的代碼,把「刷一次信用卡」的回調傳給一個第三方庫,而那個庫很不巧地在某種特殊狀況下把這個回調調用了5次,那麼,你的老闆可能不得不作好準備,在電話中親自安撫怒氣衝衝的顧客。並且,即便換一個第三方合做夥伴,就能保證再也不出相似的問題嗎?緩存
換句話說,咱們沒法100%信任接管回調的第三方(固然,那個「第三方」也多是本身)。異步
另外一個問題是,異步操做本質上是沒法保證完成時間的,所以,當多個異步操做須要按前後順序依次執行、而且後面的步驟依賴於前面步驟的返回結果時,若是用回調的寫法,就只能把後一個的步驟硬編碼在前一個步驟的回調中,整個操做流程造成一個嵌一個的回調金字塔,再加上異常處理和多分支等狀況,口味更加酸爽:函數
ajax(url, function (res){ ajax(res.url, function(res) { ajax(res.url, function(res) { if (res.status == '1') { ajax(res.url, function(res) { ... } } else if (res.status == '2') { ajax(url2, function(res) { ... } ... } } } );
這樣的流程是極其脆弱的,並且包含大量重複卻沒法複用的代碼,體驗很是糟心。oop
面對愈來愈複雜的業務場景,簡單的回調已經愈來愈力不從心,更好的解決方案在哪兒呢?性能
也許咱們能夠嘗試換一種模式:不是把回調的控制權交出去,而是讓異步操做在返回時觸發一個事件,通知主線程異步操做的結果,隨後主線程根據預先的設定執行事件相應的回調,這就是「事件訂閱模式」。在這種模式下,原本要被反轉的回調控制權又被反轉回來了,所以稱爲「反控制反轉」。僞代碼以下:學習
on('ajax_return', function(val) { doSomething(); });
ajax(url, function(res) { emitEvent('ajax_return', res); });
on()是假想的用於註冊事件回調的函數,emitEvent()是假想的用於觸發事件的函數。
這種模式解決了控制反轉的問題,並且用ES5也能輕鬆實現。可是,它尚未很好地解決異步流程的問題——總不能爲每個異步操做都單獨註冊一個事件吧?不管如何,事件訂閱模式給咱們提供了十分有益的啓示,接下來上場的主角正是以這種模式爲基礎設計的。
Promise是一種範式,專治異步操做的各類疑難雜症。本節不打算逐一介紹Promise的API,而是着重探求其設計思想,由此學習其正確的使用方法。
第一,Promise基於事件訂閱模式。咱們知道,Promise有三種狀態:未決議、決議、拒絕。從未決議變化到決議或拒絕,就至關於觸發了一個匿名事件,使得經過then方法註冊的fulfilled或rejected回調被調用,實現了反控制反轉。
第二,Promise「只能決議一次」的特性,使得「裸回調」和不可信的thenable對象均可以包裝爲可信的Promise對象。示例代碼以下:
// 例1.將ajax函數的返回結果Promise化 let p1 = new Promise((resolve, reject) => { ajax(url, function(res) { if (res.error) reject(res.error); resolve(res); }); }); // 例2.將不規範的thenable對象Promise化 let obj = { then: function(cb, errcb) { cb(1); cb(2); // 不合規範的用法! errcb('evil laugh'); } }; let p2 = new Promise((resolve, reject) => { obj.then(resolve, reject); });
// 或寫成以下語法糖
let p2 = Promise.resolve(obj);
例1中,傳給ajax的匿名函數不知道會被調用幾回,然而因爲Promise的特性,保證了只有第一次調用會使Promise的狀態發生決議,以後的調用都被直接忽略。
例2中,obj對象有一個then方法,接受兩個函數做爲參數,因此它是一個thenable對象;可是其內部的代碼卻徹底不符合Promise規範——"fulfilled"被調用了兩次,"rejected"也在resolve時被調用,徹底是亂來嘛!可是,只要把它包裝成p2,那就沒有問題了——resolve(1)順利執行,resolve(2)和reject('evil laugh')被直接忽略。
第三,then方法註冊的回調必定會被異步調用,好比:
console.log('A'); Promise.resolve('B').then(console.log); console.log('C');
執行結果是 A C B。
這是爲了將如今值(同步)和將來值(異步)歸一化,避免出現Zalgo現象(指同一個操做既可能同步返回也可能異步返回,好比緩存命中則同步返回、未命中則異步返回)。
再看一段代碼:
setTimeout(function(){console.log('A');}, 0); setTimeout(function(){console.log('B');}, 0); Promise.resolve('C').then(console.log);
Promise.resolve('D').then(console.log); console.log('E');
執行結果爲 E C D A B。
緣由在於,Promise的then回調實現異步不是用setTimeout(.., 0),而是用一種叫作Job Queue(任務隊列)的專門機制。傳統的setTimeout(.., 0)把回調放在Event Loop的末尾,做爲一個新的event老老實實排隊;而Job Queue是Event Loop中每一個event後面掛着的一個隊列,往這個隊列裏插入回調,能夠搶在下個event以前執行,至關於「插隊」,所以Promise一旦決議,能夠以最快的速度(在當前同步代碼執行完以後,馬上)調用回調,沒有別的異步可以搶在前面(除了另外一個Promise)!
第四,then方法會返回一個新的Promise,以fulfilled回調爲其resolve,以rejected回調爲其reject,所以連續調用then方法能夠構成一條Promise鏈。因爲鏈上的Promise決議有前後順序(別忘了,每一步都是異步的),所以能夠用來控制異步操做的順序。固然,通常狀況下同步操做就不要強行異步化了,我見過p.then(res=>res.text).then(...)這樣的代碼,除了增長程序複雜度之外好像沒什麼用處。。。
從以上幾點能夠看出,Promise是一種很是強大的模式,對於異步操做中可能遇到的信任問題、硬編碼流程問題等,都設計了相應的機制來加以克服,試着正確地瞭解它、使用它,你必定能體會到它的好處,從而愛不釋手。可是,探尋更優雅的異步操做方法的任務,尚未結束……
推薦閱讀:《你不知道的JavaScript·中卷》第二部分:異步和性能