JS的代码输出题(1)——异步和事件循环

453 阅读10分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路

记录一下我做过的代码输出类题目的过程,每道题有我错误的答案、正确的答案和自己写的答案分析。 如有谬误欢迎批评指正~因为题目太多了,准备分好几篇发……

题目来源:前端面试题之代码输出篇

学习事件循环的的参考资料: 事件循环的原理 演讲从event loop到async await来了

知识点

宏任务与微任务

宏任务: script(整体代码)、setTimeoutsetIntervalsetImmediate、I/O、UI rendering

微任务: Promise.then、Object observe、MutationObserverprocess.nextTick

任务的优先级: proesss.nextTick>promise.then>setTimeout>setImmediate

[​任务的优先级]  此处存疑,暂且先划去

在划分宏任务、微任务的时候并没有提到async/await,因为async/await的本质就是Promise。每次我们使用 await, 解释器都创建一个 promise 对象,然后把剩下的 async 函数中的操作放到 then 回调函数中。

不同类型的任务会进入对应的Event Queue,比如setTimeout和setInterval会进入相同(宏任务)的Event Queue。而Promise和process.nextTick会进入相同(微任务)的Event Queue。

事件循环的过程
  1. 「宏任务」、「微任务」都是队列,一段代码执行时,会先执行宏任务中的同步代码。
  2. 进行第一轮事件循环的时候会把全部的js脚本当成一个宏任务来运行。
  3. 如果执行中遇到setTimeout之类宏任务,那么就把这个setTimeout内部的函数推入「宏任务的队列」中,下一轮宏任务执行时调用。
  4. 如果执行中遇到 promise.then() 之类的微任务,就会推入到「当前宏任务的微任务队列」中,在本轮宏任务的同步代码都执行完成后,依次执行所有的微任务。
  5. 第一轮事件循环中当执行完全部的同步脚本以及微任务队列中的事件,这一轮事件循环就结束了,开始第二轮事件循环。
  6. 第二轮事件循环同理先执行同步脚本,遇到其他宏任务代码块继续追加到「宏任务的队列」中,遇到微任务,就会推入到「当前宏任务的微任务队列」中,在本轮宏任务的同步代码执行都完成后,依次执行当前所有的微任务。
  7. 开始第三轮,循环往复...

异步&事件循环

1. promise状态没有发生变化时,promise.then不会执行
const promise = new Promise((resolve, reject) => {
  console.log(1);
  console.log(2);
});
promise.then(() => {
  console.log(3);
});
console.log(4);

我的错误答案

1 2 4 3

正确答案

1 2 4

解析:由于new里面的代码是同步执行,所以会输出1 2 随后执行到输出4;promise.then虽然是微任务,但是promise的状态没有发生变化,所以then不会执行。

2. 直接打印promise对象会输出状态和结果
const promise1 = new Promise((resolve, reject) => {
  console.log('promise1')
  resolve('resolve1')
})
const promise2 = promise1.then(res => {
  console.log(res)
})

console.log('2', promise2);

我的错误答案

promise1
1, 不知道
2, undefined
resolve1

正确答案

promise1
1 Promise{<fulfilled>: resolve1}
2 Promise{<pending>}
resolve1

解析:

  1. promise1和promise2都是promise对象,怎么会输出undefined呢,我好傻。
  2. 首先同步执行,new里面的代码立即执行,所以会输出promise1;promise1变化后的回调(promise.then)进入微任务队列等待执行;
  3. 随后来到 console.log('1', promise1);,这一句执行的时候promise1已经执行了resolve方法,所以状态已经变成了fulfilled
    • 控制台打印promise对象,如果promise状态没有发生变化,会打印 Promise {<pending>},如果发生了变化,会将状态和结果共同输出( Promise{<resolved>: resolve1})。
  4. 同步代码还没有执行完,执行到本句console.log('2', promise2),但是此时promise2是一个新的Promise对象,还没有执行呢,状态依旧为pending。
  5. 同步代码执行完了,开始查看微任务队列,里面有promise1.then,此时res是'resolve1',所以输出resolve1后结束。
  6. 补充:如果此时在代码后面添加setTimeout,并再次打印promise2,就可以发现promise2的状态已经变成了Promise {<fulfilled>: undefined}
