异步(下)
在异步(上)中,只是简单介绍了一些概念,在本章中,会带着详细学习异步
5.1 面试题
1.请描述 event loop (事件循环 / 事件轮询)的机制,可画图
2.什么是宏任务和微任务,两者有什么区别?
3.Promise 有哪三种状态,如何变化?
4.promise 中 then 和 catch 的连接
// 第一题
Promise.resolve().then(() => {
console.log(100);
}).catch(() => {
console.log(200);
}).then(() => {
console.log(300);
});
// 第二题
Promise.resolve().then(() => {
console.log(400);
throw new Error('error1')
}).catch(() => {
console.log(500);
}).then(() => {
console.log(600);
});
// 第三题
Promise.resolve().then(() => {
console.log(700);
throw new Error('error2')
}).catch(() => {
console.log(800);
}).catch(() => {
console.log(900);
});
附加题:将上面三个 promise 放在一起执行,给出输出结果
5.async 和 await 语法题
async function fn() {
return 100;
};
async function excute() {
const res1 = fn();
const res2 = await fn();
}
excute(); // res1, res2 的内容
async function excute() {
console.log('start');
const res1 = await 100;
console.log('res1:', res1);
const res2 = await Promise.resolve(200)
console.log('res2:', res2);
const res3 = await Promise.reject(300)
console.log('res3:', res3);
console.log('end');
}
excute();// 打印内容
6.promise 和 setTimeout 的顺序
console.log(100);
setTimeout(() => {
console.log(200);
}, 0);
Promise.resolve().then(() => {
console.log(300);
});
console.log(400);
7.promise 和 async / await 的顺序问题
async function asyncFn1() {
console.log('asyncFn1 excute');
await asyncFn2();
console.log('asyncFn1 end');
}
async function asyncFn2() {
console.log('asyncFn2 excute');
}
console.log('script start');
setTimeout(() => {
console.log('setTimeout');
});
asyncFn1();
new Promise ((resolve) => {
console.log('promise excute');
resolve();
}).then(() => {
console.log('then excute');
});
console.log('script end');
8.手写实现 promise
5.2 event loop(事件循环 / 事件轮询)
JS 是单线程运行的,遇到等待(网络请求,定时任务等)不能卡住,需要异步(通过 回调 callback 函数形式 )的方式来执行 JS 。
异步是基于回调实现实现的,而 event loop 就是异步回调的实现原理
1.JS 如何执行(执行顺序)
从前到后,一行一行执行
如果某一行执行报错,会停止下面代码的执行
先执行完同步,再执行异步
2.简单描述 JS 整个执行流程
以下面一段代码为例:
console.log('start');
setTimeout(() => {
console.log('setTimeout');
}, 1000);
console.log('end');
分析:
执行顺序:
- 执行 console.log(‘start’) 时,会放入到执行栈( call stack )中,然后立即执行,这时候浏览器控制台( browser console )输出 start 。
- 执行setTimeout 时,为了不阻碍程序继续执行,会直接把 setTimeout 放进 web APIs,注意,这时候已经开始计时,等到计时结束,会将 setTimeout 中的执行函数放进回调队列( callback queue )中等待,注意,这个时候也不会执行。
- 执行 console.log(‘end’) 时,会放入到执行栈( call stack )中,然后立即执行,这时候浏览器控制台( browser console )输出 end 。
- 这时候同步代码已经执行完成,这时候通过 event loop 轮询,发现 回调队列( callback queue )中还有任务在等待,于是放入 执行栈( call stack )中立即执行。这时候浏览器控制台( browser console )输出 setTimeout 。
上面四步是这段代码的顺序,可以看出来,遇到异步并不会停止代码执行,而是按一定规则继续执行同步代码,同步代码执行完后,会通过 event loop 轮询查询是否有异步任务等待执行,而 event loop 正是异步回调实现的关键所在。
注:上面只是简单示例,并非完整的js执行过程
3.DOM 事件 和 event loop 的关系
DOM 事件也使用回调,基于 event loop
console.log('start');
setTimeout(() => {
console.log('setTimeout');
}, 1000);
document.getElementById('divClick').addEventListener('click', function () {
console.log('click div');
})
console.log('end');
可以看到,我们在上面代码的基础上添加了一个监听事件,整体执行与上面的一致,需要注意的是,当执行到 addEventListener 时,会保存下来这个事件(已执行,可能暂存web APIs),相当于一个 loading 状态,用户点击时会立即放进回调队列( callback queue )等待执行。这就和 event loop 实现异步的道理一样。
注意:虽然 DOM 事基于 event loop 去使用回调,但DOM事件不属于异步
5.3 Promise 进阶
1.promise 有哪三种状态
pending: 初始状态,不是成功或失败状态。
fulfilled: 意味着操作成功完成。
rejected: 意味着操作失败。
2.状态的表现和变化
1.pending 状态,不会触发 then 和 catch,PromiseState 值也是 pending
const pro = new Promise((resolve, reject) => {
// ...,没有调用 resolve 或 reject
// 这里 resolve 和 reject 都是函数
})
console.log(pro)
/**
* Promise {<pending>}
* __proto__: Promise
* [[PromiseState]]: "pending"//当前 promise 的状态
* [[PromiseResult]]: undefined//传入resolve 或 reject 中的参数,不传为 undefined
*/
2.调用resolve函数,状态会从 Pending 变为 Fulfilled, 触发后续的 then 回调函数
const pro = new Promise((resolve, reject) => {
resolve('resolve string');
})
// Promise {<fulfilled>: resolve string}
// 其中 PromiseState:fulfilled,PromiseResult:resolve string
// 触发紧邻的then函数
3. 调用resolve函数,状态会从 Pending 变为 Rejected, 会触发后续的 catch 回调函数
const pro = new Promise((resolve, reject) => {
reject('reject string');
})
// Promise {<rejected>: "reject"}
// 其中 PromiseState:rejected,PromiseResult:reject string
// 触发 catch
4.状态凝固
一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise 对象的状态改变,只有两种可能:从 Pending 变为 Resolved 和从 Pending 变为 Rejected。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果。
const pro = new Promise((resolve, reject) => {
reject('reject');
resolve('resolve');
})
// 先执行 reject, 状态改变为 rejected,触发catch,
// 那么即使后面再执行 resolve,状态不会改变,也不会再触发 then了
// 因为只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果。
5.then 和 catch 对状态的影响
1.then 正常返回状态 fulfilled,里面有报错则返回 rejected(如果 then后面还想接 then,并需要使用上一个 then 的数据,则可以在上一个 then 中使用 return 返回即可)
2.cathc 正常返回状态也是 fulfilled(注意这里),里面有报错则返回 rejected
const pro = new Promise((resolve, reject) => {
reject('reject string');
})
pro.then((res) => {
console.log(res); // 不触发
}).catch((err) => {
console.log(err); // 触发,输出 reject string
return err; // 无报错,正常返回状态也是 fulfilled,因此能触发下一个then
}).then((res) => {
console.log(res); // 触发,输出 reject string
})
注意:报错可以用 throw new Error 来触发
5.4 async / await
以前存在 callback hell 这种情况
后来出现了 promise then catch 链式调用,从嵌套执行转变到顺序执行,但是本质上也是基于回调函数
当出现 async / await 这种同步语法后 ,彻底消灭了回调函数(注意:是从语法层面上消灭,它只是语法糖,本质仍然是回调)
需要明确的一点是,由于 JS 是单线程,是不可能消灭异步和回调的。但是可以从语法上去改变,让其看起来是一个同步执行
1.async / await 和 promise 的关系
async / await 和 promise 并不冲突,两者相辅相成
直接执行 async 函数,返回的是 promise 对象
async function asyncFn() {
console.log('asyncFn');
}
async function executeFn() {
const res = asyncFn();
console.log(res); // Promise {<fulfilled>: undefined}
}
executeFn();
await 相当于 promise 的then(不会走catch), 返回的直接就是then的参数,处理的是成功的情况
async function asyncFn() {
console.log('asyncFn');
return 'return asyncFn'
}
async function executeFn() {
const res = await asyncFn();
console.log(res); // 输出: return asyncFn
}
executeFn();
由于不会走 catch 可以用 try…catch 捕获异常,代替了 promise 的catch,处理的是失败的情况
async function asyncFn() {
console.log('asyncFn');
throw new Error('error asyncFn');
}
async function executeFn() {
try {
const res = await asyncFn();
console.log(res); // 未触发
} catch(error) {
console.log(error); // 触发
}
}
executeFn();
关于 try catch:
能被 try catch 捕捉到的异常,必须是在报错的时候,线程执行已经进入 try catch 代码块,且处在 try catch 里面,这个时候才能被捕捉到。
5.5 异步的本质,async / await 是语法糖
上面已经提到过:
当出现 async / await 这种同步语法后 ,彻底消灭了回调函数(注意:是从语法层面上消灭,它只是语法糖,本质仍然是回调)
需要明确的一点是,由于 JS 是单线程,是不可能消灭异步和回调的。但是可以从语法上去改变,让其看起来是一个同步执行
在 await 后面执行的代码,都可以看作是 await 后的异步代码,需要等待同步执行完后才能执行
执行示例:
async function asyncFn1() {
console.log('asyncFn1 start'); // 2
await asyncFn2();
console.log('asyncFn1 wait'); // 5
await asyncFn3();
console.log('asyncFn1 end'); // 7
}
async function asyncFn2() {
console.log('asyncFn2'); // 3
}
async function asyncFn3() {
console.log('asyncFn3'); // 6
}
console.log('script start'); // 1
asyncFn1();
console.log('script end'); // 4
执行顺序已经由注释标记
分析:
- 首先执行第 17 行 ,输出 script start ,随后进入 asyncFn1 函数内;
- 执行第 2 行,输出 asyncFn1 start ,因为 async 会立马执行,因此进入 asyncFn2 函数;
- 执行第 10 行,输出 asyncFn2 ,退出 asyncFn2 函数;
- 如开头所说,在 await 后面执行的代码,都可以看作是 await 后的异步代码,需要等待同步执行完后才能执行,因此第 4 到 6 行都被认作是异步,需要等待同步代码首先执行,因此跳出 asyncFn1 函数;
- 执行 第 19 行,输出 script end,至此同步代码全部执行完毕;
- 通过 event loop 轮询,发现还有异步代码未执行;
- 执行第 4 行,输出 asyncFn1 wait,随后执行第 5 行,进入 asyncFn3 函数;
- 执行第 14 行,输出 asyncFn3,退出 asyncFn3 函数;
- 第 6 行本身是作为 第 5 行 await 的异步,等待同步代码;
- 后面已经无同步代码,因此执行第 6 行,输出 asyncFn1 end;
以上过程仅仅作为分析参考,如果已经完全理解了 async / await ,这一段分析就不需要看了。
5.6 for … of
forEach 常规的同步遍历
for … of 常用于异步的遍历,除此之外,for … in ,for,也支持异步
示例:
function asyncForFn(num) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(num);
}, 1000)
});
};
const arr = [1, 2, 3, 4, 5]
// 同步,结果会一下全部打印出来
// async function excute() {
// arr.forEach(async (item) => {
// const res = await asyncForFn(item);
// console.log(res);
// })
// }
// 异步,结果会隔一秒打印,直至打印完成
// async function excute() {
// for (let item in arr) {
// const res = await asyncForFn(arr[item]);
// console.log(res);
// }
// }
// 异步,结果会隔一秒打印,直至打印完成
// async function excute() {
// for (let item of arr) {
// const res = await asyncForFn(item);
// console.log(res);
// }
// }
// 异步,结果会隔一秒打印,直至打印完成
async function excute() {
for (let i = 0; i < arr.length; i += 1) {
const res = await asyncForFn(arr[i]);
console.log(res);
}
}
excute();
注1:for…in 和 for…of的区别
1.for…in 支持遍历对象;但for…of只能遍历数组
2.for…in 会将数组原型上自定义的属性和方法也遍历出来;而for…of 不会,因此不建议使用 for…in 来遍历数组(但是如下面示例,可以使用 hasOwnProperty 来进行筛选,就只会选出当前数组的下标)
const arr = [1,2,3];
Array.prototype.arr1 = function(){
console.log('consoleLog string');
}
Array.prototype.arr2 = 'stringOrNumber';
for(let index in arr){
// if (arr.hasOwnProperty(index)) {
// console.log(index, arr[index]);
// }
console.log(index, arr[index]);
}
// 可以看除用 for in遍历数组,会将数组原型下自定义的的属性和方法也遍历出来
// 打印结果
// 0 1
// 1 2
// 2 3
// arr1 ƒ (){
// console.log('consoleLog string');
// }
// arr2 stringOrNumber
注2:使用 for … in 遍历对象
Object.prototype.c = function() {};
const obj = {
a: 1,
b: 2,
}
for (let key in obj) {
// if (obj.hasOwnProperty(key)) {
// console.log(key, obj[key]);
// }
console.log(key, obj[key]);
}
// 输出 a 1, b 2, c ƒ () {}
可以看出,使用for … in 遍历对象,会将对象原型上自定义的属性和方法也遍历出来,当然,可以像代码中注释的那样,使用 hasOwnProperty 来筛选属于本对象的 key 。
5.7 宏任务 和 微任务
宏任务是由宿主(node,浏览器)发起的,而微任务由JavaScript自身发起。
1.示例题
console.log('start');
setTimeout(() => {
console.log('setTimeout');
});
Promise.resolve().then(() => {
console.log('promise');
});
console.log('end');
输出顺序:start, end, promise,setTimeout
为什么会这样输出,接着往下看
2.宏任务和微任务有哪些
宏任务:setTimeout,setInterval,ajax,DOM事件(不是异步,但依赖于event loop)
微任务:promise,async / await
3. 宏任务和微任务执行时机
结论:微任务执行时机要比宏任务早
4.event loop 和 DOM 渲染的关系
再来看这张图:
之前讲到,当 call stack 空闲(同步代码执行完),会触发 event loop 来查询是否有异步代码在等待执行。
实际上,在每次 call stack 空闲之后,触发 event loop 之前,都是 DOM 重新渲染的机会,DOM 结构如有改变,则会先进行 DOM 渲染( JS 和 DOM渲染共用一个线程),再进行 event loop 。
5.宏任务和微任务执行时机的区别
微任务:DOM 渲染前触发,如 promise
宏任务:DOM 渲染触发后,如setTimeout
结合event loop 和 DOM 渲染的关系,可以得到完整的执行顺序
再来看这道题:
console.log('start');
setTimeout(() => {
console.log('setTimeout');
});
Promise.resolve().then(() => {
console.log('promise');
});
console.log('end');
分析:
-
执行第一行,输出 start;
-
遇到 setTimeout ,放进 web APIs,开始计时,计时完成放进 callback queue 中等待执行;
-
遇到 promise ,放进 micro queue 中,等待执行;
-
执行完同步代码,首先从 micro queue 轮询是否有微任务,执行第 6 行,输出 promise;
-
微任务执行完,从 callback queue 轮询是否有宏任务,执行第 3 行,输出 setTimeout;
至此,程序执行结束
为了避免篇幅过长,面试题的解答部分会放在下一篇 JS 异步(下)答案分析中!
欢迎大家点赞,收藏,关注!!!