所謂"異步",簡單說就是一個任務(wù)分成兩段,先執(zhí)行第一段绿饵,然后轉(zhuǎn)而執(zhí)行其他任務(wù)娃闲,當(dāng)?shù)谝欢斡辛藞?zhí)行結(jié)果之后,再回過頭執(zhí)行第二段围来。JavaScript采用異步編程原因有兩點跺涤,一是JavaScript是單線程,二是為了提高CPU的利用率监透。在提高CPU的利用率的同時也提高了開發(fā)難度桶错,尤其是在代碼的可讀性上。
console.log(1);
setTimeout(function () {
console.log(2);
});
console.log(3);
callback
最開始我們在處理異步的時候胀蛮,采用的是callback回調(diào)函數(shù)的方式
asyncFunction(function(value){
// todo
})
在一般簡單的情況下院刁,這種方式是完全夠用的,但是如果碰到稍微復(fù)雜的場景粪狼,就有些力不從心退腥,例如當(dāng)異步嵌套過多的時候。
回調(diào)金字塔
但是當(dāng)我們的異步操作比較多鸳玩,而且都依賴于上一步的異步的執(zhí)行結(jié)果阅虫,那么我們就會產(chǎn)生回調(diào)金字塔,難于閱讀
step1(function (value1) {
step2(function(value2) {
step3(function(value3) {
step4(function(value4) {
// Do something with value4
});
});
});
});
當(dāng)然為了改進這種層層嵌套的寫法不跟,我們有幾種方式
1 命名函數(shù)
function fun1 (params) {
// todo
asyncFunction(fun2);
}
function fun2 (params) {
// todo
asyncFunction(fun3)
}
function fun3 (params) {
// todo
asyncFunction(fun4)
}
function fun4 (params) {
// todo
}
asyncFunction(fun1)
2 基于事件消息機制的寫法
eventbus.on("init", function(){
operationA(function(err,result){
eventbus.dispatch("ACompleted");
});
});
eventbus.on("ACompleted", function(){
operationB(function(err,result){
eventbus.dispatch("BCompleted");
});
});
eventbus.on("BCompleted", function(){
operationC(function(err,result){
eventbus.dispatch("CCompleted");
});
});
eventbus.on("CCompleted", function(){
// do something when all operation completed
});
當(dāng)然也可以利用模塊化來處理颓帝,使得代碼易于閱讀。以上這三種方式都只是在代碼的可讀性上面做了改進窝革,但是并沒有解決另外一個問題就是異常捕獲购城。
錯誤棧
function a () {
b();
}
function b () {
c();
}
function c () {
d();
}
function d () {
throw new Error('出錯啦');
}
a();
從上面的圖我們可以看到有一個比較清晰的錯誤棧信息,a調(diào)用b - b調(diào)用c - c調(diào)用d 虐译,在d中拋出了一個異常瘪板。也就是說在JavaScript中在執(zhí)行一個函數(shù)的時候首先會壓入執(zhí)行棧中,執(zhí)行完畢后會移除執(zhí)行棧漆诽,F(xiàn)ILO的結(jié)構(gòu)侮攀。我們可以很方便的從錯誤信息中定位到出錯的地方。
function a() {
b();
}
function b() {
c(cb);
}
function c(callback) {
setTimeout(callback, 0)
}
function cb() {
throw new Error('出錯啦');
}
a();
從上圖我們可以看到只打印出了是在一個setTimeout中的回調(diào)函數(shù)中出現(xiàn)了異常厢拭,執(zhí)行順序是跟蹤不到的兰英。
異常捕獲
回調(diào)函數(shù)中的異常是不能夠捕捉到的,因為是異步的供鸠,我們只能在回調(diào)函數(shù)中使用try catch捕獲畦贸,也就是我注釋的部分。
function a() {
setTimeout(function () {
// try{
throw new Error('出錯啦');
// } catch (e) {
// }
}, 0);
}
try {
a();
} catch (e) {
console.log('捕捉到異常啦,好高興哦');
}
但是try catch只能捕捉到同步的錯誤薄坏,不過在回調(diào)中也有一些比較好的錯誤處理模式趋厉,例如error-first的代碼風(fēng)格約定,這種風(fēng)格在node.js中廣泛被使用 胶坠。
function foo(cb) {
setTimeout(() => {
try {
func();
cb(null, params);
} catch (error) {
cb(error);
}
}, 0);
}
foo(function(error, value){
if(error){
// todo
}
// todo
});
但是這么做也很容易陷入惡魔金字塔中君账。
Promise
規(guī)范簡述
- promise 是一個擁有 then 方法的對象或函數(shù)。
- 一個promise有三種狀態(tài) pending, rejected, resolved 狀態(tài)一旦確定就不能改變涵但,且只能夠由pending狀態(tài)變成rejected或者resolved狀態(tài)杈绸,reject和resolved狀態(tài)不能相互轉(zhuǎn)換。
- 當(dāng)promise執(zhí)行成功時矮瘟,調(diào)用then方法的第一個回調(diào)函數(shù)瞳脓,失敗時調(diào)用第二個回調(diào)函數(shù)。
- promise實例會有一個then方法澈侠,這個then方法必須返回一個新的promise劫侧。
規(guī)范更多細節(jié)請看這里
基本用法
// 異步操作放在Promise構(gòu)造器中
const promise1 = new Promise((resolve) => {
setTimeout(() => {
resolve('hello');
}, 1000);
});
// 得到異步結(jié)果之后的操作
promise1.then(value => {
console.log(value, 'world');
}, error =>{
console.log(error, 'unhappy')
});
異步代碼,同步寫法
asyncFun()
.then(cb)
.then(cb)
.then(cb)
promise以這種鏈?zhǔn)綄懛ㄉ诳校鉀Q了回調(diào)函數(shù)處理多重異步嵌套帶來的回調(diào)地獄問題烧栋,使代碼更加利于閱讀,當(dāng)然本質(zhì)還是使用回調(diào)函數(shù)拳球。
異常捕獲
前面說過如果在異步的callback函數(shù)中也有一個異常审姓,那么是捕獲不到的,原因就是回調(diào)函數(shù)是異步執(zhí)行的祝峻。我們看看promise是怎么解決這個問題的魔吐。
asyncFun(1).then(function (value) {
throw new Error('出錯啦');
}, function (value) {
console.error(value);
}).then(function (value) {
}, function (result) {
console.log('有錯誤', result);
});
其實是promise的then方法中,已經(jīng)自動幫我們try catch了這個回調(diào)函數(shù)莱找,實現(xiàn)大致如下酬姆。
Promise.prototype.then = function(cb) {
try {
cb()
} catch (e) {
// todo
reject(e)
}
}
then方法中拋出的異常會被下一個級聯(lián)的then方法的第二個參數(shù)捕獲到(前提是有),那么如果最后一個then中也有異常怎么辦奥溺。
Promise.prototype.done = function (resolve, reject) {
this.then(resolve, reject).catch(function (reason) {
setTimeout(() => {
throw reason;
}, 0);
});
};
asyncFun(1).then(function (value) {
throw new Error('then resolve回調(diào)出錯啦');
}).catch(function (error) {
console.error(error);
throw new Error('catch回調(diào)出錯啦');
}).done((reslove, reject) => {});
我們可以加一個done方法辞色,這個方法并不會返回promise對象,所以在此之后并不能級聯(lián)浮定,done方法最后會把異常拋到全局相满,這樣就可以被全局的異常處理函數(shù)捕獲或者中斷線程。這也是promise的一種最佳實踐策略桦卒,當(dāng)然這個done方法并沒有被ES6實現(xiàn)立美,所以我們在不適用第三方Promise開源庫的情況下就只能自己來實現(xiàn)了。為什么需要這個done方法闸盔。
const asyncFun = function (value) {
return new Promise(function (resolve, reject) {
setTimeout(function () {
resolve(value);
}, 0);
})
};
asyncFun(1).then(function (value) {
throw new Error('then resolve回調(diào)出錯啦');
});
(node:6312) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): Error: then resolve回調(diào)出錯啦
(node:6312) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code
我們可以看到JavaScript線程只是報了一個警告,并沒有中止線程琳省,如果是一個嚴(yán)重錯誤如果不及時中止線程迎吵,可能會造成損失躲撰。
局限
promise有一個局限就是不能夠中止promise鏈,例如當(dāng)promise鏈中某一個環(huán)節(jié)出現(xiàn)錯誤之后击费,已經(jīng)沒有了繼續(xù)往下執(zhí)行的必要性拢蛋,但是promise并沒有提供原生的取消的方式,我們可以看到即使在前面已經(jīng)拋出異常蔫巩,但是promise鏈并不會停止谆棱。雖然我們可以利用返回一個處于pending狀態(tài)的promise來中止promise鏈。
const promise1 = new Promise((resolve) => {
setTimeout(() => {
resolve('hello');
}, 1000);
});
promise1.then((value) => {
throw new Error('出錯啦!');
}).then(value => {
console.log(value);
}, error=> {
console.log(error.message);
return result;
}).then(function () {
console.log('DJL簫氏');
});
特殊場景
- 當(dāng)我們的一個任務(wù)依賴于多個異步任務(wù)圆仔,那么我們可以使用Promise.all
- 當(dāng)我們的任務(wù)依賴于多個異步任務(wù)中的任意一個垃瞧,至于是誰無所謂,Promise.race
上面所說的都是ES6的promise實現(xiàn)坪郭,實際上功能是比較少个从,而且還有一些不足的,所以還有很多開源promise的實現(xiàn)庫歪沃,像q.js等等嗦锐,它們提供了更多的語法糖,也有了更多的適應(yīng)場景沪曙。
核心代碼
var defer = function () {
var pending = [], value;
return {
resolve: function (_value) {
value = _value;
for (var i = 0, ii = pending.length; i < ii; i++) {
var callback = pending[i];
callback(value);
}
pending = undefined;
},
then: function (callback) {
if (pending) {
pending.push(callback);
} else {
callback(value);
}
}
}
};
當(dāng)調(diào)用then的時候奕污,把所有的回調(diào)函數(shù)存在一個隊列中,當(dāng)調(diào)用resolve方法后液走,依次將隊列中的回調(diào)函數(shù)取出來執(zhí)行
var ref = function (value) {
if (value && typeof value.then === "function")
return value;
return {
then: function (callback) {
return ref(callback(value));
}
};
};
這一段代碼實現(xiàn)的級聯(lián)的功能碳默,采用了遞歸。如果傳遞的是一個promise那么就會直接返回這個promise育灸,但是如果傳遞的是一個值腻窒,那么會將這個值包裝成一個promise。
generator
基本用法
function * gen (x) {
const y = yield x + 2;
// console.log(y); // 猜猜會打印出什么值
}
const g = gen(1);
console.log('first', g.next()); //first { value: 3, done: false }
console.log('second', g.next()); // second { value: undefined, done: true }
通俗的理解一下就是yield關(guān)鍵字會交出函數(shù)的執(zhí)行權(quán)磅崭,next方法會交回執(zhí)行權(quán)儿子,yield會把generator中yield后面的執(zhí)行結(jié)果,帶到函數(shù)外面砸喻,而next方法會把外面的數(shù)據(jù)返回給generator中yield左邊的變量柔逼。這樣就實現(xiàn)了數(shù)據(jù)的雙向流動。
generator實現(xiàn)異步編程
我們來看generator如何是如何來實現(xiàn)一個異步編程(*)
const fs = require('fs');
function * gen() {
try {
const file = yield fs.readFile;
console.log(file.toString());
} catch(e) {
console.log('捕獲到異常', e);
}
}
// 執(zhí)行器
const g = gen();
g.next().value('./config1.json', function (error, value) {
if (error) {
g.throw('文件不存在');
}
g.next(value);
});
那么我們next中的參數(shù)就會是上一個yield函數(shù)的返回結(jié)果割岛,可以看到在generator函數(shù)中的代碼感覺是同步的愉适,但是要想執(zhí)行這個看似同步的代碼,過程卻很復(fù)雜癣漆,也就是流程管理很復(fù)雜维咸。那么我們可以借用TJ大神寫的co。
generator 配合 co
下面來看看如何使用:
const fs = require('fs');
const utils = require('util');
const readFile = utils.promisify(fs.readFile);
const co = require('co');
function * gen(path) {
try {
const file = yield readFile('./basic.use1.js');
console.log(file.toString());
} catch(e) {
console.log('出錯啦');
}
}
co(gen());
我們看到使用co這個執(zhí)行器配合generator和promise會非常方便,非常類似同步寫法癌蓖,而且異步中的錯誤也能很容易被try catch到瞬哼。這里之所以要使用utils.promisify這個工具函數(shù)將普通的異步函數(shù)轉(zhuǎn)換成一個promise,是因為co may only yield a chunk, promise, generator, array, or object租副。使用co 配合generator最大的一個好處就是錯誤可以try catch 到坐慰。
async/await
先來看一段async/await的異步寫法
const fs = require('fs');
const utils = require('util');
const readFile = utils.promisify(fs.readFile);
async function readJsonFile() {
try {
const file = await readFile('../generator/config.json');
console.log(file.toString());
} catch (e) {
console.log('出錯啦');
}
}
readJsonFile();
我們可以看到async/await的寫法十分類似于generator,實際上async/await就是generator的一個語法糖用僧,只不過內(nèi)置了一個執(zhí)行器结胀。并且當(dāng)在執(zhí)行過程中出現(xiàn)異常,就會停止繼續(xù)執(zhí)行责循。當(dāng)然await后面必須接一個promise糟港,而且node版本必須要>=7.6.0
才可以使用,當(dāng)然低版本也可以采用babel沼死。
補充
在開發(fā)過程中我們常常手頭會同時有幾個項目着逐,那么node的版本要求很有可能是不同的,那么我們就需要安裝不同版本的node意蛀,并且管理這些不同的版本耸别,這里推薦使用nvm,下載好nvm县钥,安裝秀姐,使用nvm list 查看node版本列表。使用nvm use 版本號 進行版本切換若贮。
在Node.js中捕獲漏網(wǎng)之魚
process.on('uncaughtException', (error: any) => {
logger.error('uncaughtException', error)
})
在瀏覽器環(huán)境中捕獲漏網(wǎng)之魚
window.addEventListener('onrejectionhandled', (event: any) => {
console.error('onrejectionhandled', event)
})
參考文章
Promise中文迷你書
剖析Promise內(nèi)部結(jié)構(gòu)省有,一步一步實現(xiàn)一個完整的、能通過所有Test case的Promise類
深入理解Promise實現(xiàn)細節(jié)
DJL簫氏的個人博客