3. 终于做对了一道题
const promise = new Promise((resolve, reject) => {
  console.log(1);
  setTimeout(() => {
    console.log("timerStart");
    resolve("success");
    console.log("timerEnd");
  }, 0);
  console.log(2);
});
promise.then((res) => {
  console.log(res);
});
console.log(4);

我的正确答案

1
2
4
timerStart
timerEnd
success
4. 又对了一道题,不过这次我犹豫了
Promise.resolve().then(() => {
  console.log('promise1');
  const timer2 = setTimeout(() => {
    console.log('timer2')
  }, 0)
});
const timer1 = setTimeout(() => {
  console.log('timer1')
  Promise.resolve().then(() => {
    console.log('promise2')
  })
}, 0)
console.log('start');

我的正确答案

start
promise1
timer1
promise2
time2

解析:我犹豫了,再来捋一下任务队列

  1. 首先执行同步代码,只有一行,输出start
  2. 遇到1-6行,是.then,加入微任务队列;timer1是宏任务(setTimeout),加入宏任务队列
  3. 开始执行微任务队列
    1. 输出promise1
    2. 遇到timer2,是宏任务,加入宏任务队列末尾
    3. 队列里没有其他微任务,进入下一个循环
  4. 开始执行宏任务队列,取出timer1
    1. 先输出timer1
    2. 遇到微任务.then,把.then加入微任务队列
    3. timer1执行完了,
  5. 开始执行微任务队列
    1. 输出promise2
    2. 队列里没有其他微任务,进入下一个循环。
  6. 开始执行宏任务队列,取出timer2,输出timer2
5. 又做对了,Promise的状态一旦变化,就不可更改
const promise = new Promise((resolve, reject) => {
    resolve('success1');
    reject('error');
    resolve('success2');
});
promise.then((res) => {
    console.log('then:', res);
}).catch((err) => {
    console.log('catch:', err);
})

我的正确答案

then: success1
6. 在promise的方法上吃亏了吧
Promise.resolve(1)
  .then(2)
  .then(Promise.resolve(3))
  .then(console.log)

我的迷惑答案

啊这?看不懂啊。

正确答案

1
Promise {<fulfilled>: undefined} // chrome会输出这句,在vscode里不会

解释:

  1. Promise.resolve方法的参数如果是一个原始值,或者是一个不具有then方法的对象,则Promise.resolve方法返回一个新的Promise对象,状态为fulfilled,同时参数传递给回调函数。
  2. then方法接受的参数是函数,如果传入的参数不是函数,那么就将其解释成then(null),导致前一个promise的结果传递给下面。
    • 2和Promise.resolve(3)都不是函数,所以都被解释成了then(null)。
  3. console.log是作为函数传入then的,所以会打印内容。
    1. 传入是参数1
    2. 本题代码在chrome控制台和在vscode里直接执行run code返回的结果不一致
7. 判断任务队列上又傻了吧
const promise1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('success')
  }, 1000)
})
const promise2 = promise1.then(() => {
  throw new Error('error!!!')
})
console.log('promise1', promise1)
console.log('promise2', promise2)
setTimeout(() => {
  console.log('promise1', promise1)
  console.log('promise2', promise2)
}, 2000)

我的错误答案

promise1 Promise{<pending>}
promise2 Promise{<pending>}
promise1 Promise{<fulfilled>:success}
promise2 Promise{<pending>}
error!!!

正确答案

promise1 Promise {<pending>}
promise2 Promise {<pending>}
Uncaught (in promise) Error: error!!!
promise1 Promise {<fulfilled>: "success"}
promise2 Promise {<rejected>: Error: error!!}

