Fork me on GitHub

Js 执行机制/eventloop

原创文章,未经允许,请勿转载

基本概念

graph BT stack((stack
函数调用栈)) queue((queue
事件队列)) heap((heap
对象分配区))
function c() {
    a()
    b()
}

function m() {
    c()
}
m()

函数调用栈的过程(从栈底往栈顶看)

b  b入栈,执行,执行完后出栈,栈变短
a  a入栈,执行,往下执行,发现b
c  c执行,发现a
m  入栈,执行 发现调用c,于是c入栈

event loop 简化模型

js是单线程的,事件驱动的,这里的事件包含:I/O、网络、计时器、鼠标、键盘输入等等

while (queue.waitForEvent()) { //等待事件,如有用户点击按钮或者定时器到期
  queue.processNextEvent();//执行事件,这里可能会产出新事件,如:调用了 setTimeout,新事件会送入到队列中,等待被后面的循环执行
}

为什么单线程?我们假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加了一个节点,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准呢?基于此,其他所有操作系统所有框架,UI线程都只有一个

现在我们允许使用 Web Worker 创建一个子线程,但是worker不能直接操作UI,必须把数据提交给主线程,通过主线程操作UI

基于 event loop 的逻辑,我们考虑到 setTimeout(callback,0) 里的 callback 并不会立即执行,它首先依赖于队列的长度,最早也是下一班车发车时被执行

所以 setTimeout 并不会100%的精确度按照我们希望的时间去执行,而是取决于当前事件队列的长度,如果事件很多,那么就可能被延迟执行

下面的代码演示了这一点

Edit zero delay

const s = Date.now();

setTimeout(function() {
  //这里并不会在500毫秒后立即打印下面这行log
  console.log("经过了 " + (Date.now() - s) + " 毫秒");
}, 500);

//这里阻塞了2秒,因为是单线程,所以这2秒内,其他事件都没机会执行
while(true) {
  if(Date.now() - s >= 2000) {
    console.log("2秒过去了");
    break;
  }
}

深入理解js的执行机制

js的事件队列细分为两种,microtasksmacrotask ,两种队列在event loop中的处理方式不一样

考虑下面代码的执行结果是怎样的?

Edit js event loop

//立即执行
console.log(1)

//把callback放到 task queue
setTimeout(()=>{
    console.log(2)
},0)

function asyncFunc(){
    return new Promise((resolve)=>{
        resolve()
    })
}

//把callback放到 task queue
asyncFunc().then(()=>{
    console.log(3)
})

//立即执行
console.log(4)

//把callback放到 task queue
setTimeout(()=>{
    console.log(5)
},0)

结果是:

1    ----
4        |  一班车
3    ----
2           二班车
5           三班车

描述 microtasksmacrotask 的简化模型如下

while (eventLoop.waitForTask()) {
  //macroTask会在每一班车的最先执行,每班车只处理一个task
  const taskQueue = eventLoop.selectTaskQueue()
  if (taskQueue.hasNextTask()) {
    taskQueue.processNextTask()
  }
  //microTask会在当班车的末尾集中处理
  const microtaskQueue = eventLoop.microTaskQueue
  while (microtaskQueue.hasNextMicrotask()) {
    microtaskQueue.processNextMicrotask()
  }
}

以下API的回调会推送到 macroTask 宏任务队列:setTimeout setInterval setImmediate

以下API的回调会推送到 microTask 微任务队列:process.nextTick Promise MutationObserver

浏览器js执行是不会阻塞的,通过事件队列,让渲染、网络、IO等操作能在单线程的事件循环中被分开执行,充分发挥CPU的效率,保证界面交互的流畅度,但是alert和sync的XHR是阻塞的,调用alert之后,js阻塞,直到alert框关闭,才执行后面的语句

知识延伸

不只是浏览器的工作原理是基于事件驱动,实际上大部分操作系统的应用逻辑都类似

windows 的 WindowProc

while (GetMessage(&msg, NULL, 0, 0)) {
    TranslateMessage(&msg);
    DispatchMessage(&msg);
}

mac OS 的 [NSApp run]

void PollEvents() {
    NSAutoreleasePool* pool = [[NSAutoreleasePool alloc]init];

    for(;;) {
        NSEvent* event = [NSApp nextEventMatchingMask : NSEventMaskAny
            untilDate : [NSDatedistantPast] inMode : NSDefaultRunLoopMode
            dequeue:YES];
        if(event == nil)
        break;
        [NSApp sendEvent : event];
    }

    [pool release];
}

while(true) {
    PollEvents();
}

来源:悠游悠游,2019-05-31,原文地址:https://yymmss.com/p/event-loop.html