弑君者

V1

2022/04/14阅读:15主题:默认主题

一道有意思的面试题

浏览器的事件循环

浏览器的事件循环,相信大家都很了解了,可是我们有没有深入思考过呢?

比如为什么会有宏任务?

为什么会有微任务?

能不能去掉微任务只用宏任务或者是只用宏任务不用微任务?

如果让你设计一个事件循环系统,你会如何设计?

或者说目前的事件循环,有哪些你觉得还可以改进的地方,如果是你,你会如何改进呢?

如果脱离了浏览器,你会如何设计?

你的业务场景下,有没有使用类似思想的场景,或者是你使用循环思想处理过那些业务场景?

任何事情,相信只有深入思考,我们都会有不同的感悟和不同收获。

事件循环来解决实际问题

理论说了很多,最后还是要有落地的地方,现在我们来看看如何利用事件循环做一些事情。

假如是让你实现以下功能?

功能1

Lazyman("hack")
输出 
HI,This is hack

这个功能很简单,可以很简单的实现

class MyLazyMan {
  constructor(name) {
    this.name = name;
    console.log(`HI,This is ${this.name}`);
  }

}
function LazyMan(name{
  return new MyLazyMan(name)
}
LazyMan("hack")

那么我们假如我们加上其它的一些函数呢,如果是加上一个sleep和函数和eat函数呢,比如以下功能?

你会怎么实现呢?

功能2

LazyMan("Hank").sleep(1).eat("dinner")
输出 
Hi! This is Hank!
// 等待1秒..
Wake up after 1
Eat dinner

这里功能其实就是经常见到的就是一个收集依赖,然后再恰当的时机进行调用

这里就是在sleep之后,进行调用就可以了。

 class MyLazyMan {
  constructor(name) {
    this.name = name;
    this.callback = [];
    this.timeId = null;
    console.log(`HI,This is ${this.name}`);
  }
  sleep(time) {
    if(this.timeId) {
      clearTimeout(this.timeId)
    }
    this.timeId = setTimeout(() => {
      console.log(`Wake up after ${time}`);
      this.callback.forEach((fn) =>fn());
      return this;
    }, 1000*time);
    return this;
  }
  eat(food) {
    this.callback.push(() => console.log(`Eat ${food}`))
    return this;
  }

}
function LazyMan(name{
  return new MyLazyMan(name)
}
// LazyMan("hack")

new LazyMan("hack").sleep(10).eat("apple")

上面的功能已经实现了,假如现在有以下功能3,又该如何实现呢?

功能3

LazyMan("Hank").eat("dinner").eat("supper")
输出 
Hi This is Hank!
Eat dinner
Eat supper

这里和功能2唯一不一样的,就是因为没有调用sleep,所以此时可以根据是不是调用了sleep来确定是不是要立即调用eat方法

 class MyLazyMan {
  constructor(name) {
    this.name = name;
    this.callback = [];
    this.timeId = null;
    console.log(`HI,This is ${this.name}`);
  }
  sleep(time) {
    if(this.timeId) {
      clearTimeout(this.timeId)
    }
    this.timeId = setTimeout(() => {
      console.log(`Wake up after ${time}`);
      this.callback.forEach((fn) =>fn());
      return this;
    }, 1000*time);
    return this;
  }
  eat(food) {
    if(this.timeId) {
      this.callback.push(() => console.log(`Eat ${food}`))
    }else {
      console.log(`Eat ${food}`)
    }

    return this;
  }

}
function LazyMan(name{
  return new MyLazyMan(name)
}
// LazyMan("hack")

// LazyMan("hack").sleep(1).eat("apple")

LazyMan("hank").eat("dinner").eat("supper")

最后看下功能4,这里会增加一个sleepFirst函数,用来延迟前面构造函数的调用,这个时候可能就需要延迟首次执行的函数的时间,所以这个时候就需要在第一次执行构造函数的时候,收集到依赖,然后在sleepFirst里边进行调用,否则执行顺序就会不对。

所以这里的整体逻辑

功能4

LazyMan("Hank").sleepFirst(5).eat("supper")
输出 
// 等待5秒
Wake up after 5
Hi This is Hank!
Eat supper

代码如下

class MyLazyMan {
      constructor(name) {
        this.name = name;
        this.callback = [];
        this.timeId = null;
        this.timeFirstId = null;
        this.callback.push(() => {
          console.log(`HI,This is ${this.name}`);
        })
        this.selfTime = setTimeout(() => {
          console.log(`HI,This is ${this.name}`);
        },0)
      }
      sleep(time) {
        if(this.timeId) {
          clearTimeout(this.timeId)
        }
        if(this.selfTime) {
          clearTimeout(this.selfTime)
        }
        if(this.callback.length>0) {
         const first = this.callback.shift();
         first();
        }
        this.timeId = setTimeout(() => {
          console.log(`Wake up after ${time}`);
          this.callback.forEach((fn) =>fn());
          this.callback.length = 0;
          return this;
        }, 1000*time);
        return this;
      }
      eat(food) {
        if(this.selfTime) {
          clearTimeout(this.selfTime)
        }
        if(this.timeId || this.timeFirstId) {
          this.callback.push(() => console.log(`Eat ${food}`))
        }else {
          if(this.callback.length>0) {
            const first = this.callback.shift();
            first();
          }
          console.log(`Eat ${food}`)
        }

        return this;
      }
      sleepFirst(time) {
        if(this.selfTime) {
          clearTimeout(this.selfTime)
        }
        if(this.timeFirstId) {
          clearTimeout(this.timeFirstId)
        }else {
          this.timeFirstId = setTimeout(()=> {
            this.callback.forEach((fn) =>fn());
            this.callback.length = 0;
            return this;
          },1000*time)
        }
        return this;
      }
    }
    function LazyMan(name{
      return new MyLazyMan(name)
    }
    // LazyMan("hack")

    // LazyMan("hack").sleep(1).eat("apple")

   // LazyMan("hank").eat("dinner").eat("supper")
   LazyMan("Hank").sleepFirst(5).eat("supper")

其它实现

通过上面的实现,我们发现需要解决两个问题,

第一 如何可以按照延迟前面函数的执行,先把依赖收集完成之后,等到合适的时机按照顺序执行。

解决这1个问题,我们已经知道的可以延迟执行函数的方法有setTimeout和Promise还有async和await方法。

所以现在的就是通过某种方法利用我们已经知道的方法解决上面的两个问题。

1. callback

callback是基于队列进行实现的,那么是如何解决上面的两个问题的呢?

第一 如何可以按照延迟前面函数的执行,先把依赖收集完成之后,等到合适的时机按照顺序执行。

这里是通过形成一个链表,首先使用setTimeout先把链表的第一个函数放入到宏任务里边等待下一次执行。 同时巧妙的利用指向链表的第一个元素的指针,当指针的内容变化的时候,对执行顺序没有影响来巧妙的实现sleepFirst


class LazyMan {
  constructor(name) {
    this.name = name
    this.sayName = this.sayName.bind(this)
    this.next = this.next.bind(this)
    this.queue = [this.sayName]
    setTimeout(this.next, 0)
  }

  callByOrder(queue) {
    let sequence = Promise.resolve()
    this.queue.forEach(item => {
      sequence = sequence.then(item)
    })
  }
  
  next(){
   const currTask = this.queue.shift()
    currTask && currTask()
  }

  sayName() {
    console.log(`Hi! this is ${this.name}!`)
    this.next()
  }

  holdOn(time) {
    setTimeout(() => {
      console.log(`Wake up after ${time} second`)
      this.next()
    }, time * 1000)
  }

  sleep(time) {
    this.queue.push(this.holdOn(time))
    return this
  }

  eat(meal) {
    this.queue.push(() => {
      console.log(`eat ${meal}`)
      this.next()
    })
    return this
  }

  sleepFirst(time) {
    // 这里通过改变链表指向的第一个元素的内容,来巧妙的实现可以前面先收集依赖,延迟执行操作
    this.queue.unshift(this.holdOn(time))
    return this
  }
}

2 Promise

我们都知道promise同样可以改变函数的执行顺序,可以延后函数的执行,那就是还是同样的问题,如何解决执行顺序的问题

第一 如何可以按照延迟前面函数的执行,先把依赖收集完成之后,等到合适的时机按照顺序执行。

那么使用Promise 如何来解决这个问题呢?

我们也可以自己思考下,带着问题找到答案比答案有趣多了😁

这里是通过设置一个变量 this._preSleepTime ,通过这个变量 this._preSleepTime 来控制函数的执行,因为then函数是在微任务那边执行,所以当执行的时候此时 this._preSleepTime 已经改变了,所以此时执行顺序可以根据 this._preSleepTime 这个变量的状态来改变

class LazyMan {
  constructor(name) {
    this.name = name
    this._preSleepTime = 0
    this.sayName = this.sayName.bind(this)
    this.p = Promise.resolve().then(() => {
      // 使用这个变量,因为then是在后面执行,所以如果执行的时候, this._preSleepTime 改变了,那么此时的执行的函数也会改变了
      if (this._preSleepTime > 0) {
        return this.holdOn(this._preSleepTime)
      }
    }).then(this.sayName)
  }

  sayName() {
    console.log(`Hi! this is ${this.name}!`)
  }

  holdOn(time) {
    return new Promise(resolve => {
      setTimeout(() => {
        console.log(`Wake up after ${time} second`)
        resolve()
      }, time * 1000)
    })
  }

  sleep(time) {
    this.p = this.p.then(
      () => this.holdOn(time)
    )
    return this
  }

  eat(meal) {
    this.p = this.p.then(() => {
      console.log(`eat ${meal}`)
    })
    return this
  }

  sleepFirst(time) {
    this._preSleepTime = time
    return this
  }
}

3 Promise + 队列

可以把前面两种方法综合起来处理,

第一 如何可以按照延迟前面函数的执行,先把依赖收集完成之后,等到合适的时机按照顺序执行。

这里其实就很简单了,还是利用链表的特殊性和Promise 一起来处理,也是把事件都推入到链表中。

class LazyMan {
  constructor(name) {
    this.name = name
    this.sayName = this.sayName.bind(this)
    this.queue = [this.sayName]
    // 这里推入到下一个微任务中
    Promise.resolve().then(() => this.callByOrder(this.queue))
  }

  callByOrder(queue) {
    let sequence = Promise.resolve()
    this.queue.forEach(item => {
      sequence = sequence.then(item)
    })
  }

  sayName() {
    return new Promise((resolve) => {
      console.log(`Hi! this is ${this.name}!`)
      resolve()
    })
  }

  holdOn(time) {
    return () => new Promise(resolve => {
      setTimeout(() => {
        console.log(`Wake up after ${time} second`)
        resolve()
      }, time * 1000)
    })
  }

  sleep(time) {
    this.queue.push(this.holdOn(time))
    return this
  }

  eat(meal) {
    this.queue.push(() => {
      console.log(`eat ${meal}`)
    })
    return this
  }

  sleepFirst(time) {
    this.queue.unshift(this.holdOn(time))
    return this
  }
}

4 Promise + async

这里的思路和使用Promise思路差不多,就是使用async来顺序执行队列

class LazyMan {
  constructor(name) {
    this.name = name
    this.sayName = this.sayName.bind(this)
    this.queue = [this.sayName]
    setTimeout(async () => {
      for (let todo of this.queue) {
        await todo()
      }
    }, 0)
  }

  callByOrder(queue) {
    let sequence = Promise.resolve()
    this.queue.forEach(item => {
      sequence = sequence.then(item)
    })
  }

  sayName() {
    return new Promise((resolve) => {
      console.log(`Hi! this is ${this.name}!`)
      resolve()
    })
  }

  holdOn(time) {
    return () => new Promise(resolve => {
      setTimeout(() => {
        console.log(`Wake up after ${time} second`)
        resolve()
      }, time * 1000)
    })
  }

  sleep(time) {
    this.queue.push(this.holdOn(time))
    return this
  }

  eat(meal) {
    this.queue.push(() => {
      console.log(`eat ${meal}`)
    })
    return this
  }

  sleepFirst(time) {
    this.queue.unshift(this.holdOn(time))
    return this
  }
}

5 不使用class

这里的思路其实和第二种思路差不多的,也是通过链表来修改,保证在执行的时候,执行到对应的函数


function lazyMan (name{
    const tasks = []
    const methods = {
        say(name) {
            tasks.push(() => console.log(`Hi! This is ${name}`))
            return this
        },
        eat(food) {
            tasks.push(() => console.log(`Eat ${food}`))
            return this
        }, 
        sleepFirst(time) {
            tasks.unshift(() => new Promise(resolve => setTimeout(resolve, time * 1000)))
            return this
        },  
        sleep(time) {
            tasks.push(() => new Promise(resolve => setTimeout(resolve, time * 1000)))
            return this;
        }
    }

    setTimeout(function run({
        if(!tasks.length) return
        Promise.resolve(tasks.shift()()).then(run)
    }, 0)
    methods.say(name)

    return methods
}

总结

我们可以学到什么呢?

我理解到了一个运行态的概念,比如我们在这里解决该问题的时候,大多数时候是先把函数push到待运行的队列中,然后改变某些状态,等待改变运行的时候,函数的逻辑就根据改变的状态执行不同的逻辑。

分类:

前端

标签:

前端

作者介绍

弑君者
V1