解析:

  1. 同步代码
    1. new里面的立即执行,setTimeout1(2)加入宏任务队列;
    2. promise2加入微任务队列
    3. 开始输出,promise1待定(9),promise2待定(10)
    4. setTimeout2(11)加入宏任务。
  2. 检查微任务队列,promise1状态无变化,.then不执行
  3. 宏任务setTimeout1,promise1状态变化(fulfilled)
  4. 检查微任务队列,.then执行,抛出错误,promise2的状态变为失败(rejected),无其他微任务
  5. 宏任务setTimeout2,输出两个状态变化的后的promise。
8. 做不对promise的题了吗?
Promise.resolve(1)
  .then(res => {
    console.log(res);
    return 2;
  })
  .catch(err => {
    return 3;
  })
  .then(res => {
    console.log(res);
  });

我的错误答案

1

正确答案

1
2

解析:resolve传入参数的情况看[6](#6. 在promise的方法上吃亏了吧),promise可以进行链式调用

  1. resolve(1)进入了第一个.then方法
    1. 打印res(1)
    2. return 2相当于返回一个resolve(2)
  2. resolve(2)并没有被catch捕获,顺利的进入了最后的then,所以会输出2。
  3. .catch还是会返回一个promise对象,假如第一个then抛出错误,最后一个then就会输出3了。
9. 返回一个任意非promise的值都会被包裹成promise对象
Promise.resolve().then(() => {
  return new Error('error!!!')
}).then(res => {
  console.log("then: ", res)
}).catch(err => {
  console.log("catch: ", err)
})

我的错误答案

catch: error!!!

正确答案

then:  Error: error!!!

解析:返回一个任意非promise的值都会被包裹成promise对象,因此第二行被包裹成为了return Promise.resolve(new Error('error!!!')),没有出错,因此被then捕获了。如果是throw Error,那么就会被catch捕获了。

10. Promise不可以循环引用
const promise = Promise.resolve().then(() => {
  return promise;
})
promise.catch(console.err)

我的答案

TypeError

正确答案

Uncaught (in promise) TypeError: Chaining cycle detected for promise #<Promise>
11. then的第一个函数处理fulfilled,第二个函数处理reject
Promise.reject('err!!!')
  .then((res) => {
    console.log('success', res)
  }, (err) => {
    console.log('error', err)
  }).catch(err => {
    console.log('catch', err)
  })

我的正确答案

error err!!!
12. catch过的错误还会打印吗?
Promise.resolve()
  .then(function success (res) {
    throw new Error('error!!!')
  }, function fail1 (err) {
    console.log('fail1', err)
  }).catch(function fail2 (err) {
    console.log('fail2', err)
  })

我的错误答案

error!!!
fail2 error!!!

正确答案

fail2 error!!!

解析:已经有catch方法捕获错误了,控制台就不会输出错误了。就好比try···catch···,如果catch里的语句写对了会报错吗,很难的啦。

13. finally的执行顺序
Promise.resolve('1')
  .then(res => {
    console.log(res)
  })
  .finally(() => {
    console.log('finally')
  })
Promise.resolve('2')
  .finally(() => {
    console.log('finally2')
  	return '我是finally2返回的值'
  })
  .then(res => {
    console.log('finally2后面的then函数', res)
  })

我的错误答案

1
finally
finally2
finally2后面的then函数 我是finally2返回的值

正确答案

1
finally2
finally
finally2后面的then函数 2

解析:我看不懂了,开始借助webstrom开始单步调试。

  1. 同步代码:line1→line8→line15
  2. 微任务:
    1. .then line3→line4() 输出 1
    2. .finally line10→11 输出 finally2
    3. .finally line6→line7 输出 finally
    4. .then line14 输出 finally2后面的then函数 2
  3. 理解:每一个promise链上的then都是异步的 :
    1. 第一次执行微任务.then,随后开始执行其他代码。
    2. .finally2加入队列,开始执行
    3. .finally1加入队列,开始执行
    4. 最后执行.then。

finally不管Promise对象最后状态如如何都会执行,而且finally对象的回调函数不接受任何的参数,返回值默认为上一次Promise的对象值,不过如果抛出的是一个异常则返回异常的Promise。

14. finally的错误捕获
Promise.resolve('1')
  .finally(() => {
    console.log('finally1')
    throw new Error('我是finally中抛出的异常')
  })
  .then(res => {
    console.log('finally后面的then函数', res)
  })
  .catch(err => {
    console.log('捕获错误', err)
  })

我的答案

finally1
捕获错误 Error:我是finally中抛出的异常
15. promise.all
function runAsync (x) {
    const p = new Promise(r => setTimeout(() => r(x, console.log(x)), 1000))
    return p
}

Promise.all([runAsync(1), runAsync(2), runAsync(3)]).then(res => console.log(res))

我的答案

1
2
3
[1,2,3]
16. all虽然完事了,但是别人还没完呢
function runAsync (x) {
  const p = new Promise(r => setTimeout(() => r(x, console.log(x)), 1000))
  return p
}
function runReject (x) {
  const p = new Promise((res, rej) => setTimeout(() => rej(`Error: ${x}`, console.log(x)), 1000 * x))
  return p
}
Promise.all([runAsync(1), runReject(4), runAsync(3), runReject(2)])
       .then(res => console.log(res))
       .catch(err => console.log(err))

我的错误答案

Error:4

正确答案

//1s后输出
1
3
//2s后输出
2
Error:2
//4s后输出
4

解析:

  1. promise.all在2秒后遇见了错误,于是不等了,返回错误结果,由catch捕获了,但是里面的事件没有停止执行。
  2. all只有里面的Promise全是resolve才会正常返回各个promise的结果数组。
17.race:跑得最快才能被then捕获
function runAsync (x) {
  const p = new Promise(r => setTimeout(() => r(x, console.log(x)), 1000))
  return p
}
Promise.race([runAsync(1), runAsync(2), runAsync(3)])
  .then(res => console.log('result: ', res))
  .catch(err => console.log(err))

我的正确答案

1
result: 1
2
3
18.race:跑得最快就行,错误也可以被捕获
function runAsync(x) {
  const p = new Promise(r =>
    setTimeout(() => r(x, console.log(x)), 1000)
  );
  return p;
}
function runReject(x) {
  const p = new Promise((res, rej) =>
    setTimeout(() => rej(`Error: ${x}`, console.log(x)), 1000 * x)
  );
  return p;
}
Promise.race([runReject(0), runAsync(1), runAsync(2), runAsync(3)])
  .then(res => console.log("result: ", res))
  .catch(err => console.log(err));

我的正确答案

0
Error:0
1
2
3
19. async await到底在干什么?
async function async1() {
  console.log("async1 start");
  await async2();
  console.log("async1 end");
}
async function async2() {
  console.log("async2");
}
async1();
console.log('start')

我的错误答案

async1 start
start
async2
async1 end

正确答案

async1 start
async2
start
async1 end

解析:在这里先补补知识:在mdn上看async async/await是什么?

明白了await在等什么就明白了,可以把await看做一个new promise,await后面的内容被加入了promise.then中等待执行。所以await里面的函数同步执行;执行完了跳出async1,开始输出start。

20. 还是await
async function async1() {
  console.log("async1 start");
  await async2();
  console.log("async1 end");
  setTimeout(() => {
    console.log('timer1')
  }, 0)
}
async function async2() {
  setTimeout(() => {
    console.log('timer2')
  }, 0)
  console.log("async2");
}
async1();
setTimeout(() => {
  console.log('timer3')
}, 0)
console.log("start")

我的错误答案

async1 start
async2
start
timer2
async1 end
timer3
timer1

正确答案

async1 start
async2
start
async1 end
timer2
timer3
timer1

解析:line4和line5输出错误,本质上是对队列不够了解。async2里面的setTimeout里面的东西被推入宏队列,async2的使命就结束了,不堵塞后面了。输出完async1 end以后,此时没有微任务了,宏任务队列里先进先出。