美文网首页
从 setTimeout 看浏览器对 Event Loop 的实

从 setTimeout 看浏览器对 Event Loop 的实

作者: 喜悦的狮子 | 来源:发表于2020-09-06 22:40 被阅读0次
JavaScript 第 1 篇

这篇博客起源于以下面试题,由此开始查资料回顾 setTimeout 到底是如何运行的。希望你可以通过这篇文章了解 setTimeout 运行机制、Event Loop 的概念以及浏览器(Chromium)对 Event Loop 的实现。本篇没有讲到的宏任务和微任务,下次一定。

请修改下面的代码, 使其可以输出0-9,至少写出三种不同的解法
for (var i = 0; i< 10; i++){
    setTimeout(() => {
    console.log(i);
    }, 1000)
}

以下是Chromium 源码地址

Event Loop 是什么?

在维基百科中的定义是 Event Loop是一个程序结构,用于等待和发送消息和事件。

Javascript 是单线程的,即同一时间只能做一件事,这种方式一方面避免了多线程之间 DOM 操作的冲突,另一方面也导致了阻塞的可能性。 图1 引用自《什么是 Event Loop?》

如图1,如果线程中有一个任务耗时过长就会导致整个线程的阻塞,大大影响用户体验。

而 Event Loop 就是为了解决单线程阻塞的问题,让整个线程能够高效得运行


图2 引用自Philip Roberts的演讲《Help, I'm stuck in an event-loop》

在上图中,主线程执行时会产生 堆(heap)与栈(stack),栈中的任务执行是同步的,它们会调用 DOM 操作,ajax 请求,定时器 setTimeout,而这这些请求都会被添加到 消息队列(callback queue)。

只有当栈中的任务全部被执行完毕后,主线程才会去执行消息队列中的任务。

举个简单的例子

console.log(1);
setTimeout(()=>{console.log(2)}, 1000)
console.log(3)

以上的代码与下面的代码结果一致。

setTimeout(()=>{console.log(2)}, 1000)
console.log(1);
console.log(3)

setTimeout 是如何运行的?

setTimeout 的 使用请看 菜鸟教程,需要注意的是,它的第 3 个及后续的参数可以传递给第一个参数(函数),可以用来实现闭包

setTimeout是一个定时器,可以指定某个函数在多少秒后执行,相比较一般的异步函数添加到消息队列中等待被主线程调用,为了在指定时间被执行,setTimeout 中的函数被添加到了延迟执行队列中。

延迟执行队列在 Chromium 的定义如下

DelayedIncomingQueue delayed_incoming_queue;

当通过 JavaScript调用 setTimeout 设置回调函数的时候,渲染进程会创建一个回调任务,
包含了回调函数、当前发起时间、延迟执行时间,然后将该任务添加到延迟队列中。

关于延迟执行队列的实现放在了最后。

struct DelayTask{
  int64 id;
  CallBackFunction fn;
  int start_time;
  int delay_time;
};
DelayTask Task;
Task.fn = sayHello;
Task.start_time = getCurrentTime(); // 获取当前时间
Task.delay_time = 1000;// 设置延迟执行时间

下面就让我们一起来看一下浏览器是如何实现 Event Loop 的吧!

事件循环

任务并不是在线程运行之前就全部安排好的,为了安排在运行过程中加入的任务,需要采取任务循环的机制。

图3 引用自《浏览器工作原理与实践》

如图3 在进程启动时,便会创建一个类似于 while(true)的循环,每执行一次循环体的过程称为Tick。每个Tick的过程就是查看是否有事件待处理,如果有,就取出事件以及相关的回调函数并执行。

// 示例代码
// GetInput
// 等待用户从键盘输入 1 个数字,并返回。
int GetInput() {
  int intput_number = 0;
  cout <<"请入 1 个数字";
  cin>>input_number;
  return input_number
}
// 主线程 (Main Thread)
void MainThread(){
     while(true){
          int num = GetInput();
          print(" 输入数字为:%d",num );
      }
}

消息队列

有了事件循环,就可以接受新的任务了,不过此时的任务都来自线程内部,如果此时有外部的 IO 线程,比如鼠标点击事件,ajax 请求该如何处理呢?

通用的模式是消息队列。


图4

如图4,消息队列是一个先进先出的队列数据结构,添加任务需要在队列的尾部,然后在头部取出,有了消息队列后我们的Event Loop 就比较完整了。

如图5,此时我们添加了一个消息队列,IO 线程中产生的新任务会添加到消息队列的尾部,主线程会循环得从消息队列头部中读取任务并执行

图5
// 首先,构造一个队列。

class TaskQueue{
  public:
  Task takeTask(); // 取出队列头部的一个任务
  void pushTask(Task task); // 添加一个任务到队列尾部
};

// 接下来,改造主线程,让主线程从队列中读取任务:
TaskQueue task_queue;
void ProcessTask();
void MainThread(){
  while(true){
    Task task = task_queue.takeTask();
    ProcessTask(task);
  }
}

// 在上面的代码中添加了一个消息队列的对象,然后在主线程的循环代码块中,从消息队列中读取一个任务,
//然后执行该任务,主线程就这样一直循环往下执行,因此只要消息队列中有任务,主线程就会去执行。

// 这样改造后,主线程执行的任务都全部从消息队列中获取。
// 所以如果有其他线程想要发送任务让主线程去执行,只需要将任务添加到该消息队列中就可以了,添加任务的代码如下:

Task clickTask;
task_queue.pushTask(clickTask)

延迟执行队列

因为消息队列是先进先出的,所以直接把定时器任务放到消息队列中,不能保证任务在指定时间被执行。为了解决这一问题,浏览器为定时器一类的任务特别安排了延迟执行队列。

void TimerTask(){
  // 从 delayed_incoming_queue 中取出已经到期的定时器任务
  // 依次执行这些任务
}
 
TaskQueue task_queue;
void ProcessTask();
bool keep_running = true;
void MainTherad(){
  while(true){
    // 执行消息队列中的任务
    Task task = task_queue.takeTask();
    ProcessTask(task);
    
    // 执行延迟队列中的任务
    ProcessDelayTask()
 
    if(!keep_running) // 如果设置了退出标志,那么直接退出线程循环
        break; 
  }
}

在上面的代码中专门添加了一个ProcessDelayTask 函数,用来处理延迟执行函数,处理完消息队列中的一个任务之后,就会开始执行ProcessDelayTask 函数。该函数会根据发起时间和延迟时间计算出到期的任务,依次执行所有的到期任务。执行完毕后,再进入下一个循环。

参考文章

什么是 Event Loop?
JavaScript 运行机制详解:再谈Event Loop
带你彻底弄懂Event Loop
chromium码仓库
极客时间-浏览器工作原理与实践
chromium浏览器开发系列第三篇:chromium源码目录结构

相关文章

网友评论

      本文标题:从 setTimeout 看浏览器对 Event Loop 的实

      本文链接:https://www.haomeiwen.com/subject/fyecektx.html