前言

众所周知,JavaScript 是一门单线程语言,虽然在 html5 中提出了 Web-Worker ,但这并未改变 JavaScript 是单线程这一核心。可看HTML规范中的这段话:

To coordinate events, user interaction, scripts, rendering, networking, and so forth, user agents must use event loops as described in this section. There are two kinds of event loops: those for browsing contexts, and those for workers.

为了协调事件、用户交互、脚本、UI 渲染和网络处理等行为,用户引擎必须使用 event loops。Event Loop 包含两类:一类是基于 Browsing Context ,一种是基于 Worker ,二者是独立运行的。 下面本文用一个例子,着重讲解下基于 Browsing Context 的事件循环机制。

来看下面这段 JavaScript 代码:

console.log('script start');

setTimeout(function() {
console.log('setTimeout');
}, 0); Promise.resolve().then(function() {
console.log('promise1');
}).then(function() {
console.log('promise2');
}); console.log('script end');

先猜测一下这段代码的输出顺序是什么,再去浏览器控制台输入一下,看看实际输出的顺序和你猜测出的顺序是否一致,如果一致,那就说明,你对 JavaScript 的事件循环机制还是有一定了解的,继续往下看可以巩固下你的知识;而如果实际输出的顺序和你的猜测不一致,那么本文下面的部分会为你答疑解惑。

任务队列

所有的任务可以分为同步任务和异步任务,同步任务,顾名思义,就是立即执行的任务,同步任务一般会直接进入到主线程中执行;而异步任务,就是异步执行的任务,比如ajax网络请求,setTimeout 定时函数等都属于异步任务,异步任务会通过任务队列( Event Queue )的机制来进行协调。具体的可以用下面的图来大致说明一下:

同步和异步任务分别进入不同的执行环境,同步的进入主线程,即主执行栈,异步的进入 Event Queue 。主线程内的任务执行完毕为空,会去 Event Queue 读取对应的任务,推入主线程执行。 上述过程的不断重复就是我们说的 Event Loop (事件循环)。

在事件循环中,每进行一次循环操作称为tick,通过阅读规范可知,每一次 tick 的任务处理模型是比较复杂的,其关键的步骤可以总结如下:

  1. 在此次 tick 中选择最先进入队列的任务( oldest task ),如果有则执行(一次)
  2. 检查是否存在 Microtasks ,如果存在则不停地执行,直至清空Microtask Queue
  3. 更新 render
  4. 主线程重复执行上述步骤

可以用一张图来说明下流程:

这里相信有人会想问,什么是 microtasks ?规范中规定,task分为两大类, 分别是 Macro Task (宏任务)和 Micro Task(微任务), 并且每个宏任务结束后, 都要清空所有的微任务,这里的 Macro Task也是我们常说的 task ,有些文章并没有对其做区分,后面文章中所提及的task皆看做宏任务( macro task)。

(macro)task 主要包含:script( 整体代码)、setTimeout、setInterval、I/O、UI 交互事件、setImmediate(Node.js 环境)

microtask主要包含:Promise、MutaionObserver、process.nextTick(Node.js 环境)

setTimeout/Promise 等API便是任务源,而进入任务队列的是由他们指定的具体执行任务。来自不同任务源的任务会进入到不同的任务队列。其中 setTimeout 与 setInterval 是同源的。

分析示例代码

千言万语,不如就着例子讲来的清楚。下面我们可以按照规范,一步步执行解析下上面的例子,先贴一下例子代码(免得你往上翻)。

console.log('script start');

setTimeout(function() {
console.log('setTimeout');
}, 0); Promise.resolve().then(function() {
console.log('promise1');
}).then(function() {
console.log('promise2');
}); console.log('script end');
  1. 整体 script 作为第一个宏任务进入主线程,遇到 console.log,输出 script start
  2. 遇到 setTimeout,其回调函数被分发到宏任务 Event Queue 中
  3. 遇到 Promise,其 then函数被分到到微任务 Event Queue 中,记为 then1,之后又遇到了 then 函数,将其分到微任务 Event Queue 中,记为 then2
  4. 遇到 console.log,输出 script end

