JS的事件轮询(Event Loop)机制

前言

JS单线程、JS的事件循环(Event Loop)、执行栈、任务队列(消息队列)、主线程、宏队列(macrotask)、微队列(microtask),前端er相信不少人对这些词并不陌生,即使对js的api熟能生巧,可是却并不理解这些机制流程的话,那可能JS的提高很难了,这里也是属于提高JS的一个分水岭,在介绍这些概念以前,咱们先思考几个很是经典的面试题,答案最后公布,看完这篇文章,或许就可以焕然大悟:透过现象看本质! 注:本章全部环境都是基于浏览器环境,暂不考虑node环境; 题目一:javascript

setTimeout(() => {
    console.log(1);
}, 0);

new Promise((resolve) => {
    console.log(2);
    resolve();
}).then(() => {
    console.log(3);
});

console.log(4);
// 输出最后的结果
复制代码

题目二:前端

setTimeout(() => {
    console.log(1);
}, 0);

new Promise((resolve) => {
    console.log(2);
    setTimeout(() => {
        console.log(5);
    }, 0);
    resolve();
}).then(() => {
    console.log(3);
});

console.log(4);
// 输出最后的结果
复制代码

题目三:java

setTimeout(() => {
    console.log(1);
}, 0);
new Promise((resolve,reject) =>{
    console.log(2)
    resolve(3)
}).then((val) =>{
    console.log(val);
})
console.log(4);
// 输出最后的结果
复制代码

题目四:node

let a = () => {
  setTimeout(() => {
    console.log('任务队列函数1')
  }, 0)
  for (let i = 0; i < 5000; i++) {
    console.log('a的for循环')
  }
  console.log('a事件执行完')
}

let b = () => {
  setTimeout(() => {
    console.log('任务队列函数2')
  }, 0)
  for (let i = 0; i < 5000; i++) {
    console.log('b的for循环')
  }
  console.log('b事件执行完')
}

let c = () => {
  setTimeout(() => {
    console.log('任务队列函数3')
  }, 0)
  for (let i = 0; i < 5000; i++) {
    console.log('c的for循环')
  }
  console.log('c事件执行完')
}

a();
b();
c();
// 输出最后的结果
复制代码

JS单线程

JavaScript为何是单线程,难道不能实现为多线程吗?

进程与任务

通常状况下,一个进程一次只能执行一个任务,若是有不少任务须要执行,不外乎三种解决方法:面试

(1)排队:由于一个进程一次只能执行一个任务,只好等前面的任务执行完了,再执行后面的任务。 (2)新建进程:使用fork命令,为每一个任务新建一个进程。 (3)新建线程:由于进程太耗费资源,因此现在的程序每每容许一个进程包含多个线程,由线程去完成任务。 它是一种单线程语言,全部任务都在一个线程上完成,即采用上面的第一种方法。一旦遇到大量任务或者遇到一个耗时的任务,网页就会出现"假死",由于JavaScript停不下来,也就没法响应用户的行为。ajax

单线程

JavaScript从诞生起就是单线程,这跟历史有关系。缘由大概是不想让浏览器变得太复杂,由于多线程须要共享资源、且有可能修改彼此的运行结果,对于一种网页脚本语言来讲,这就太复杂了。后来就约定俗成,JavaScript为一种单线程语言。(Worker API能够实现多线程,可是JavaScript自己始终是单线程的。)api

若是某个任务很耗时,好比涉及不少I/O(输入/输出)操做,那么线程的运行大概是下面的样子。 promise

JS单线程
上图的绿色部分是程序的运行时间,红色部分是等待时间。能够看到,因为I/O操做很慢,因此这个线程的大部分运行时间都在空等I/O操做的返回结果。这种运行方式称为"同步模式"(synchronous I/O)或"堵塞模式"(blocking I/O)。

若是采用多线程,同时运行多个任务,那极可能就是下面这样。 浏览器

JS单线程

上图代表,多线程不只占用多倍的系统资源,也闲置多倍的资源,这显然不合理。多线程

其实JavaScript单线程是指浏览器在解释和执行javascript代码时只有一个线程,即JS引擎线程,浏览器自身还会提供其余线程来支持这些异步方法,浏览器的渲染线程大概有一下几种:

JS引擎线程 事件触发线程 定时触发器线程 异步http请求线程 GUI渲染线程 ...

浏览器环境

js做为主要运行在浏览器的脚本语言,js主要用途之一是操做DOM。 在js高程中举过一个栗子,若是js同时有两个线程,同时对同一个dom进行操做,这时浏览器应该听哪一个线程的,如何判断优先级? 为了不这种问题,js必须是一门单线程语言,而且在将来这个特色也不会改变。

解决的问题

Event Loop就是为了解决这个问题而提出的。

"Event Loop是一个程序结构,用于等待和发送消息和事件。(a programming construct that waits for and dispatches events or messages in a program.)"

简单说,就是在程序中设置两个线程:一个负责程序自己的运行,称为"主线程";另外一个负责主线程与其余进程(主要是各类I/O操做)的通讯,被称为"Event Loop线程"(能够译为"消息线程")。

