因为对知识点的理解不到位,导致文章大改。以后不想写博客了。
一、基础知识
1.1 Promise
Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理且更强大。ES6将其写进了语言标准,统一了用法,并原生提供了Promise对象。
- Promise是一个构造函数
- 可以创建Promise的实例,const p = new Promise()
- new出来的Promise实例,代表一个异步操作,该异步操作立即执行
- Promise.prototype原型对象上包含一个.then()方法
- 每一次new Promise()构造函数得到的实例对象,都可以通过原型链的方式访问到.then(),例如p.then()
- .then()方法用来预先指定成功和失败的回调函数
- p.then(成功的回调函数,失败的回调函数)
- 成功的回调函数是必选的,失败的回调函数式可选的
- 在.then()方法返回一个新的promise,可继续.then()
- 相当于在这个新的promise上继续设置回调函数
- 如果在promise的链式操作中发生了错误,可以使用Promise.prototype.catch方法进行捕获和处理。如果不希望前面的错误导致后续的.then无法正常执行,则可以将.catch的调用提前。
1.2 同步任务与异步任务
JS任务分为两类:同步任务与异步任务
- 同步任务(非耗时任务):在主线程排队执行的任务
- 异步任务(耗时任务):委托给宿主环境执行的任务
- 宏任务:异步Ajax、setTimeout、setInterval、文件操作等
- 微任务:Promise.then、Promise.catch等
1.3 事件循环机制
JavaScript主线程从“任务队列”中读取异步任务的回调函数,放到执行栈中依次执行。这个过程是循环不断的,所以整个的这种运行机制又称为事件循环。
- 每一个宏任务执行完之后,都会检查是否存在待执行的微任务
- 如果有,则执行完所有微任务之后,再继续执行下一个宏任务。
- 没有,则执行下一个宏任务。
Promise.then加入微任务队列的时机:(非常重要)
参考:promise then 的回调函数是在什么时候进入微任务队列的? - SegmentFault 思否、
异步的 Promise的 then 方法的回调是何时被添加到microtasks queue中的? - 知乎
如果在调用then时,如果promise已经resolve了,那么就是fullfilled状态,会将其任务加入微任务队列;如果在调用then时,promise没有resolve或reject,处于pending状态,会将其任务缓存在一个结构里,直到promise被接受或者拒绝时,加入微任务队列。
二、面试题
1、实现sleep()函数
setTimout((), 1000);
console.log("finished");
通常来说,第一反应是直接用setTimout()作为sleep()函数,但上述方法不可行,因为setTimeout会直接被加入宏任务队列,而下面的console.log是同步任务,所以会直接先输出。
setTimout(()=>console.log("finished"), 1000);
这种方法可以实现1s后执行回调函数里的内容,但使用了回调函数的实现方式,可读性和维护性差
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms))
}
sleep(1000).then( ()=> console.log("finished"));
可以使用Promise优雅的构建sleep方法,避免使用回调函数。
async await实际上只是语法糖,提供同步编程方式实现异步调用。(也就是看上去是同步的,休眠了1s之后做其他的事情,但是实际上是通过异步来实现的。
function sleep(ms){
return new Promise((resolve)=>setTimeout((resolve), ms));
}
//立即执行函数
(async function (){
await sleep(1000);
console.log("fin");
//需要执行的其他的代码
})();
实现每隔1s输出一个数字:
function sleep(ms){
return new Promise((resolve)=>setTimeout((resolve), ms));
}
(async function (){
for(let i = 0; i < 5; i++){
await sleep(1000);
console.log(i);
}
})();
2、执行顺序-1
let promise = new Promise(function(resolve, reject){
console.log("AAA");
resolve();
});
promise.then(() => console.log("BBB"));
console.log("CCC")
// AAA
// CCC
// BBB
Promise新建后会立即执行,所以首先输出AAA
。然后,promise调用resolve(),将.then()方法加入微任务队列,继续执行后面的同步任务,输出CCC
,最后清空微任务队列,最后输出BBB
。
3、执行顺序-2
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<link rel="shortcut icon" href="#">
</head>
<script>
setTimeout(function () {
console.log('setTimeout', document.body);
}, 0);
new Promise((resolve) => {
console.log("hi");
resolve();
}).then(()=>{ console.log('promise', document.body);});
console.log(document.body);
//输出
//hi
//null
//promise null
//setTimout hello
</script>
<body>
<div>hello</div>
</body>
</html>
考点一:Script脚本的加载:脚本比先一步页面加载,如何解决上述的问题?
- 将所有需要的<script>标签都放在</body>之前,确保脚本执行之前完成页面渲染而不会造成页面堵塞问题。
- 合并JS代码,尽可能少的使用script标签
- 通过给script标签增加 defer属性或者是 async 属性来实现,async和defer不同之处是async加载完成后会自动执行脚本,defer加载完成后需要等待页面也加载完成才会执行代码。<script src="file.js" defer></script>
- 动态创建script来加载
function loadJS( url, callback ){
var script = document.createElement('script'),
fn = callback || function(){};
script.type = 'text/javascript';
//IE
if(script.readyState){
script.onreadystatechange = function(){
if( script.readyState == 'loaded' || script.readyState == 'complete' ){
script.onreadystatechange = null;
fn();
}
};
}else{
//其他浏览器
script.onload = function(){ fn(); };
}
script.src = url;
document.getElementsByTagName('head')[0].appendChild(script);
}
//用法
loadJS('file.js', function(){ alert(1);});
考点二:微任务队列,宏任务队列
- setTimeout会将任务加入宏任务队列
- Promise新建后会立即执行,所以会先输出"hi",此时promise的状态为fullfilled,.then中的回调函数加入微任务队列。
- 输出document.body,但因为脚本的加载阻塞的页面的加载,所以输出"null"
- 清空微任务队列,输出"promise null"
- 此时页面的加载已经完成,输出宏任务队列的内容,"setTimeout hello"
4、执行顺序-3
const setDelay = (millisecond) => {
return new Promise((resolve) => {
setTimeout(() => {
console.log("hi");
resolve(`我延迟了${millisecond}毫秒后输出的`)
}, millisecond)
})
}
const setDelaySecond = (seconds) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(`我延迟了${seconds}秒后输出的,是第二个函数`)
}, seconds * 1000)
})
}
setDelay(2000)
.then((result) => {
console.log(result)
console.log('我进行到第一步的');
return setDelaySecond(3)
})
.then((result) => {
console.log('我进行到第二步的');
console.log(result);
})
//输出:
//hi
//setDelay:延迟了2000毫秒后输出
//我进行到第一步的
//我进行到第二步的
//setDelaySecond:延迟了3秒后输出
考点:promise回调函数加入微任务队列的时机
如果在调用then时,如果promise已经resolve了,那么就是fullfilled状态,会将其任务加入微任务队列;如果在调用then时,promise没有resolve或reject,处于pending状态,会将其任务缓存在一个结构里,直到promise被接受或者拒绝时,加入微任务队列。
这里的"hi"和"setDelay:延迟了2000毫秒后输出"是一起输出的,因为它们同时加入微任务队列。
5、执行顺序-4
基础知识:async声明的函数的返回本质上是一个Promise,在await表达式之后的代码可以被认为是存在在链式调用的then回调方法中。
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2() {
console.log('async2');
}
console.log('illegalscript start');
setTimeout(function () {
console.log('setTimeout');
}, 0);
async1();
new Promise(function (resolve) {
console.log('promise1');
resolve();
}).then(function () {
console.log('promise2');
});
console.log('illegalscript end');
输出顺序:
illegalscript start
async1 start
async2
promise1
illegalscript end
async1 end
promise2
setTimeout
理解:执行到await async2的时候,相当于new了一个promise,因此直接console.log('async2');,而console.log('async1 end');相当于promise的.then()方法,被加入到微任务队列里。后面的console.log('promise2');也被加入到微任务队列里,所以会在同步任务执行完成之后,输出微任务队列里的两个console.log。
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2() {
console.log('async2 start');
new Promise((resolve, reject) => {
resolve();
console.log('async2 promise');
})
}
console.log('script start');
setTimeout(function () {
console.log('setTimeout');
}, 0);
async1();
new Promise(function (resolve) {
console.log('promise1');
resolve();
}).then(function () {
console.log('promise2');
}).then(function () {
console.log('promise3');
});
console.log('script end');
输出顺序:
script start
async1 start
async2 start
async2 promise
promise1
script end
async1 end
promise2
promise3
setTimeout
理解:执行到await async2的时候,相当于new了一个promise,因此直接输出async2 start,且async2函数内的promise也是立刻执行的,于是马上输出async2 promise,此时async1 end被加入到微任务队列。
如果将async2函数内的内容改为:
async function async2() {
console.log('async2 start');
return new Promise((resolve, reject) => {
resolve();
console.log('async2 promise');
})//new promise 会立即执行, then会分发到微任务
}
输出顺序:
script start
async1 start
async2 start
async2 promise
promise1
script end
promise2
promise3
async1 end
setTimeout
(依旧不知道为什么async1 end会在最后输出,只能猜测await fn在resolve之后.then()里的东西,放在一个介于微任务队列和宏任务队列的容器中,等到微任务队列中的东西清空后执行)