至此,Event Queue 中存在三个任务,如下表:

宏任务 微任务
setTimeout then1
- then2
  1. 执行微任务,首先执行then1,输出 promise1, 然后执行 then2,输出 promise2,这样就清空了所有微任务
  2. 执行 setTimeout 任务,输出 setTimeout 至此,输出的顺序是:script start, script end, promise1, promise2, setTimeout

so,你猜对了吗?

看看你掌握了没

再来一个题目,来做个练习:


console.log('script start'); setTimeout(function() {
console.log('timeout1');
}, 10); new Promise(resolve => {
console.log('promise1');
resolve();
setTimeout(() => console.log('timeout2'), 10);
}).then(function() {
console.log('then1')
}) console.log('script end');

这个题目就稍微有点复杂了,我们再分析下:

首先,事件循环从宏任务 (macrotask) 队列开始,最初始,宏任务队列中,只有一个 scrip t(整体代码)任务;当遇到任务源 (task source) 时,则会先分发任务到对应的任务队列中去。所以,就和上面例子类似,首先遇到了console.log,输出 script start; 接着往下走,遇到 setTimeout 任务源,将其分发到任务队列中去,记为 timeout1; 接着遇到 promise,new promise 中的代码立即执行,输出 promise1, 然后执行 resolve ,遇到 setTimeout ,将其分发到任务队列中去,记为 timemout2, 将其 then 分发到微任务队列中去,记为 then1; 接着遇到 console.log 代码,直接输出 script end 接着检查微任务队列,发现有个 then1 微任务,执行,输出then1 再检查微任务队列,发现已经清空,则开始检查宏任务队列,执行 timeout1,输出 timeout1; 接着执行 timeout2,输出 timeout2 至此,所有的都队列都已清空,执行完毕。其输出的顺序依次是:script start, promise1, script end, then1, timeout1, timeout2

用流程图看更清晰:

总结

有个小 tip:从规范来看,microtask 优先于 task 执行,所以如果有需要优先执行的逻辑,放入microtask 队列会比 task 更早的被执行。

最后的最后,记住,JavaScript 是一门单线程语言,异步操作都是放到事件循环队列里面,等待主执行栈来执行的,并没有专门的异步执行线程。

参考文献

这一次,彻底弄懂 JavaScript 执行机制 Tasks, microtasks, queues and schedules 从一道题浅说 JavaScript 的事件循环

