JavaScript中的Event Loop(事件循环)机制

文章首次发表在 我的博客

前言

最近面试了不少家公司,这道题几乎是必被问到的一道题。以前总以为本身了解得差很少,可是当第一次被问到的时候,殊不知道该从哪里开始提及,涉及到的知识点不少。因而花时间整理了一下。并不只仅是由于面试遇到了,而是理解JavaScript事件循环机制会让咱们日常遇到的疑惑也获得解答。html

通常面试官会这么问,出道题,让你说出打印结果。而后会问分别说说浏览器的node的事件循环,区别是什么,什么是宏任务和微任务,为何要有这两种任务...html5

本篇文章参考了不少文章,同时加上本身的理解,若是有问题但愿你们指出。node

事件循环

  1. JavaScript是单线程,非阻塞的
  2. 浏览器的事件循环git

    • 执行栈和事件队列
    • 宏任务和微任务
  3. node环境下的事件循环github

    • 和浏览器环境有何不一样
    • 事件循环模型
    • 宏任务和微任务
  4. 经典题目分析

1. JavaScript是单线程,非阻塞的

单线程:web

JavaScript的主要用途是与用户互动,以及操做DOM。若是它是多线程的会有不少复杂的问题要处理,好比有两个线程同时操做DOM,一个线程删除了当前的DOM节点,一个线程是要操做当前的DOM阶段,最后以哪一个线程的操做为准?为了不这种,因此JS是单线程的。即便H5提出了web worker标准,它有不少限制,受主线程控制,是主线程的子线程。面试

非阻塞:经过 event loop 实现。vim

2. 浏览器的事件循环

执行栈和事件队列