JS单线程
上图主线程的绿色部分,仍是表示运行时间,而橙色部分表示空闲时间。每当遇到I/O的时候,主线程就让Event Loop线程去通知相应的I/O程序,而后接着日后运行,因此不存在红色的等待时间。等到I/O程序完成操做,Event Loop线程再把结果返回主线程。主线程就调用事先设定的回调函数,完成整个任务。

能够看到,因为多出了橙色的空闲时间,因此主线程得以运行更多的任务,这就提升了效率。这种运行方式称为"异步模式"(asynchronous I/O)或"非堵塞模式"(non-blocking mode)。

这正是JavaScript语言的运行方式。单线程模型虽然对JavaScript构成了很大的限制,但也所以使它具有了其余语言不具有的优点。若是部署得好,JavaScript程序是不会出现堵塞的,这就是为何node.js平台能够用不多的资源,应付大流量访问的缘由。

执行栈与任务队列

由于js是单线程语言,当遇到异步任务(如ajax操做等)时,不可能一直等待异步完成,再继续往下执行,在这期间浏览器是空闲状态,显而易见这会致使巨大的资源浪费。

执行栈

当执行某个函数、用户点击一次鼠标,Ajax完成,一个图片加载完成等事件发生时,只要指定过回调函数,这些事件发生时就会进入任务队列中,等待主线程读取,遵循先进先出原则。

执行任务队列中的某个任务,这个被执行的任务就称为执行栈。

主线程

要明确的一点是,主线程跟执行栈是不一样概念,主线程规定如今执行执行栈中的哪一个事件。 **主线程循环:**即主线程会不停的从执行栈中读取事件,会执行完全部栈中的同步代码。 当遇到一个异步事件后,并不会一直等待异步事件返回结果,而是会将这个事件挂在与执行栈不一样的队列中,咱们称之为任务队列(Task Queue)。 当主线程将执行栈中全部的代码执行完以后,主线程将会去查看任务队列是否有任务。若是有,那么主线程会依次执行那些任务队列中的回调函数。

js异步执行的运行机制

1)全部任务都在主线程上执行,造成一个执行栈。 2)主线程以外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。 3)一旦"执行栈"中的全部同步任务执行完毕,系统就会读取"任务队列"。那些对应的异步任务,结束等待状态,进入执行栈并开始执行。

主线程不断重复上面的第三步。

浏览器事件机制

浏览器在执行js代码过程当中会维护一个执行栈,每一个方法都会进栈执行以后而后出栈(FIFO)。与此同时,浏览器又维护了一个消息队列,全部的异步方法,在执行结束后都会将回调方法塞入消息队列中,当全部执行栈中的任务所有执行完毕后,浏览器开始往消息队列寻找任务,先进入消息队列的任务先执行。

浏览器事件机制

宏任务和微任务

那么若是两个不一样种类的异步任务执行后,哪一个会先执行?就像开头提到的面试题,setTimeout和promise哪一个会先执行?这时候要提到概念:宏任务和微任务。 概念以下:

**宏任务(Macrotasks):**js同步执行的代码块,setTimeout、setInterval、XMLHttprequest、setImmediate、I/O、UI rendering等。 **微任务(Microtasks):**promise、process.nextTick(node环境)、Object.observe, MutationObserver等。

执行栈中执行的任务都是宏任务,当宏任务遇到Promise的时候会建立微任务,当Promise状态fullfill的时候塞入微任务队列。在一次宏任务完成后,会检查微任务队列有没有须要执行的任务,有的话按顺序执行微任务队列中全部的任务。以后再开始执行下一次宏任务。具体步骤:

(1)执行主代码块 (2)若遇到Promise,把then以后的内容放进微任务队列 (3)一次宏任务执行完成,检查微任务队列有无任务 (4)有的话执行全部微任务 (5)执行完毕后,开始下一次宏任务。

如何区分宏任务和微任务呢?划分的标准是什么?

宏任务本质:参与了事件循环的任务。

回到 Chromium 中,须要处理的消息主要分红了三类:

Chromium 自定义消息 Socket 或者文件等 IO 消息 UI 相关的消息

  1. 与平台无关的消息,例如 setTimeout 的定时器就是属于这个
  2. Chromium 的 IO 操做是基于 libevent 实现,它自己也是一个事件驱动的库
  3. UI 相关的其实属于 blink 渲染引擎过来的消息,例如各类 DOM 的事件 其实与 JavaScript 的引擎无关,都是在 Chromium 实现的。

微任务本质:直接在 Javascript 引擎中的执行的,没有参与事件循环的任务。

(1)是个内存回收的清理任务,使用过 Java 的童鞋应该都很熟悉,只是在 JavaScript 这是V8内部调用的 (2)就是普通的回调,MutationObserver 也是这一类 (3)Callable (4)包括 Fullfiled 和 Rejected 也就是 Promise 的完成和失败 (5)Thenable 对象的处理任务