深入理解JavaScript事件循环机制的更多相关文章

  1. 一篇文章图文并茂地带你轻松学完 JavaScript 事件循环机制(event loop)

    JavaScript 事件循环机制 (event loop) 本篇文章已经默认你有了基础的 ES6 和 javascript语法 知识. 本篇文章比较细致,如果已经对同步异步,单线程等概念比较熟悉的读 ...

  2. 深入理解 JavaScript 事件循环(一)— event loop

    引言 相信所有学过 JavaScript 都知道它是一门单线程的语言,这也就意味着 JS 无法进行多线程编程,但是 JS 当中却有着无处不在的异步概念 .在初期许多人会把异步理解成类似多线程的编程模式 ...

  3. 深入浅出Javascript事件循环机制

    一.JS单线程.异步.同步概念 众所周知,JS是单线程(如果一个线程删DOM,一个线程增DOM,浏览器傻逼了-所以只能单着了),虽然有webworker酱紫的多线程出现,但也是在主线程的控制下.web ...

  4. 浏览器中的JavaScript事件循环机制

    浏览器的事件循环机制是HTML中定义的规范. JavaScript有一个主线程和调用栈,所有的任务都会被放到调用栈等待主线程执行. JS调用栈 是一种先进后出的数据结构.当函数被调用时,会被添加到栈中 ...

  5. javaScript 事件循环机制

    JavaScript是单线程的编程语言,只能同一时间内做一件事.但是在遇到异步事件的时候,js线程并没有阻塞,还会继续执行,这就是因为JS有事件循环机制. 事件循环流程总结 主线程开始执行一段代码, ...

  6. javascript事件循环机制 浅尝手记

    引入 众所周知Javascript是一个单线程的机制,虽然可以依托多线程的浏览器实现页面如何实现页面复杂的渲染.事件响应,但仍不会改变其单线程的本质:所以对于js的事件循环机制的了解是一个前端人员的必 ...

  7. JS JavaScript事件循环机制

    区分进程和线程 进程是cpu资源分配的最小单位(系统会给它分配内存) 不同的进程之间是可以同学的,如管道.FIFO(命名管道).消息队列 一个进程里有单个或多个线程 浏览器是多进程的,因为系统给它的进 ...

  8. 深入理解 JavaScript 事件循环(二)— task and microtask

    引言 microtask 这一名词是 JS 中比较新的概念,几乎所有人都是在学习 ES6 的 Promise 时才接触这一新概念,我也不例外.当我刚开始学习 Promise 的时候,对其中回调函数的执 ...

  9. [译] 深入理解 JavaScript 事件循环(二)— task and microtask

    引言 microtask 这一名词是 JS 中比较新的概念,几乎所有人都是在学习 ES6 的 Promise 时才接触这一新概念,我也不例外.当我刚开始学习 Promise 的时候,对其中回调函数的执 ...

随机推荐

  1. js之 单例模式

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

  2. Linux上的10个Touch命令实例

    Linux Touch 命令 Touch命令可以用来修改文件访问或修改的时间戳.实际上,它更常仅仅用于快速的创建一个空白文件. 这篇文章展示了一些非常简单和快速的实例,使用Touch命令来修改时间戳和 ...

  3. Eclipse个人快捷键设置及常用快捷键

    Eclipse中字母自动提示的快捷键设置,具体如下图所示,加上大写字母A-Z 二.常用快捷键设置 (1)在java代码中按 ctrl+O 快速查看类的属性和方法列表如下所示 (2)在java代码中选中 ...

  4. hadoop题目(一)

    一.简要描述如何安装配置一个开源hadoop,只描述即可,列出完整步骤. 答:①创建一个用户和用户组,用来管理hadoop项目:   ②修改确定IP地址:vim /etc/sysconfig/netw ...

  5. Lavarel Route::resource

    RESTful Resource controller A RESTful resource controller sets up some default routes for you and ev ...

  6. ubuntu16.04运行ros的时候编译工作空间catkin_make出现的一个问题Could not find a package configuration file provided by

    最近在进行ros里面的gazebo仿真之前需要对自己创建的工作空间进行编译,但是进行编译的时候输入catkin_make出现如下错误提示 查阅ROS问答社区之后发现两个比较有用的链接,如下 https ...

  7. 学习Spring

    一.Spring 概述 1.概述   Spring 框架是一个开源的 Java 平台,它最初是由 Rod Johnson 编写的,并且于 2003 年 6 月首次在 Apache 2.0 许可下发布. ...

  8. 北大poj- 1007

    DNA排序 逆序数可以用来描述一个序列混乱程度的量.例如,“DAABEC”的逆序数为5,其中D大于他右边的4个数,E大于他右边的1个数,4+1=5:又如,“ZWQM”的逆序数为3+2+1+0=6. 现 ...

  9. C# Ini、Json、Xml 封装类

    1.Ini是什么?(我对它的理解,用于存储用户配置信息的文件,该文件放在用户电脑...) INI文件是一个无固定标准格式的配置文件.它以简单的文字与简单的结构组成,常常使用在Windows操作系统,或 ...

  10. php 后知后觉

    1.$this :动态调用,指当前对象 通过一个例子,说明一下我当时的心理历程: /** * 父类 */ class A { function show() { echo $this->msg( ...