衆所周知javascript是單線程的,它的設計之初是爲瀏覽器設計的GUI編程語言,GUI編程的特性之一是保證UI線程必定不能阻塞,不然體驗不佳,甚至界面卡死。javascript
所謂的單線程就是一次只能完成一個任務,其任務的調度方式就是排隊,這就和火車站洗手間門口的等待同樣,前面的那我的沒有搞定,你就只能站在後面排隊等着。java
這種模式的好處是實現起來簡單,執行環境相對單純,壞處就是隻要有一個任務耗時很長,後面的任務都會必須排隊等着,會拖延整個程序的執行。常見的瀏覽器無響應(假死),每每就是由於某一段Javascript代碼長時間運行(好比死循環),致使了整個頁面卡在這個地方,其餘任務沒法執行。node
爲了解決這個問題,Javascript語言將任務的執行模式分紅兩種:同步(Synchronous)和異步(Asynchronous)。jquery
「同步」就是上面所說的,後面的任務等待上一個任務結束,而後再執行。git
所謂異步簡單說就是一個任務分紅兩段,先執行一段,轉而執行其餘任務,等作好了準備轉而執行第二段。github
如下是當有ABC三個任務,同步或異步執行的流程圖:npm
同步編程
thread ->|----A-----||-----B-----------||-------C------|
複製代碼
異步:api
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請求,服務器性能會急劇降低,很快就會失去響應。promise
本文簡單梳理總結了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層回調,若是更多步驟呢?顯然這樣的代碼只是寫起來比較爽可是缺點也不少。
也正是基於這些緣由,在JavaScript世界中,一直都在尋找着可以讓異步JavaScript開發變得更簡單的可行的方案。這個時候就出現了promise,它解決了上述的問題。
Promise 的最大優點是標準化,各種異步工具庫都按照統一規範實現,即便是async函數也能夠無縫集成。因此用 Promise 封裝 API 通用性強,用起來簡單,學習成本低。
一個Promise表明的是一個異步操做的最終結果。
Promise意味着[許願|承諾]一個尚未完成的操做,但在將來會完成的。與Promise最主要的交互方法是經過將函數傳入它的then方法從而獲取得Promise最終的值或Promise最終拒絕(reject)的緣由。要點有三個:
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+標準所規定的特性。
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關鍵字,你能夠輕鬆地達成以前使用生成器和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件事兒
以後還會分享node的基本概念和eventLoop(宏任務和微任務)
(完)