宏任务,微任务的优先级

promise是在当前脚本代码执行完后,马上执行的,它并无参与事件循环,因此它的优先级是高于 setTimeout。 宏任务和微任务的总结: 宏任务 Macrotasks 就是参与了事件循环的异步任务。 微任务 Microtasks 就是没有参与事件循环的“异步”任务。

执行顺序

一、先执行主线程 二、遇到宏队列(macrotask)放到宏队列(macrotask) 三、遇到微队列(microtask)放到微队列(microtask) 四、主线程执行完毕 五、执行微队列(microtask),微队列(microtask)执行完毕 六、执行一次宏队列(macrotask)中的一个任务,执行完毕 七、执行微队列(microtask),执行完毕 八、依次循环。。。

Event Loop(事件循环)

  js是单线程的,执行较长的js时候,页面会卡死,没法响应,可是全部的操做都会被记住到另外的队列。好比:点击了一个元素,不会马上的执行,可是等到js加载完毕后就会执行刚才点击的操做,可以知道有一个队列记录了全部有待执行的操做,这个队列分为微观和宏观。微观会比宏观执行得更快。

  event loop它最主要是分三部分:主线程、宏队列(macrotask)、微队列(microtask) js的任务队列分为同步任务和异步任务,全部的同步任务都是在主线程里执行的,异步任务可能会在macrotask或者microtask里面。

  事件循环就是多线程的一种工做方式,Chrome里面是使用了共享的task_runner对象给本身和其它线程post task过来存起来,用一个死循环不断地取出task执行,或者进入休眠等待被唤醒。Mac的Chrome渲染线程和浏览器线程还借助了Mac的sdk Cococa的NSRunLoop来作为UI事件的消息源。Chrome的多进程通讯(不一样进程的IO线程的本地socket通讯)借助了libevent的事件循环,并加入了到了主消息循环里面。

称为事件循环的缘由大多来源于源码:

while (queue.waitForMessage()) {
  queue.processNextMessage();
}
复制代码

宏任务 > 全部微任务 > 宏任务,以下图所示:

事件循环

事件循环中,每一次循环称为 tick, 每一次tick的任务以下:

执行栈选择最早进入队列的宏任务(一般是script总体代码),若是有则执行 检查是否存在 Microtask,若是存在则不停的执行,直至清空 microtask 队列 更新render(每一次事件循环,浏览器均可能会去更新渲染) 重复以上步骤

Event Loop总体流程

事件循环

题目解析

题目一:

答案:2 4 3 1
复制代码

(1)setTimeout丢给浏览器的异步线程处理,由于时间是0,立刻放入消息队列 (2)new Promise里面的console.log(2)加入执行栈,并执行,而后退出 (3)直接resolve,then后面的内容加入微任务队列 (4)console.log(4)加入执行栈,执行完成后退出 (5)检查微任务队列,发现有任务,执行console.log(3) (6)发现消息队列有任务,执行下一次宏任务console.log(1)

题目二:

答案:2 4 3 1 5
复制代码

(1)setTimeout丢给浏览器的异步线程处理,由于时间是0,立刻放入消息队列 (2)new Promise里面的console.log(2)加入执行栈,并执行 (3)setTimeout给浏览器的异步线程处理,由于时间是0,立刻放入消息队列,而后退出 (4)直接resolve,then后面的内容加入微任务队列 (5)console.log(4)加入执行栈,执行完成后退出 (6)检查微任务队列,发现有任务,执行console.log(3) (7)发现消息队列有任务,执行下一次宏任务console.log(1) (8)发现消息队列有任务,执行下一次宏任务console.log(5)

题目三:

答案:2 4 3 1
复制代码

(1)先执行script同步代码: 先执行new Promise中的console.log(2),then后面的不执行属于微任务而后执行console.log(4) (2)执行完script宏任务后,执行微任务,console.log(3),没有其余微任务了 (3)执行另外一个宏任务,定时器,console.log(1)

题目四:

答案:
(5000)a的for循环
a事件执行完
(5000)b的for循环
b事件执行完
(5000)c的for循环
c事件执行完
任务队列函数1
任务队列函数2
任务队列函数3
复制代码

结果是当a、b、c函数都执行完成以后,三个setTimeout才会依次执行

node环境中的事件机制

node环境中的事件机制要比浏览器复杂不少,node的事件轮询有阶段的概念。每一个阶段切换的时候执行,process.nextTick之类的全部微任务。

node环境中的事件机制

timer阶段

执行全部的时间已经到达的计时事件

peding callbacks阶段

这个阶段将执行全部上一次poll阶段没有执行的I/O操做callback,通常是报错。

idle.prepare

能够忽略

poll阶段

这个阶段特别复杂

阻塞等到全部I/O操做,执行全部的callback. 全部I/O回调执行完,检查是否有到时的timer,有的话回到timer阶段 没有timer的话,进入check阶段.

check阶段

执行setImmediate

close callbacks阶段

执行全部close回调事件,例如socket断开。

相关文章
相关标签/搜索