所謂"異步",簡單說就是一個任務分紅兩段,先執行第一段,而後轉而執行其餘任務,當第一段有了執行結果以後,再回過頭執行第二段。JavaScript採用異步編程緣由有兩點,一是JavaScript是單線程,二是爲了提升CPU的利用率。在提升CPU的利用率的同時也提升了開發難度,尤爲是在代碼的可讀性上。javascript
console.log(1);
setTimeout(function () {
console.log(2);
});
console.log(3);
複製代碼
最開始咱們在處理異步的時候,採用的是callback回調函數的方式java
asyncFunction(function(value){
// todo
})
複製代碼
在通常簡單的狀況下,這種方式是徹底夠用的,可是若是碰到稍微複雜的場景,就有些力不從心,例如當異步嵌套過多的時候。node
可是當咱們的異步操做比較多,並且都依賴於上一步的異步的執行結果,那麼咱們就會產生回調金字塔,難於閱讀git
step1(function (value1) {
step2(function(value2) {
step3(function(value3) {
step4(function(value4) {
// Do something with value4
});
});
});
});
複製代碼
固然爲了改進這種層層嵌套的寫法,咱們有幾種方式 1 命名函數github
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
});
複製代碼
固然也能夠利用模塊化來處理,使得代碼易於閱讀。以上這三種方式都只是在代碼的可讀性上面作了改進,可是並無解決另一個問題就是異常捕獲。json
function a () {
b();
}
function b () {
c();
}
function c () {
d();
}
function d () {
throw new Error('出錯啦');
}
a();
複製代碼
從上面的圖咱們能夠看到有一個比較清晰的錯誤棧信息,a調用b - b調用c - c調用d ,在d中拋出了一個異常。也就是說在JavaScript中在執行一個函數的時候首先會壓入執行棧中,執行完畢後會移除執行棧,FILO的結構。咱們能夠很方便的從錯誤信息中定位到出錯的地方。windows
function a() {
b();
}
function b() {
c(cb);
}
function c(callback) {
setTimeout(callback, 0)
}
function cb() {
throw new Error('出錯啦');
}
a();
複製代碼
從上圖咱們能夠看到只打印出了是在一個setTimeout中的回調函數中出現了異常,執行順序是跟蹤不到的。promise
回調函數中的異常是不可以捕捉到的,由於是異步的,咱們只能在回調函數中使用try catch捕獲,也就是我註釋的部分。瀏覽器
function a() {
setTimeout(function () {
// try{
throw new Error('出錯啦');
// } catch (e) {
// }
}, 0);
}
try {
a();
} catch (e) {
console.log('捕捉到異常啦,好高興哦');
}
複製代碼
可是try catch只能捕捉到同步的錯誤,不過在回調中也有一些比較好的錯誤處理模式,例如error-first的代碼風格約定,這種風格在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構造器中
const promise1 = new Promise((resolve) => {
setTimeout(() => {
resolve('hello');
}, 1000);
});
// 獲得異步結果以後的操做
promise1.then(value => {
console.log(value, 'world');
}, error =>{
console.log(error, 'unhappy')
});
複製代碼
asyncFun()
.then(cb)
.then(cb)
.then(cb)
複製代碼
promise以這種鏈式寫法,解決了回調函數處理多重異步嵌套帶來的回調地獄問題,使代碼更加利於閱讀,固然本質仍是使用回調函數。
前面說過若是在異步的callback函數中也有一個異常,那麼是捕獲不到的,緣由就是回調函數是異步執行的。咱們看看promise是怎麼解決這個問題的。
asyncFun(1).then(function (value) {
throw new Error('出錯啦');
}, function (value) {
console.error(value);
}).then(function (value) {
}, function (result) {
console.log('有錯誤', result);
});
複製代碼
實際上是promise的then方法中,已經自動幫咱們try catch了這個回調函數,實現大體以下。
Promise.prototype.then = function(cb) {
try {
cb()
} catch (e) {
// todo
reject(e)
}
}
複製代碼
then方法中拋出的異常會被下一個級聯的then方法的第二個參數捕獲到(前提是有),那麼若是最後一個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回調出錯啦');
}).catch(function (error) {
console.error(error);
throw new Error('catch回調出錯啦');
}).done((reslove, reject) => {});
複製代碼
咱們能夠加一個done方法,這個方法並不會返回promise對象,因此在此以後並不能級聯,done方法最後會把異常拋到全局,這樣就能夠被全局的異常處理函數捕獲或者中斷線程。這也是promise的一種最佳實踐策略,固然這個done方法並無被ES6實現,因此咱們在不適用第三方Promise開源庫的狀況下就只能本身來實現了。爲何須要這個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回調出錯啦');
});
複製代碼
(node:6312) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): Error: then resolve回調出錯啦
(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線程只是報了一個警告,並無停止線程,若是是一個嚴重錯誤若是不及時停止線程,可能會形成損失。
promise有一個侷限就是不可以停止promise鏈,例如當promise鏈中某一個環節出現錯誤以後,已經沒有了繼續往下執行的必要性,可是promise並無提供原生的取消的方式,咱們能夠看到即便在前面已經拋出異常,可是promise鏈並不會中止。雖然咱們能夠利用返回一個處於pending狀態的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簫氏');
});
複製代碼
上面所說的都是ES6的promise實現,實際上功能是比較少,並且還有一些不足的,因此還有不少開源promise的實現庫,像q.js等等,它們提供了更多的語法糖,也有了更多的適應場景。
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);
}
}
}
};
複製代碼
當調用then的時候,把全部的回調函數存在一個隊列中,當調用resolve方法後,依次將隊列中的回調函數取出來執行
var ref = function (value) {
if (value && typeof value.then === "function")
return value;
return {
then: function (callback) {
return ref(callback(value));
}
};
};
複製代碼
這一段代碼實現的級聯的功能,採用了遞歸。若是傳遞的是一個promise那麼就會直接返回這個promise,可是若是傳遞的是一個值,那麼會將這個值包裝成一個promise。
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關鍵字會交出函數的執行權,next方法會交回執行權,yield會把generator中yield後面的執行結果,帶到函數外面,而next方法會把外面的數據返回給generator中yield左邊的變量。這樣就實現了數據的雙向流動。
咱們來看generator如何是如何來實現一個異步編程(*)
const fs = require('fs');
function * gen() {
try {
const file = yield fs.readFile;
console.log(file.toString());
} catch(e) {
console.log('捕獲到異常', e);
}
}
// 執行器
const g = gen();
g.next().value('./config1.json', function (error, value) {
if (error) {
g.throw('文件不存在');
}
g.next(value);
});
複製代碼
那麼咱們next中的參數就會是上一個yield函數的返回結果,能夠看到在generator函數中的代碼感受是同步的,可是要想執行這個看似同步的代碼,過程卻很複雜,也就是流程管理很複雜。那麼咱們能夠借用TJ大神寫的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這個執行器配合generator和promise會很是方便,很是相似同步寫法,並且異步中的錯誤也能很容易被try catch到。這裏之因此要使用utils.promisify這個工具函數將普通的異步函數轉換成一個promise,是由於co may only yield a chunk, promise, generator, array, or object。使用co 配合generator最大的一個好處就是錯誤能夠try catch 到。
先來看一段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的一個語法糖,只不過內置了一個執行器。而且當在執行過程當中出現異常,就會中止繼續執行。固然await後面必須接一個promise,並且node版本必需要>=7.6.0
纔可使用,固然低版本也能夠採用babel。
在開發過程當中咱們經常手頭會同時有幾個項目,那麼node的版本要求頗有多是不一樣的,那麼咱們就須要安裝不一樣版本的node,而且管理這些不一樣的版本,這裏推薦使用nvm,下載好nvm,安裝,使用nvm list 查看node版本列表。使用nvm use 版本號 進行版本切換。
在Node.js中捕獲漏網之魚
process.on('uncaughtException', (error: any) => {
logger.error('uncaughtException', error)
})
複製代碼
在瀏覽器環境中捕獲漏網之魚
window.addEventListener('onrejectionhandled', (event: any) => {
console.error('onrejectionhandled', event)
})
複製代碼