JS 中最基礎的非同步呼叫方式是callback,它將回呼函數callback 傳給非同步API,由瀏覽器或Node 在非同步完成後,通知JS 引擎呼叫callback。對於簡單的非同步操作,用 callback 實現,是夠用的。但隨著負責互動頁面和 Node 出現,callback 方案的弊端開始浮現。 Promise 規範孕育而生,並納入 ES6 的規範中。後來 ES7 又在 Promise 的基礎上將 async 函數納入標準。此為 JavaScript 非同步進化史。
通常,程式碼是由上往下依序執行的。如果有多個任務,就必須排隊,前一個任務完成,後一個任務才會執行。這種執行模式稱之為:同步(synchronous)。新手容易把計算機用語中的同步,和日常用語中的同步混淆。如,「把檔案同步到雲端」的同步,指的是「使...保持一致」。而在計算機中,同步指的是任務從上往下依序執行的模式。例如:
A(); B(); C();
在這段程式碼中,A、B、C是三個不同的函數,每個函數都是一個不相關的任務。在同步模式,電腦會先執行 A 任務,再執行 B 任務,最後執行 C 任務。在大部分情況,同步模式都沒問題。但如果 B 任務是耗時很長的網路請求,而 C 任務恰好是展現新頁面,就會導致網頁卡頓。
更好解決方案是,將 B 任務分成兩個部分。一部分立即執行網路請求的任務,另一部分在請求回來後的執行任務。這種一部分立即執行,另一部分在未來執行的模式稱為非同步。
A(); // 在现在发送请求 ajax('url1',function B() { // 在未来某个时刻执行 }) C(); // 执行顺序 A => C => B
實際上,JS 引擎並沒有直接處理網路請求的任務,它只是調用了瀏覽器的網路請求接口,由瀏覽器發送網路請求並監聽返回的資料。 JavaScript 非同步能力的本質是瀏覽器或 Node 的多執行緒能力。
未來執行的函數通常也叫 callback。使用 callback 的非同步模式,解決了阻塞的問題,但也帶來了一些其他問題。在最開始,我們的函數是從上往下書寫的,也是從上往下執行的,這種「線性」模式,非常符合我們的思維習慣,但是現在卻被 callback 打斷了!在上面一段程式碼中,現在它跳過 B 任務先執行了 C任務!這種非同步「非線性」的程式碼會比同步「線性」的程式碼,更難閱讀,因此也更容易滋生 BUG。
試著判斷下面這段程式碼的執行順序,你會對「非線性」程式碼比「線性」程式碼更難閱讀,體會更深。
A(); ajax('url1', function(){ B(); ajax('url2', function(){ C(); } D(); }); E(); // A => E => B => D => C
這段程式碼中,從上往下執行的順序被 Callback 打亂了。我們的閱讀代碼視線是A => B => C => D => E,但是執行順序是A => E => B => D => C,這就是非線性程式碼帶來的糟糕之處。
透過將ajax後面執行的任務提前,可以更容易看懂程式碼的執行順序。雖然程式碼因為嵌套看起來不美觀,但現在的執行順序卻是從上到下的「線性」方式。這種技巧在寫多重巢狀的程式碼時,是非常有用的。
A(); E(); ajax('url1', function(){ B(); D(); ajax('url2', function(){ C(); } }); // A => E => B => D => C
上一段程式碼只有處理了成功回調,並沒有處理異常回呼。接下來,把異常處理回呼加上,再來討論程式碼「線性」執行的問題。
A(); ajax('url1', function(){ B(); ajax('url2', function(){ C(); },function(){ D(); }); },function(){ E(); });
加上異常處理回呼後,url1的成功回呼函數 B 和異常回呼函數 E,被分開了。這種「非線性」的情況又出現了。
在 node 中,為了解決的異常回呼導致的「非線性」的問題,制定了錯誤優先的策略。 node 中 callback 的第一個參數,專門用來判斷是否發生異常
A(); get('url1', function(error){ if(error){ E(); }else { B(); get('url2', function(error){ if(error){ D(); }else{ C(); } }); } });
到此,callback 造成的「非線性」問題基本上已解決。遺憾的是,使用 callback 嵌套,一層層if else和回呼函數,一旦嵌套層數多起來,閱讀起來不是很方便。此外,callback 一旦出現異常,只能在目前回呼函數內部處理異常。
在 JavaScript 的非同步進化史中,湧現出一系列解決 callback 弊端的函式庫,而 Promise 成為了最終的勝者,並成功地被引入了 ES6 中。它將提供了一個更好的「線性」書寫方式,並解決了非同步異常只能在當前回呼中被捕獲的問題。
Promise 就像一個中介,它承諾會將一個可信任的非同步結果回傳。首先 Promise 和非同步介面簽訂一個協議,成功時,呼叫resolve函數通知 Promise,異常時,呼叫reject通知 Promise。另一方面 Promise 和 callback 也簽訂一個協議,由 Promise 在將來返回可信任的值給then和catch中註冊的 callback。
// 创建一个 Promise 实例(异步接口和 Promise 签订协议) var promise = new Promise(function (resolve,reject) { ajax('url',resolve,reject); }); // 调用实例的 then catch 方法 (成功回调、异常回调与 Promise 签订协议) promise.then(function(value) { // success }).catch(function (error) { // error })
Promise 是个非常不错的中介,它只返回可信的信息给 callback。它对第三方异步库的结果进行了一些加工,保证了 callback 一定会被异步调用,且只会被调用一次。
var promise1 = new Promise(function (resolve) { // 可能由于某些原因导致同步调用 resolve('B'); }); // promise依旧会异步执行 promise1.then(function(value){ console.log(value) }); console.log('A'); // A B (先 A 后 B) var promise2 = new Promise(function (resolve) { // 成功回调被通知了2次 setTimeout(function(){ resolve(); },0) }); // promise只会调用一次 promise2.then(function(){ console.log('A') }); // A (只有一个) var promise3 = new Promise(function (resolve,reject) { // 成功回调先被通知,又通知了失败回调 setTimeout(function(){ resolve(); reject(); },0) }); // promise只会调用成功回调 promise3.then(function(){ console.log('A') }).catch(function(){ console.log('B') }); // A(只有A)
介绍完 Promise 的特性后,来看看它如何利用链式调用,解决异步代码可读性的问题的。
var fetch = function(url){ // 返回一个新的 Promise 实例 return new Promise(function (resolve,reject) { ajax(url,resolve,reject); }); } A(); fetch('url1').then(function(){ B(); // 返回一个新的 Promise 实例 return fetch('url2'); }).catch(function(){ // 异常的时候也可以返回一个新的 Promise 实例 return fetch('url2'); // 使用链式写法调用这个新的 Promise 实例的 then 方法 }).then(function() { C(); // 继续返回一个新的 Promise 实例... }) // A B C ...
如此反复,不断返回一个 Promise 对象,再采用链式调用的方式不断地调用。使 Promise 摆脱了 callback 层层嵌套的问题和异步代码“非线性”执行的问题。
Promise 解决的另外一个难点是 callback 只能捕获当前错误异常。Promise 和 callback 不同,每个 callback 只能知道自己的报错情况,但 Promise 代理着所有的 callback,所有 callback 的报错,都可以由 Promise 统一处理。所以,可以通过catch来捕获之前未捕获的异常。
Promise 解决了 callback 的异步调用问题,但 Promise 并没有摆脱 callback,它只是将 callback 放到一个可以信任的中间机构,这个中间机构去链接我们的代码和异步接口。
异步(async)函数是 ES7 的一个新的特性,它结合了 Promise,让我们摆脱 callback 的束缚,直接用类同步的“线性”方式,写异步函数。
声明异步函数,只需在普通函数前添加一个关键字 async 即可,如async function main(){} 。在异步函数中,可以使用await关键字,表示等待后面表达式的执行结果,一般后面的表达式是 Promise 实例。
async function main{ // timer 是在上一个例子中定义的 var value = await timer(100); console.log(value); // done (100ms 后返回 done) } main();
异步函数和普通函数一样调用 main() 。调用后,会立即执行异步函数中的第一行代码 var value = await timer(100) 。等到异步执行完成后,才会执行下一行代码。
除此之外,异步函数和其他函数基本类似,它使用try...catch来捕捉异常。也可以传入参数。但不要在异步函数中使用return来返回值。
var timer = new Promise(function create(resolve,reject) { if(typeof delay !== 'number'){ reject(new Error('type error')); } setTimeout(resolve,delay,'done'); }); async function main(delay){ try{ var value1 = await timer(delay); var value2 = await timer(''); var value3 = await timer(delay); }catch(err){ console.error(err); // Error: type error // at create (<anonymous>:5:14) // at timer (<anonymous>:3:10) // at A (<anonymous>:12:10) } } main(0);
异步函数也可以被当作值,传入普通函数和异步函数中执行。但是在异步函数中,使用异步函数时要注意,如果不使用await,异步函数会被同步执行。
async function main(delay){ var value1 = await timer(delay); console.log('A') } async function doAsync(main){ main(0); console.log('B') } doAsync(main); // B A
这个时候打印出来的值是 B A。说明 doAsync 函数并没有等待 main 的异步执行完毕就执行了 console。如果要让 console 在 main 的异步执行完毕后才执行,我们需要在main前添加关键字await。
async function main(delay){ var value1 = await timer(delay); console.log('A') } async function doAsync(main){ await main(0); console.log('B') } doAsync(main); // A B
由于异步函数采用类同步的书写方法,所以在处理多个并发请求,新手可能会像下面一样书写。这样会导致url2的请求必需等到url1的请求回来后才会发送。
var fetch = function (url) { return new Promise(function (resolve,reject) { ajax(url,resolve,reject); }); } async function main(){ try{ var value1 = await fetch('url1'); var value2 = await fetch('url2'); conosle.log(value1,value2); }catch(err){ console.error(err) } } main();
使用Promise.all的方法来解决这个问题。Promise.all用于将多个Promise实例,包装成一个新的 Promis e实例,当所有的 Promise 成功后才会触发Promise.all的resolve函数,当有一个失败,则立即调用Promise.all的reject函数。
var fetch = function (url) { return new Promise(function (resolve,reject) { ajax(url,resolve,reject); }); } async function main(){ try{ var arrValue = await Promise.all[fetch('url1'),fetch('url2')]; conosle.log(arrValue[0],arrValue[1]); }catch(err){ console.error(err) } } main();
目前使用 Babel 已经支持 ES7 异步函数的转码了,大家可以在自己的项目中开始尝试。
以上就是JavaScript 异步进化史的内容,更多相关内容请关注PHP中文网(www.php.cn)!