所谓"异步",简单说就是一个任务分红两段,先执行第一段,而后转而执行其余任务,当第一段有了执行结果以后,再回过头执行第二段。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)
})
复制代码