通过上一节我们可知:
- 异步的实现方式是通过事件循环(事件轮循),而事件循环的核心是回调函数
下面简单阐述下异步执行机制:
- 调用异步线程,当事件被触发以后,将对应的回调函数推入到任务队列当中,当主线程的任务结束之后,将回调函数的任务通过事件轮询的方式推到主线程当中
1. 异步回调造成的问题
1.1 回调地狱
【回调地狱】 一句话概括:异步回调函数的嵌套。缺点: 难于维护,不便拓展,可读性差
下面看一下Node 中异步函数的回调地狱:
我们在同级目录下创建 app.js、name.txt、getSource.text 和 source.txt。
下面为 app.js 中的代码
// 引入 node 中的 fs 模块,调用fs中的 readFile 方法来读取文件的内容,该方法传入一个路径、字符编码和回调函数
let fs = require("fs");
fs.readFile("./text.txt", "utf-8", (error, data) => {
if (error) {
console.log(error);
}
fs.readFile(data, "utf-8", (error, data) => {
if (error) {
console.log(error);
}
fs.readFile(data, "utf-8", (error, data) => {
if (error) {
console.log(error);
}
console.log(data);
});
});
});
控制台 输出 node app.js 执行,可以看到此时输出了 99,也就是拿到了 source 中的数据,但此时如果 source.txt 中依然有另外的地址信息,那么这个上面这个 JS 代码将会像套娃一样一层一层向前推进,如果其中的某一环节处了问题,我们是很难确定问题具体出现在哪一环的
我们再来看看普通回调函数的回调地狱:
setTimeout(()=> {
console.log(1);
setTimeout(()=> {
console.log(2);
setTimeout(()=> {
console.log(3);
},1000)
},2000)
},3000)
与上面类似,当代码像套娃一样以层层递进的模样出现时,就陷入了回调地狱
1.2. try catch 不能捕获异常
【 try catch 的捕捉机制 】
-
能捕捉到的异常必须是线程执行已经进入
try catch但try catch未执行完的时候抛出来的 -
try catch只能捕获同步代码的异常,不能捕获异步代码的异常
原因:当异步函数抛出异常时,对于宏任务而言,执行函数时已经将该函数推入栈,此时并不在
try catch所在的栈(主线程已经离开了try catch),所以try catch并不能捕获到错误
// try catch 捕获同步代码错误,不会报错
try {
console.log(a);
} catch(e) {
console.log(e);
}
// 无法捕获到,会直接报错
try {
setTimeout(() => {
console.log(a)
})
} catch (e) {
console.log(e);
}
1.3. 并列的多个异步任务
【 并列的多个异步 】
如下面的代码,异步不会阻塞当前线程,而是直接向下执行;所以可以直接认为当前的所有异步代码是同一时机注册的异步代码。而产生的问题是:并不能确定每个异步任务什么时候都完成运行,可能会存在相互竞争的状态
解决方案:
- 每个异步代码里都加一个判断条件。缺点:很笨重,代码重复
- ES5发布订阅模式
- Promise
// 并不能确定文件什么时候读取完
let arr = [];
function show(data) {
console.log(data);
}
fs.readFile('./name.txt', 'utf-8', (err, data) => {
if (data) {
arr.push(data)
}
arr.length === 3 && show(arr);
});
fs.readFile('./number.txt', 'utf-8', (err, data) => {
if (data) {
arr.push(data)
}
arr.length === 3 && show(arr);
});
fs.readFile('./score.txt', 'utf-8', (err, data) => {
if (data) {
arr.push(data)
}
arr.length === 3 && show(arr);
});
2. Promise
3.1 Promise 定义
【 理解 】 存放异步操作的容器,存放一个以后才会结束的事件(异步操作)
- 如KFC点餐,先给了一个小票,然后等汉堡做好,这个小票就是Promise
【 用法 】 Promise 是一个系统内置的构造函数,使用时需要被实例化
-
let promise = new Promise( executor ) -
它的参数是一个函数
(executor), 执行者;该函数又有两个参数 为:resolve和reject分别对应成功和失败各自回调 -
Promise本身是一个异步操作,但它的函数里是同步执行的
console.log(new Promise(function(resolve, reject) {}));
// 参数中的函数,本质上是同步执行的
new Promise(function(resolve, reject) {
console.log('promise')
});
console.log(1)
3.2 特征
3.2.1 三种状态
Promise本身就代表一个异步操作,所以它有三种状态
pending(进行中)fulfilled(resolve)(已成功)reject(已失败)
Tip:对象的这三种状态不受外界影响
3.2.2 状态的不可逆
从pending转为 fulfilled 或 reject,但不会反向转换
【 与事件中的异步不同 】 Promise 状态固化以后,再对 Promise 对象添加回调,是可以直接拿到这个结果的;如果说是事件的话,一旦错过了,就是真的错过了,再也不会监听到了。
3.3 参数
Promise的参数是一个函数,这个函数叫 executor( 执行者),这个函数的参数又是两个函数:
resolve:调用resolve(),能够传参, 可以将 promise 状态改为fulfilled, 接下来就可以执行成功所对应的回调函(then())数拿到值reject:调用reject(),可以将 promise 状态改为reject,其余相同
可以通过参数的执行方式改变 promise 的状态
let promise = new Promise((resolve, reject) => {
Math.random() * 100 > 60 ? resolve('及格') : reject('不及格');
});
// 绑定回调成功和失败的处理函数
promise.then((value) => { // 第一个参数是注册成功的回调函数
console.log(value);
}, (reason) => { // 第二个参数是注册失败的回调函数
console.log(reason)
});
思考下面的代码会输出什么
setTimeout(function() {
console.log('setTime'); // 异步代码
}, 30)
let promise = new Promise(function(resolve, reject) {
// 同步代码
console.log(0);
resolve(1); // 调用异步的回调函数
});
// 异步代码,推入任务队列,主线程任务结束后推入执行栈,微任务 > 宏任务
promise.then((value) => {
console.log(value);
}, (reason) => {
console.log(reason);
});
console.log(2);
3.4 链式调用
【 原理 】
.then方法在原型上的,所以可以链式调用
let promise = new Promise(function(resolve, reject) {
resolve(1);
// reject(10);
});
// 第一次 then 的返回值作为下一次 then 执行的参数
promise.then((value) => {
console.log(value);
// 可以手动返回一个new Promise,这个时候同第一次new Promise是一致的
return new Promise((resolve, reject) => {
resolve('newPromise ok')
});
}, (reason) => {
console.log(reason);
return 2;
}).then((value) => {
/*
第二次链式调用无法像第一次调用时直接拿到值,
而是需要在上面手动添加 return
*/
console.log('ok then2:' + value);
}, (reason) => {
console.log('no then2:' + reason)
});
3. 宏任务、微任务
JS异步代码中,分为:
- 宏任务:宏任务队列 -> 除了下面两种以外的所以异步任务,都可以认为是宏任务
- 微任务:微任务队列 ->
promise, Node中的process.nextTick()-> 微任务的优先级更高
先同步任务,然后微任务,再宏任务。 在异步代码中说宏任务才有意义
宏任务和微任务之间的嵌套
思考下面的代码会输出什么
Promise.resolve().then(() => { // 微任务
console.log('promise1');
setTimeout(() => { // 宏任务
console.log('setTimeout2')
})
})
setTimeout(() => { // 宏任务
// 第一轮循环,微任务执行结束,开始执行宏任务
console.log('setTimeout1');
// 微任务 -> 第二轮循环中,微任务优先执行
Promise.resolve().then(() => {
console.log('promise2');
})
})
4. Promise A+ 规范
Tip: 定义了promise相关的行为和方法,ES6 promise 是Promise A+ 规范的实现