LazyMan 是什么
来看一下 LazyMan 的调用方式:
new LazyMan('Tony').eat('lunch').eat('dinner').sleep(10).sleepFirst(5).eat('junk food');
以及预期的运行结果:
// Hi I am Tony
// 等待了5秒...
// I am eating lunch
// I am eating dinner
// 等待了10秒...
// I am eating junk food
观察可知,LazyMan 要实现的几个功能有:
- 构造时接收名字,并打印.
- 链式调用.
.eat(food)
:直接打印..sleep(t)
:真实的等待了 t 秒..sleepFirst(t)
:与sleep
基本功能 一样,但它必须紧跟着打印名字之后执行。
async & await 简单介绍
要解决这道题,我们需要先了解一下 async & await,其原理在我之前的博客中有介绍过。其基本使用方式如下:
async function read() {
let content = await xxx
...
}
await
后面的代码会被 Promise.resolve()
进行解析,通过 .then()
方法接收的 onFulFilled
回调函数将 promise
的成功结果通过 next
赋值给 content
(promise
是失败的话,需要用 try & catch
,此处不做考虑),同时,next
使得接下来的代码得以运行。
结论就是: await
右边的 promise
(resolve
过的) 在状态变成 fulFilled
之后,通过 .then
并执行 onFulFilled
回调(放入微任务队列),才会让接下来的代码执行。
async & await 中的 setTimeout
之所以要讨论这个内容,是因为我之前以为 await
会等待任何的异步代码执行完成,我太想当然了。
我们知道 setTimeout
是宏任务,次于微任务执行。来看一个例子,
async function fn() {
let res = await new Promise((resolve, reject) => {
setTimeout(() => {
console.log(1)
resolve(2)
}, 1000)
})
console.log(res)
}
fn()
打印结果
因为
await
需要等待的 promise
状态变成 fulFilled
才能往下执行,所以必须等待 setTimeout
中的回调被执行完成,才能调用 resolve(2)
。
接下来,我们将 resolve(2)
放在 setTimeout
外面
async function fn() {
let res = await new Promise((resolve, reject) => {
setTimeout(() => {
console.log(1)
}, 1000)
resolve(2)
})
console.log(res)
}
fn()
打印结果
其中 2 是很快就打印出来的,而 1 是等待了一秒之后打印的。
再极端一点,我们不给 promise 改变状态的机会
async function fn() {
try {
let res = await new Promise((resolve, reject) => {
setTimeout(() => {
console.log(1)
}, 1000)
// resolve(2)
})
console.log(res)
} catch (err) {
console.log(err)
}
}
fn()
结果
我们可以看到,只要 promise 没有状态,等到宏任务执行完了,await 下面的代码也执行不了,而且这也不会抛出一种错误,所以尽量不要这样操作。
得出结论:await 等待的是 promise 的状态变化,而不是等待任何异步的代码执行完成! 这其实在第二部分对原理的分析中可以很明确的知道,虽然我之前了解过原理,也只是想当然的去思考,千万不要想当然!
LazyMan 的实现
理解了上面的内容之后,对于实现一个 LazyMan
还是不难的。要点就是要将 promsie
状态的变化放在 setTimeout
中,这样才能让 await
对 setTimeout
进行等待。
class LazyMan {
constructor(name) {
this.name = name
this.stack = []
this.timer = null
this.sayHi()
}
sayHi() {
console.log('Hi I am ' + this.name)
}
eat(food) {
this.stack.push(() => {
console.log('I am eating ' + food)
})
return this.next()
}
sleep(time) {
this.stack.push(() => {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('等待了' + time + '秒...')
resolve()
}, time * 1000)
})
})
return this.next()
}
sleepFirst(time) {
this.stack.unshift(() => {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('等待了' + time + '秒...')
resolve()
}, time * 1000)
})
})
return this.next()
}
next() {
clearTimeout(this.timer)
//只是设置了定时器,放在宏任务队列中
this.timer = setTimeout(async () => {
for (let i = 0; i < this.stack.length; i++) {
await this.stack[i]()
}
}, 0)
return this
}
}
对其他的细节做一下解释:
- 链式调用,返回当前实例即可,这里不用像
Promise
那样new
一个。 - 因为不知道链式调用的次数(不用链式也可以),所以每链式调用之后就要对队列中的函数进行执行,当下一次调用的时候,会将计时器清空,因为同步代码优于宏任务执行,所以计时器设置为 0 也不会在链式输入读完之前执行,通过这种防抖的方式,能保证在最后一次链式输入之后,队列中的任务能一次执行。
sleepFirst
只需要直接放入到队列的最前端即可(不考虑多个sleepFirst
调用)。