为了更好地理解Event Loop,请看下图(转引自Philip Roberts的演讲 《Help, I'm stuck in an event-loop》
Help, I'm stuck in an event-looppromise

执行栈: 同步代码的执行,按照顺序添加到执行栈中浏览器

function a() {
    b();
    console.log('a');
}
function b() {
    console.log('b')
}
a();

咱们能够经过使用 Loupe(Loupe是一种可视化工具,能够帮助您了解JavaScript的调用堆栈/事件循环/回调队列如何相互影响)工具来了解上面代码的执行状况。

调用状况

  1. 执行函数 a()先入栈
  2. a()中先执行函数 b() 函数b() 入栈
  3. 执行函数b(), console.log('b') 入栈
  4. 输出 bconsole.log('b')出栈
  5. 函数b() 执行完成,出栈
  6. console.log('a') 入栈,执行,输出 a, 出栈
  7. 函数a 执行完成,出栈。

事件队列: 异步代码的执行,遇到异步事件不会等待它返回结果,而是将这个事件挂起,继续执行执行栈中的其余任务。当异步事件返回结果,将它放到事件队列中,被放入事件队列不会马上执行起回调,而是等待当前执行栈中全部任务都执行完毕,主线程空闲状态,主线程会去查找事件队列中是否有任务,若是有,则取出排在第一位的事件,并把这个事件对应的回调放到执行栈中,而后执行其中的同步代码。

咱们再上面代码的基础上添加异步事件,

function a() {
    b();
    console.log('a');
}
function b() {
    console.log('b')
    setTimeout(function() {
        console.log('c');
    }, 2000)
}
a();

此时的执行过程以下
img

咱们同时再加上点击事件看一下运行的过程

$.on('button', 'click', function onClick() {
    setTimeout(function timer() {
        console.log('You clicked the button!');    
    }, 2000);
});

console.log("Hi!");

setTimeout(function timeout() {
    console.log("Click the button!");
}, 5000);

console.log("Welcome to loupe.");

img

简单用下面的图进行一下总结

执行栈和事件队列

宏任务和微任务

为何要引入微任务,只有一种类型的任务不行么?

页面渲染事件,各类IO的完成事件等随时被添加到任务队列中,一直会保持先进先出的原则执行,咱们不能准确地控制这些事件被添加到任务队列中的位置。可是这个时候忽然有高优先级的任务须要尽快执行,那么一种类型的任务就不合适了,因此引入了微任务队列。

不一样的异步任务被分为:宏任务和微任务
宏任务:

  • script(总体代码)
  • setTimeout()
  • setInterval()
  • postMessage
  • I/O
  • UI交互事件

微任务:

  • new Promise().then(回调)
  • MutationObserver(html5 新特性)

运行机制

异步任务的返回结果会被放到一个任务队列中,根据异步事件的类型,这个事件实际上会被放到对应的宏任务和微任务队列中去。

在当前执行栈为空时,主线程会查看微任务队列是否有事件存在

  • 存在,依次执行队列中的事件对应的回调,直到微任务队列为空,而后去宏任务队列中取出最前面的事件,把当前的回调加到当前指向栈。
  • 若是不存在,那么再去宏任务队列中取出一个事件并把对应的回到加入当前执行栈;

当前执行栈执行完毕后时会马上处理全部微任务队列中的事件,而后再去宏任务队列中取出一个事件。同一次事件循环中,微任务永远在宏任务以前执行。

在事件循环中,每进行一次循环操做称为 tick,每一次 tick 的任务处理模型是比较复杂的,但关键步骤以下:

  • 执行一个宏任务(栈中没有就从事件队列中获取)
  • 执行过程当中若是遇到微任务,就将它添加到微任务的任务队列中
  • 宏任务执行完毕后,当即执行当前微任务队列中的全部微任务(依次执行)
  • 当前宏任务执行完毕,开始检查渲染,而后GUI线程接管渲染
  • 渲染完毕后,JS线程继续接管,开始下一个宏任务(从事件队列中获取)

简单总结一下执行的顺序:
执行宏任务,而后执行该宏任务产生的微任务,若微任务在执行过程当中产生了新的微任务,则继续执行微任务,微任务执行完毕后,再回到宏任务中进行下一轮循环。

宏任务和微任务

深刻理解js事件循环机制(浏览器篇) 这边文章中有个特别形象的动画,你们能够看着理解一下。

console.log('start')

setTimeout(function() {
  console.log('setTimeout')
}, 0)

Promise.resolve().then(function() {
  console.log('promise1')
}).then(function() {
  console.log('promise2')
})

console.log('end')

浏览器事件循环

  1. 全局代码压入执行栈执行,输出 start
  2. setTimeout压入 macrotask队列,promise.then 回调放入 microtask队列,最后执行 console.log('end'),输出 end
  3. 调用栈中的代码执行完成(全局代码属于宏任务),接下来开始执行微任务队列中的代码,执行promise回调,输出 promise1, promise回调函数默认返回 undefined, promise状态变成 fulfilled ,触发接下来的 then回调,继续压入 microtask队列,此时产生了新的微任务,会接着把当前的微任务队列执行完,此时执行第二个 promise.then回调,输出 promise2
  4. 此时,microtask队列 已清空,接下来会会执行 UI渲染工做(若是有的话),而后开始下一轮 event loop, 执行 setTimeout的回调,输出 setTimeout

最后的执行结果以下

  • start
  • end
  • promise1
  • promise2
  • setTimeout

node环境下的事件循环

和浏览器环境有何不一样

表现出的状态与浏览器大体相同。不一样的是 node 中有一套本身的模型。node 中事件循环的实现依赖 libuv 引擎。Node的事件循环存在几个阶段。

若是是node10及其以前版本,microtask会在事件循环的各个阶段之间执行,也就是一个阶段执行完毕,就会去执行 microtask队列中的任务。

node版本更新到11以后,Event Loop运行原理发生了变化,一旦执行一个阶段里的一个宏任务(setTimeout,setInterval和setImmediate)就马上执行微任务队列,跟浏览器趋于一致。下面例子中的代码是按照最新的去进行分析的。

事件循环模型

┌───────────────────────┐
┌─>│        timers         │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │     I/O callbacks     │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │     idle, prepare     │
│  └──────────┬────────────┘      ┌───────────────┐
│  ┌──────────┴────────────┐      │   incoming:   │
│  │         poll          │<──connections───     │
│  └──────────┬────────────┘      │   data, etc.  │
│  ┌──────────┴────────────┐      └───────────────┘
│  │        check          │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
└──┤    close callbacks    │
   └───────────────────────┘

事件循环各阶段详解

node中事件循环的顺序

外部输入数据 --> 轮询阶段(poll) --> 检查阶段(check) --> 关闭事件回调阶段(close callback) --> 定时器检查阶段(timer) --> I/O 事件回调阶段(I/O callbacks) --> 闲置阶段(idle, prepare) --> 轮询阶段...

这些阶段大体的功能以下:

  • 定时器检测阶段(timers): 这个阶段执行定时器队列中的回调如 setTimeout() 和 setInterval()。
  • I/O事件回调阶段(I/O callbacks): 这个阶段执行几乎全部的回调。可是不包括close事件,定时器和setImmediate()的回调。
  • 闲置阶段(idle, prepare): 这个阶段仅在内部使用,能够没必要理会
  • 轮询阶段(poll): 等待新的I/O事件,node在一些特殊状况下会阻塞在这里。
  • 检查阶段(check): setImmediate()的回调会在这个阶段执行。
  • 关闭事件回调阶段(close callbacks): 例如socket.on('close', ...)这种close事件的回调

poll:
这个阶段是轮询时间,用于等待还未返回的 I/O 事件,好比服务器的回应、用户移动鼠标等等。
这个阶段的时间会比较长。若是没有其余异步任务要处理(好比到期的定时器),会一直停留在这个阶段,等待 I/O 请求返回结果。
check:
该阶段执行setImmediate()的回调函数。

close:
该阶段执行关闭请求的回调函数,好比socket.on('close', ...)。

timer阶段:
这个是定时器阶段,处理setTimeout()和setInterval()的回调函数。进入这个阶段后,主线程会检查一下当前时间,是否知足定时器的条件。若是知足就执行回调函数,不然就离开这个阶段。

I/O callback阶段:
除了如下的回调函数,其余都在这个阶段执行:

  • setTimeout()和setInterval()的回调函数
  • setImmediate()的回调函数
  • 用于关闭请求的回调函数,好比socket.on('close', ...)

宏任务和微任务

宏任务:

  • setImmediate
  • setTimeout
  • setInterval
  • script(总体代码)
  • I/O 操做等。

微任务:

  • process.nextTick
  • new Promise().then(回调)

Promise.nextTick, setTimeout, setImmediate的使用场景和区别

Promise.nextTick
process.nextTick 是一个独立于 eventLoop 的任务队列。
在每个 eventLoop 阶段完成后会去检查 nextTick 队列,若是里面有任务,会让这部分任务优先于微任务执行。
是全部异步任务中最快执行的。

setTimeout:
setTimeout()方法是定义一个回调,而且但愿这个回调在咱们所指定的时间间隔后第一时间去执行。

setImmediate:
setImmediate()方法从意义上将是马上执行的意思,可是实际上它倒是在一个固定的阶段才会执行回调,即poll阶段以后。

经典题目分析

一. 下面代码输出什么

async function async1() {
    console.log('async1 start');
    await async2();
    console.log('async1 end');
}
async function async2() {
    console.log('async2');
}
console.log('script start');
setTimeout(function() {
    console.log('setTimeout');
}, 0)
async1();
new Promise(function(resolve) {
    console.log('promise1');
    resolve();
}).then(function() {
    console.log('promise2');
});
console.log('script end');

先执行宏任务(当前代码块也算是宏任务),而后执行当前宏任务产生的微任务,而后接着执行宏任务

  1. 从上往下执行代码,先执行同步代码,输出 script start
  2. 遇到setTimeout,现把 setTimeout 的代码放到宏任务队列中
  3. 执行 async1(),输出 async1 start, 而后执行 async2(), 输出 async2,把 async2() 后面的代码 console.log('async1 end')放到微任务队列中
  4. 接着往下执行,输出 promise1,把 .then()放到微任务队列中;注意Promise自己是同步的当即执行函数,.then是异步执行函数
  5. 接着往下执行, 输出 script end。同步代码(同时也是宏任务)执行完成,接下来开始执行刚才放到微任务中的代码
  6. 依次执行微任务中的代码,依次输出 async1 endpromise2, 微任务中的代码执行完成后,开始执行宏任务中的代码,输出 setTimeout

最后的执行结果以下

  • script start
  • async1 start
  • async2
  • promise1
  • script end
  • async1 end
  • promise2
  • setTimeout

二. 下面代码输出什么

console.log('start');
setTimeout(() => {
    console.log('children2');
    Promise.resolve().then(() => {
        console.log('children3');
    })
}, 0);

new Promise(function(resolve, reject) {
    console.log('children4');
    setTimeout(function() {
        console.log('children5');
        resolve('children6')
    }, 0)
}).then((res) => {
    console.log('children7');
    setTimeout(() => {
        console.log(res);
    }, 0)
})

这道题跟上面题目不一样之处在于,执行代码会产生不少个宏任务,每一个宏任务中又会产生微任务

  1. 从上往下执行代码,先执行同步代码,输出 start
  2. 遇到setTimeout,先把 setTimeout 的代码放到宏任务队列①中
  3. 接着往下执行,输出 children4, 遇到setTimeout,先把 setTimeout 的代码放到宏任务队列②中,此时.then并不会被放到微任务队列中,由于 resolve是放到 setTimeout中执行的
  4. 代码执行完成以后,会查找微任务队列中的事件,发现并无,因而开始执行宏任务①,即第一个 setTimeout, 输出 children2,此时,会把 Promise.resolve().then放到微任务队列中。
  5. 宏任务①中的代码执行完成后,会查找微任务队列,因而输出 children3;而后开始执行宏任务②,即第二个 setTimeout,输出 children5,此时将.then放到微任务队列中。
  6. 宏任务②中的代码执行完成后,会查找微任务队列,因而输出 children7,遇到 setTimeout,放到宏任务队列中。此时微任务执行完成,开始执行宏任务,输出 children6;

最后的执行结果以下

  • start
  • children4
  • children2
  • children3
  • children5
  • children7
  • children6

三. 下面代码输出什么

const p = function() {
    return new Promise((resolve, reject) => {
        const p1 = new Promise((resolve, reject) => {
            setTimeout(() => {
                resolve(1)
            }, 0)
            resolve(2)
        })
        p1.then((res) => {
            console.log(res);
        })
        console.log(3);
        resolve(4);
    })
}


p().then((res) => {
    console.log(res);
})
console.log('end');
  1. 执行代码,Promise自己是同步的当即执行函数,.then是异步执行函数。遇到setTimeout,先把其放入宏任务队列中,遇到p1.then会先放到微任务队列中,接着往下执行,输出 3
  2. 遇到 p().then 会先放到微任务队列中,接着往下执行,输出 end
  3. 同步代码块执行完成后,开始执行微任务队列中的任务,首先执行 p1.then,输出 2, 接着执行p().then, 输出 4
  4. 微任务执行完成后,开始执行宏任务,setTimeout, resolve(1),可是此时 p1.then已经执行完成,此时 1不会输出。

最后的执行结果以下

  • 3
  • end
  • 2
  • 4

你能够将上述代码中的 resolve(2)注释掉, 此时 1才会输出,输出结果为 3 end 4 1

const p = function() {
    return new Promise((resolve, reject) => {
        const p1 = new Promise((resolve, reject) => {
            setTimeout(() => {
                resolve(1)
            }, 0)
        })
        p1.then((res) => {
            console.log(res);
        })
        console.log(3);
        resolve(4);
    })
}


p().then((res) => {
    console.log(res);
})
console.log('end');
  • 3
  • end
  • 4
  • 1

最后强烈推荐几个很是好的讲解 event loop 的视频:

参考

相关文章
相关标签/搜索