眾所周知javascript是單執行緒的,它的設計之初是為瀏覽器設計的GUI程式語言,GUI程式設計的特性之一是保證UI執行緒一定不能阻塞,否則體驗不佳,甚至介面卡死。
所謂的單執行緒就是一次只能完成一個任務,其任務的排程方式就是排隊,這就和火車站洗手間門口的等待一樣,前面的那個人沒有搞定,你就只能站在後面排隊等著。
這種模式的好處是實現起來簡單,執行環境相對單純,壞處就是隻要有一個任務耗時很長,後面的任務都會必須排隊等著,會拖延整個程式的執行。常見的瀏覽器無響應(假死),往往就是因為某一段Javascript程式碼長時間執行(比如死迴圈),導致了整個頁面卡在這個地方,其他任務無法執行。
為了解決這個問題,Javascript語言將任務的執行模式分成兩種:同步(Synchronous)和非同步(Asynchronous)。
“同步”就是上面所說的,後面的任務等待上一個任務結束,然後再執行。
什麼是“非同步”?
所謂非同步簡單說就是一個任務分成兩段,先執行一段,轉而執行其他任務,等做好了準備轉而執行第二段。
以下是當有ABC三個任務,同步或非同步執行的流程圖:
同步
thread ->|----A-----||-----B-----------||-------C------|
複製程式碼
非同步:
A-Start ---------------------------------------- A-End
| B-Start ----------------------------------------|--- B-End
| | C-Start -------------------- C-End | |
V V V V V V
thread-> |-A-|---B---|-C-|-A-|-C-|--A--|-B-|--C--|---A-----|--B--|
複製程式碼
"非同步"非常重要。在瀏覽器端,耗時很長的操作都應該非同步執行,避免瀏覽器失去響應,最好的例子就是Ajax操作。在伺服器端,"非同步模式"甚至是唯一的模式,因為執行環境是單執行緒的,如果允許同步執行所有http請求,伺服器效能會急劇下降,很快就會失去響應。
本文簡單梳理總結了JavaScript非同步函式的發展歷史如下圖:
- 回撥函式
- Promise
- Generator+co
- async,await
回撥函式Callbacks
似乎一切應該從回撥函式開始談起。
非同步JavaScript
在Javascript 中,非同步程式設計方式只能通過JavaScript中的一等公民函式才能完成:這種方式意味著我們可以將一個函式作為另一個函式的引數,在這個函式的內部可以呼叫被傳遞進來的函式(即回撥函式)。
這也正是回撥函式誕生的原因:如果你將一個函式作為引數傳遞給另一個函式(此時它被稱為高階函式),那麼在函式內部, 你可以呼叫這個函式來完成相應的任務。
回撥函式沒有返回值(不要試圖用return),僅僅被用來在函式內部執行某些動作。
看下面的例子:
step1(function (value1) {
step2(value1, function(value2) {
step3(value2, function(value3) {
step4(value3, function(value4) {
// Do something with value4
});
});
});
});
複製程式碼
這裡只是做4步,巢狀了4層回撥,如果更多步驟呢?顯然這樣的程式碼只是寫起來比較爽但是缺點也很多。
過度使用回撥函式所會遇到的挑戰:
- 如果不能合理的組織程式碼,非常容易造成回撥地獄(callback hell),這會使得你的程式碼很難被別人所理解。
- 不能捕獲異常 (try catch 同步執行,回撥函式會加入佇列,無法捕獲錯誤)
- 無法使用return語句返回值,並且也不能使用throw關鍵字。
也正是基於這些原因,在JavaScript世界中,一直都在尋找著能夠讓非同步JavaScript開發變得更簡單的可行的方案。這個時候就出現了promise,它解決了上述的問題。
Promise
Promise 的最大優勢是標準化,各類非同步工具庫都按照統一規範實現,即使是async函式也可以無縫整合。所以用 Promise 封裝 API 通用性強,用起來簡單,學習成本低。
一個Promise代表的是一個非同步操作的最終結果。
Promise意味著[許願|承諾]一個還沒有完成的操作,但在未來會完成的。與Promise最主要的互動方法是通過將函式傳入它的then方法從而獲取得Promise最終的值或Promise最終拒絕(reject)的原因。要點有三個:
- 遞迴,每個非同步操作返回的都是promise物件
- 狀態機:三種狀態轉換,只在promise物件內部可以控制,外部不能改變狀態
- 全域性異常處理
1)定義
var promise = new Promise(function(resolve, reject) {
// do a thing, possibly async, then…
if (/* everything turned out fine */) {
resolve("Stuff worked!");
}
else {
reject(Error("It broke"));
}
});
複製程式碼
每個Promise定義都是一樣的,在建構函式裡傳入一個匿名函式,引數是resolve和reject,分別代表成功和失敗時候的處理。
2) 呼叫
promise.then(function(text){
console.log(text)// Stuff worked!
return Promise.reject(new Error('我是故意的'))
}).catch(function(err){
console.log(err)
})
複製程式碼
它的主要互動方式是通過then函式,如果Promise成功執行resolve了,那麼它就會將resolve的值傳給最近的then函式,作為它的then函式的引數。如果出錯reject,那就交給catch來捕獲異常就好了。
我們可以通過呼叫promise的示例,瞭解一下propmise的一些原理及特性:
普通呼叫例項:
let fs = require('fs');
let p = new Promise(function(resolve,reject){
fs.readFile('./1.txt','utf8',(err,data)=>{
err?reject(err):resolve(data);
})
})
p.then((data)=>{console.log(data)},(err)=>{console.log(err)});
複製程式碼
1.promise例項可以多次呼叫then方法:
p.then((data)=>{console.log(data)},(err)=>{console.log(err)});
p.then((data)=>{console.log(data)},(err)=>{console.log(err)});
複製程式碼
2.promise例項可以支援then方法的鏈式呼叫,jquery實現鏈式是通過返回當前的this。但是promise不可以通過返回this來實現。因為後續通過鏈式增加的then不是通過原始的promise物件的狀態來決定走成功還是走失敗的。
p.then((data)=>{console.log(data)},(err)=>{console.log(err)}).then((data)=>{console.log(data)})
複製程式碼
3.只要then方法中的成功回撥和失敗回撥,有返回值(包括undefiend),都會走到下個then方法中的成功回撥中,並且把返回值作為下個then成功回撥的引數傳進去。
第一個then走成功:
p.then((data)=>{return undefined},(err)={console.log()}).then((data)=>{console.log(data)})
輸出:undefiend
第一個then走失敗:
p.then((data)=>{console.log(1)},(err)={return undefined).then((data)=>{console.log(data)})
輸出:undefiend
複製程式碼
4.只要then方法中的成功回撥和失敗回撥,有一個丟擲異常,則都會走到下一個then中的失敗回撥中
第一個then走成功:
p.then((data)=>{throw new Err("錯誤")},(err)={console.log(1)}).then((data)=>{console.log('成功')},(err)=>{console.log(err)})
輸出:錯誤
第一個then走失敗:
p.then((data)=>{console.log(1)},(err)={throw new Err("錯誤")).then((data)=>{console.log('成功')},(err)=>{console.log(err)})
輸出:錯誤
複製程式碼
5.成功和失敗 只能走一個,如果成功了,就不會走失敗,如果失敗了,就不會走成功;
6.如果then方法中,返回的不是一個普通值,仍舊是一個promise物件,該如何處理?
答案:它會等待這個promise的執行結果,並且傳給下一個then方法。如果成功,就把這個promise的結果傳給下一個then的成功回撥並且執行,如果失敗就把錯誤傳給下一個then的失敗回撥並且執行。
7.具備catch捕獲錯誤;如果catche前面的所有then方法都沒有失敗回撥,則catche會捕獲到錯誤資訊執行他就是用來兜兒底用的
p是一個失敗的回撥:
p.then((data)=>{console.log('成功')}).then((data)=>{成功}).catche(e){console.log('錯誤')}
複製程式碼
8.返回的結果和 promise是同一個,永遠不會成功和失敗
var r = new Promise(function(resolve,reject){
return r;
})
r.then(function(){
console.log(1)
},function(err){
console.log(err)
})
複製程式碼
可以看到結果一直都是pending狀態
當你沒有現成的Promise時,你可能需要藉助一些Promise庫,一個流行的選擇是使用 bluebird。 這些庫可能會提供比原生方案更多的功能,並且不侷限於Promise/A+標準所規定的特性。
Generator(ECMAScript6)+co
JavaScript 生成器是個相對較新的概念, 它是ES6(也被稱為ES2015)的新特性。想象下面這樣的一個場景:
當你在執行一個函式的時候,你可以在某個點暫停函式的執行,並且做一些其他工作,然後再返回這個函式繼續執行, 甚至是攜帶一些新的值,然後繼續執行。
上面描述的場景正是JavaScript生成器函式所致力於解決的問題。當我們呼叫一個生成器函式的時候,它並不會立即執行, 而是需要我們手動的去執行迭代操作(next方法)。也就是說,你呼叫生成器函式,它會返回給你一個迭代器。迭代器會遍歷每個中斷點。
function* foo () {
var index = 0;
while (index < 2) {
yield index++; //暫停函式執行,並執行yield後的操作
}
}
var bar = foo(); // 返回的其實是一個迭代器
console.log(bar.next()); // { value: 0, done: false }
console.log(bar.next()); // { value: 1, done: false }
console.log(bar.next()); // { value: undefined, done: true }
複製程式碼
更進一步的,如果你想更輕鬆的使用生成器函式來編寫非同步JavaScript程式碼,我們可以使用 co 這個庫,co是著名的tj大神寫的。
Co是一個為Node.js和瀏覽器打造的基於生成器的流程控制工具,藉助於Promise,你可以使用更加優雅的方式編寫非阻塞程式碼。
使用co,前面的示例程式碼,我們可以使用下面的程式碼來改寫:
co(function* (){
yield Something.save();
}).then(function() {
// success
})
.catch(function(err) {
//error handling
});
複製程式碼
你可能會問:如何實現並行操作呢?答案可能比你想象的簡單,如下(其實它就是Promise.all而已):
yield [Something.save(), Otherthing.save()];
複製程式碼
終極解決方案Async/ await
簡而言之,使用async關鍵字,你可以輕鬆地達成之前使用生成器和co函式所做到的工作。
在這背後,async函式實際使用的是Promise,這就是為什麼async函式會返回一個Promise的原因。
因此,我們使用async函式來完成類似於前面程式碼所完成的工作,可以使用下面這樣的方式來重新編寫程式碼:
async function save(Something) {
try {
await Something.save(); // 等待await後面的程式碼執行完,類似於yield
} catch (ex) {
//error handling
}
console.log('success');
}
複製程式碼
使用async函式,你需要在函式宣告的最前面加上async關鍵字。這之後,你可以在函式內部使用await關鍵字了,作用和之前的yield作用是類似的。
使用async函式完成並行任務與yiled的方式非常的相似,唯一不同的是,此時Promise.all不再是隱式的,你需要顯示的呼叫它:
async function save(Something) {
await Promise.all[Something.save(), Otherthing.save()]
}
複製程式碼
Async/Await是非同步操作的終極解決方案,Koa 2在node 7.6釋出之後,立馬釋出了正式版本,並且推薦使用async函式來編寫Koa中介軟體。
這裡給出一段Koa 2應用裡的一段程式碼:
exports.list = async (ctx, next) => {
try {
let students = await Student.getAllAsync();
await ctx.render('students/index', {
students : students
})
} catch (err) {
return ctx.api_error(err);
}
};
複製程式碼
它做了3件事兒
- 通過await Student.getAllAsync();來獲取所有的students資訊。
- 通過await ctx.render渲染頁面
- 由於是同步程式碼,使用try/catch做的異常處理
之後還會分享node的基本概念和eventLoop(巨集任務和微任務)
(完)