LazyMan
一开始我还以为这是考懒加载的题目,后来我才发现这是一道考验数据结构和异步编程功底的题目
先看第一题,实现以下功能
LazyMan("Hank")
// 你好,我是 Hank
你一看,这不傻子都会吗,直接
LazyMan=(name)=>{
console.log(`你好,我是${name}`)
}
好,我们来看第二题,再添加以下功能
LazyMan("Hank").sleep(10).eat("lunch")
//
你好,我是 Hank
(沉默十秒)
我醒了,我刚睡了 10 秒
吃午餐
你一看,这中间停 10s 不就行了嘛
LazyMan=(name)=>{
console.log(`你好,我是${name}`)
const api={
sleep(n){
let t1 = Date.now()
while(Date.now() - t1 < 10000){}
console.log(`我醒了,我刚睡 了${n}秒`)
return api
},
eat(type){
console.log(type==='lunch'?'吃午餐':'')
return api
}
}
return api
}
一开始我还有个疑问,那 sleep 不能使用定时器吗,结果是不可以,那样的话你还没睡醒,午餐就飞到了你的脸上,人在床上躺,饭从天上来
你现在可能会感到小有成就感,那么我们添加最后一个功能
LazyMan("Hank").sleepFirst(5).eat("supper")
相信和我一样的小伙伴看到这个需求的第一眼,心里想,这是什么变态需求,时间是不可逆的,你竟然让我把现在的代码带回以前的时空?
这时我们写的代码显然完成不了这个功能,我们需要换个思路,既然我不确定你是否要现在执行,那么我就先储存下来
LazyMan = (name) => {
const queue = []
const say = () => {console.log(`你好,我是${name}`);next()}
queue.push(say)
const next = () => {
const first = queue.shift()
first?.()
}
const api = {
queue,
sleep(n) {
const task = () => {
setTimeout(() => {
console.log(`我醒了,我刚睡 了${n}秒`)
next()
}, n * 1000)
}
queue.push(task)
return api
},
eat(type) {
const task = () => {
console.log(type === 'lunch' ? '吃午餐' : '')
next()
}
queue.push(task)
return api
},
sleepFirst(n) {
const task = () => {
setTimeout(() => {
console.log(`我醒了,我刚睡了${n}秒`)
next()
}, n * 1000)
}
queue.unshift(task)
return api
},
}
setTimeout(()=>next())
return api
}
我们只需要把需要做的函数排好队,依次执行即可,数组的 unshift 方法很容易让 sleepFirst 插队
next 方法标记每个函数的结束,它会去取出并执行数组中的第一个函数
第一次事件循环过程如下
首先传入了 name,声明了 say 方法,并且立即放入数组,此时数组只有一项,然后声明 api 对象,遇到定时器,放入宏任务队列,返回 api 对象
然后调用对象的 sleepFirst 方法,声明 task,并插队,此时数组第一项是 sleepFirst 的回调,第二项才是 say 的回调,返回 api 对象
最后调用 eat 方法,声明 task 并放入数组,此时数组中函数的顺序已经正确,并且第一个宏任务 script 已经执行完毕
第二次事件循环
此时没有微任务,所以先执行最开始放入的宏任务,也就是定时器,next 方法在此时被第一次调用
next 首先会执行数组中第一个函数,也就是 sleepFirst 的回调,在其回调中 next 还会被调用,将剩余函数依次执行
所以第一次 next 不能设置为同步代码,否则在 next 执行时数组中只有一个 say 方法,执行后,虽然数组中存在函数,但是激活数组的钥匙已经使用过了,这就是异步的魅力