众所周知,JavaScript 是单线程的,所谓单线程,就是指一次只能完成一个任务,若是有多个任务就必需要排队,前面的一个任务完成了,再执行后面的任务,以此类推。html
须要注意的是 JavaScript 只在一个线程上运行,不表明浏览器内核只有一个线程,事实上浏览器内部有多个线程,主线程用于 JavaScript 代码的编译和执行,其它线程都是在后台配合主线程。web
JavaScript 之因此选择单线程,跟历史有关系。JavaScript 从诞生起就是单线程,缘由是不想让浏览器变得太复杂,多线程须要面临锁、状态同步等问题,这对于一种网页脚本语言来讲开销太大。若是 JavaScript 同时有两个线程,一个线程在网页 DOM 节点上添加内容,另外一个线程删除了这个节点,这时浏览器应该以哪一个线程为准?因此,为了不复杂性,JavaScript 一开始就是单线程,这已经成了这门语言的核心特征。编程
上面说了 JavaScript 是单线程的,这种模式下,若是有一个很是耗时的任务进行的话,后面的任务都得排队等着,这时候应用程序就没法去作其余的事情,为此 JavaScript 语言的任务执行模式分为两个部分:同步(Synchronous)和异步(Asynchronous)api
那么 JavaScript 是如何来执行异步任务的呢,就是后面要讲的事件循环机制。promise
讲事件循环以前,咱们先来看一下 JavaScript 中的 call stack。下图是 JavaScript 引擎的一个简化图:浏览器
上图中看出 JavaScript 引擎主要包含两个部分:服务器
前面说了,JavaScript 是一种单线程编程语言,这意味着它只有一个 Call Stack
。所以,它一次仅能作一件事。Call Stack 是一个数据结构,它基本记录了咱们在程序执行中的所处的位置,若是咱们进入一个函数,咱们把它放在堆栈的顶部。若是咱们从一个函数中返回,咱们弹出堆栈的顶部。网络
上面图中能够看出,当开始执行 JS 代码时,首先向调用栈中压入一个 main()函数(表明了全局上下文),而后执行咱们的代码,根据先进后出的原则,后执行的代码会先弹出栈。数据结构
若是在调用堆栈中执行的函数调用须要花费大量时间才能进行处理,会发生什么? 例如,假设你想在浏览器中使用 JavaScript 进行一些复杂的图像转换。这时候浏览器就被阻塞了,这意味着浏览器没法渲染,它不能运行任何其余代码,它就是被卡住了。这时候就想到了咱们前面讲过的异步任务的处理方式,那么如何执行异步任务呢,就是下面要讲的事件循环(event loop)机制多线程
尽管容许执行异步 JavaScript 代码(如 setTimeout 函数),但直到 ES6 出现,实际上 JavaScript 自己历来没有任何明确的异步概念。 JavaScript 引擎历来都只是执行单个程序模块而不作更多别的事情。 那么,谁来告诉 JS 引擎去执行你编写的一大段程序?实际上,JS 引擎并非孤立运行,它运行在一个宿主环境中,对于大多数开发人员来讲,宿主环境就是一个典型的 Web 浏览器或 Node.js。全部环境中的共同点是一个称为事件循环的内置机制,它随着时间的推移处理程序中多个模块的执行顺序,并每次调用 JS 引擎。
因此,例如,当你的 JavaScript 程序发出一个 Ajax 请求来从服务器获取一些数据时,你在一个回调函数中写好了 “响应” 代码,JS 引擎将会告诉宿主环境:
“嘿,我如今暂停执行,可是每当你完成这个网络请求,而且你有一些数据,请调用这个函数并返回给我。
而后浏览器开始监听来自网络的响应,当响应返回给你的时候,宿主环境会将回调函数插入到事件循环中来安排回调函数的执行顺序。
咱们来看下面的图表:
咱们都使用过 setTimeout、AJAX 这些 API, 可是,这些 API 不是由 JS 引擎提供的。那这些 Web APIs 究竟是什么? 从本质上讲,它们是浏览器并行启动的一部分,是你没法访问的线程,你仅仅只能够调用它们。
前面说了浏览器内核是多线程,在内核控制下各线程相互配合以保持同步,一个浏览器一般由如下常驻线程组成:
上图中看出,JavaScript 运行时,除了正在运行的主线程,还存在一个 callback queue(也叫task queue),即任务队列,里面是各类须要当前程序处理的异步任务(实际上,根据异步任务的类型,存在多个任务队列)。
异步执行的运行机制以下:
下面咱们经过一个例子来看一下具体的执行过程。
console.log('Hi');
setTimeout(function cb1() {
console.log('cb1');
}, 5000);
console.log('Bye');
复制代码
setTimeout 有个要注意的地方,如上述例子延迟 5s 执行,不是严格意义上的 5s,正确来讲是至少 5s 之后会执行。由于 Web API 会设定一个 5s 的定时器,时间到期后将回调函数加到队列中,此时该回调函数还不必定会立刻运行,由于队列中可能还有以前加入的其余回调函数,并且还必须等到 Call Stack 空了以后才会从队列中取一个回调执行。这也是不少人说 JavaScript 中的定时器其实不是彻底精确的缘由。
关于事件循环的详细讲解,推荐一个视频《what the hack is event loop》
每一个线程都有本身的事件循环,因此每一个 web worker 有本身的事件循环(event loop),因此它能独立地运行。一个事件循环有多个 task 来源,而且保证在 task 来源内的执行顺序,在每次循环中浏览器要选择从哪一个来源中选取 task,任务源能够分为 微任务(microtask) 和 宏任务(macrotask),在ES6规范中,microtask 称为 jobs, macrotask 称为 task。
macrotask 主要包括下面几个:
microtask 主要包含:
参考 whatwg规范中关于任务队列的定义咱们能够了解到:
有点绕,咱们下面先看一个例子来解释一下:
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');
// script start
// script end
// promise1
// promise2
// setTimeout
复制代码
咱们来分析一下上面代码的具体执行步骤。表格中红色的表示当前正在执行的任务。
macrotasks | microtasks | call stack | Log |
---|---|---|---|
script |
script | script start |
macrotasks | microtasks | call stack | Log |
---|---|---|---|
script |
script | script start | |
setTimeout callback |
macrotasks | microtasks | call stack | Log |
---|---|---|---|
script |
Promise then 1 | script | script start |
setTimeout callback |
macrotasks | microtasks | call stack | Log |
---|---|---|---|
script |
Promise then 1 |
Promise callback 1 |
script start |
setTimeout callback | script end |
macrotasks | microtasks | call stack | Log |
---|---|---|---|
script |
Promise then 1 |
Promise callback 1 |
script start |
setTimeout callback | script end | ||
promise1 |
macrotasks | microtasks | call stack | Log |
---|---|---|---|
script |
Promise then 2 |
Promise callback 2 |
script start |
setTimeout callback | script end | ||
promise1 | |||
promise2 |
macrotasks | microtasks | call stack | Log |
---|---|---|---|
setTimeout callback |
setTimeout callback |
script start | |
script end | |||
promise1 | |||
promise2 | |||
setTimeout |