用JavaScript模拟事件循环

303 阅读4分钟

本文属于原创文章,转载请注明--来自桃源小盼的博客

哇叽哇叽

对于很多概念性的原理,可能三两句话就能概括,但必然损失了很多细节。而实际的代码呢,无法忽略细节,最多是简化一些。

那么就让我们一起来用伪代码来模拟事件循环机制吧。

Talk is cheap. Show me the code.

说起来容易做起来难,历史上的马谡可能是最佳反面代表人物了。

为什么是事件循环机制,而不是别的机制?

js主线程要做各种类型的任务,例如:dom事件、布局计算、js任务、用户输入、动画、定时器。

如何解决未来的新任务?

各种事件不可能是同一时间执行,会在未来产生新的事件,所以就需要有一个机制像前台接待员一样,一直守在那里,时刻检测是否有新任务了,一有新任务就执行它,这就是事件循环机制。

while(true) {
  doSomething()
}

如何解决积攒的新任务?

新任务太多了,前台接待员无法同时处理多个任务,只能让大家排队了,这就是任务队列机制。

为什么无法同时处理多个任务?因为js(渲染进程的主线程)是单线程执行模式。

队列是先进先出的数据结构,在js中可以理解为数组。

const queue = []
const stop = false

while(true) {
  const task = queue.unshift()
  task()

  // 退出标志
  if (stop) {
    break
  }
}

高优先级任务被阻塞了

如果只有一个消息队列,那么高优先级的任务一直在等待,可能会产生页面卡顿。 所以按照任务的类型分了几种队列。优先级依次向下。

  • 用户交互
  • 合成页面
  • 默认(资源加载、定时器等)
  • 空闲(垃圾回收等)
class Queue {
  handleQueue = []  // 交互队列
  composeQueue = [] // 合成队列
  baseQueue = []    // 默认队列
  freeQueue = []    // 空闲队列

  // 插入新任务
  add(task, type) {
    if (type === 'handle') {
      this.handleQueue.push(task)
    } else if (type === 'compose') {
      this.composeQueue.push(task)
    } else if (type === 'base') {
      this.baseQueue.push(task)
    } else if (type === 'free') {
      this.freeQueue.push(task)
    }
  }

  // 获取一个任务
  get() {
    const queue = []
    if (handleQueue.length > 0) {
      queue = handleQueue
    } else if (composeQueue.length > 0) {
      queue = composeQueue
    } else if (baseQueue.length > 0) {
      queue = baseQueue
    } else if (freeQueue.length > 0) {
      queue = freeQueue
    }
    return queue.unshift()
  }
}

const queue = new Queue()
const stop = false

while(true) {
  const task = queue.get()
  task()

  // 退出标志
  if (stop) {
    break
  }
}

页面在不同阶段,高优目标是不同的

页面在加载阶段,第一目标是先把页面渲染出来。 页面在交互阶段,第一目标是及时响应用户的操作。

为了满足不同阶段的目标,需要调整不同阶段任务队列的优先级。

natapp1

class Queue {
  handleQueue = []
  composeQueue = []
  baseQueue = []
  freeQueue = []
  priority = []
  // 设置优先级
  setPriority(lifecycle) {
    if (lifecycle === 'pageload') { // 页面加载
      this.priority = ['baseQueue', 'handleQueue', 'composeQueue', 'freeQueue']
    }
    else if (lifecycle === 'handle') { // 交互阶段
      this.priority = ['handleQueue', 'composeQueue', 'baseQueue', 'freeQueue']
    } else if (lifecycle === 'free') { // 空闲阶段
      this.priority = ['baseQueue', 'handleQueue', 'freeQueue', 'composeQueue']
    }
  }

  get() {
    const curr = []
    // 根据优先级顺序来获取任务
    this.priority.forEach(priority => {
      const queue = this[priority]
      if (queue.length > 0) {
        return queue.unshift()
      }
    })
  }
  // 省略
  add(task, type) {}
}

const queue = new Queue()
const stop = false

queue.setPriority('pageload')

while(true) {
  const task = queue.get()
  task()

  // 退出标志
  if (stop) {
    break
  }
}

如何在渲染前做一些任务?

有时候我们想在当前任务完成前再紧接着做一些任务,但是如果插入到队伍末尾,那么需要的时间可能长,可能短,这就无法稳定地按照预期来做了。

所以增加了微任务队列,在当前任务即将完成时,再执行一些事情,不用等太久。

class Task {
  microQueue = []
  // 执行任务
  do() {
    // start doSomething
    // doSomething
    // end doSomething
    // 检查微任务队列
    if (microQueue.length > 0) {
      microQueue.forEach(microTask => microTask())
    }
  }
  // 添加微任务
  addMicro(microTask) {
    this.microQueue(microTask)
  }
}

// 省略,同上
class Queue {
  add(task, type) {}

  get() {}

  setPriority(lifecycle) {}
}

const queue = new Queue()
queue.add(new Task(), 'base')

while(true) {
  const task = queue.get()
  task.do()

  // 退出标志
  if (stop) {
    break
  }
}

低级任务饿死现象

一直在执行高优任务,低级任务就会出现饿死现象,所以连续执行一定数量的高优任务后,需要执行一次低级任务。

异步回调

这里先说一个常识,js虽然是单线程执行,但是浏览器却是多进程的。

一个异步任务,可能是由浏览器的其他进程或者线程去执行,然后再将执行结果利用 IPC 的方式通知渲染进程,之后渲染进程再将对应的消息添加到消息队列中。

setTimeout实现机制有何不同之处?

由于存在时间的概念,并不能直接放入消息队列中。浏览器又增加了一个延迟队列,还有其他的一些延迟任务都在这里执行。每次执行完消息队列中的一个任务,就要检查一遍延迟队列。

const delayQueue = []

// 检查延迟队列中的任务,是否到时间了
function checkDelayQueue () {
  delayQueue.map(task => {
    if ('到期了') {
      task()
    }
  })
}

// 省略
class Queue {}

const queue = new Queue()

while(true) {
  const task = queue.get()
  task.do()

  checkDelayQueue()

  // 退出标志
  if (stop) {
    break
  }
}

结尾

以上代码不是实际的浏览器实现,只是为了更好理解事件循环机制提供帮助。

希望你也写出自己的实现版本。

参考

  1. 《浏览器工作原理与实践》
  2. 《JavaScript忍者秘籍》