前端开发的童鞋应该都知道,JavaScript 是一门单线程的脚本语言。这就意味着 JavaScript 代码在执行的时候,只有一个主线程来执行全部的任务,同一个时间只能作同一件事情。html
那么为何 JavaScript 不设计成多线程的语言呢?前端
这是由其执行的环境是浏览器环境所决定的。试想一下若是 JavaScript 是多线程语言的话,那么当两个线程同时对 Dom 节点进行操做的时候,则可能会出现有歧义的问题,例如一个线程操做的是在一个 Dom 节点中添加内容,另外一个线程操做的是删除该 Dom 节点,那么应该以哪一个线程为准呢?因此 JavaScript 做为浏览器的脚本语言,其设计只能是单线程的。html5
须要注意的是,Html5 提出了 Web Worker,容许建立多个在后台运行的子线程来执行 JavaScript 脚本。可是因为子线程彻底受主线程控制,并且不可以干扰用户界面(即不能操做 Dom),因此这并无改变 JavaScript 单线程的本质。node
上面讲到,JavaScript 是一门单线程的脚本语言。所谓单线程,就是指全部的任务都须要排队一个个执行,只有前一个任务执行完了才能够执行后一个任务。这就形成了一个问题,若是前一个任务耗时过长,则会阻塞下一个任务的执行,在页面上用户的感知便会是浏览器卡死的现象。promise
而因为在大部分的状况中,形成任务耗时过长不是任务自己计算量大而致使 CPU 处理不过来,而是由于该任务须要与 IO 设备交互而致使的耗时过长,但这时 CPU 倒是处于闲置状态的。因此为了解决这个问题,便有了本章节的 JavaScript(也能够说是浏览器的)事件循环(Event Loop)机制。浏览器
在 JavaScript 事件循环机制中,使用到了三种数据对象,分别是栈(Stack)、堆(Heap)和队列(Queue)。bash
在 JavaScript 事件循环机制中,使用的栈数据结构即是执行上下文栈,每当有函数被调用时,便会建立相对应的执行上下文并将其入栈;使用到堆数据结构主要是为了表示一个大部分非结构化的内存区域存放对象;使用到的队列数据结构即是任务队列,主要用于存放异步任务。数据结构
在 JavaScript 代码运行过程当中,会进入到不一样的执行环境中,一开始执行时最早进入到全局环境,此时全局上下文首先被建立并入栈,以后当调用函数时则进入相应的函数环境,此时相应函数上下文被建立并入栈,当处于栈顶的执行上下文代码执行完毕后,则会将其出栈。这里的栈即是执行上下文栈。多线程
举个例子~异步
function fn2() {
console.log('fn2')
}
function fn1() {
console.log('fn1')
fn2();
}
fn1();
复制代码
上述代码中的执行上下文栈变化行为以下图
在 JavaScript 事件循环机制中,存在多种任务队列,其分为宏任务(macro-task)和微任务(micro-task)两种。
上述所描述的 setTimeout、Promise 等都是指一种任务源,其对应一种任务队列,真正放入任务队列中的,是任务源指定的异步任务。在代码执行过程当中,遇到上述任务源时,会将该任务源指定的异步任务放入不一样的任务队列中。
不一样的任务源对应的任务队列其执行顺序优先级是不一样的,上述宏任务和微任务的前后顺序表明了其任务队列执行顺序的优先级。
即在宏任务队列中,各个队列的优先级为 setTimeout > setInterval > I/O 在微任务队列中,各个队列的优先级为 Promise > Object.observe > MutationObserver
对于 UI rendering 来讲,浏览器会在每次清空微任务队列会根据实际状况触发,这里不作详细赘述。
简单来讲,事件循环机制的流程就是,主线程执行 JavaScript 总体代码后将遇到的各个任务源所指定的任务分发到各个任务队列中,而后微任务队列和宏任务队列交替入栈执行直到清空全部的任务队列,全局上下文出栈。
这里要注意的是,任务源所指定的异步任务,并非当即被放入任务队列中的,而是在接收到响应结果后才会将其放入任务队列中排队。如 setTimeout 中指定延迟事件为 1s,则在 1s 后才会将该任务源所指定的任务队列放入队列中;I/O 交互只有接收到响应结果后才将其异步任务放入队列中排队等待执行。
是否是感受挺抽象的,举个例子来实际感觉一下~
console.log('global');
setTimeout(function() {
console.log('setTimeout1');
new Promise(function(resolve) {
console.log('setTimeout1_promise');
resolve();
}).then(function() {
console.log('setTimeout1_promiseThen')
})
process.nextTick(function() {
console.log('setTimeout1_nextTick');
})
},0)
new Promise(function(resolve) {
console.log('promise1');
resolve();
}).then(function() {
console.log('promiseThen1')
})
setImmediate(function() {
console.log('setImmediate');
})
process.nextTick(function() {
console.log('nextTick');
})
new Promise(function(resolve) {
console.log('promise2');
resolve();
}).then(function() {
console.log('promiseThen2')
})
setTimeout(function() {
console.log('setTimeout2');
},0)
复制代码
在这个例子中,主要分析在事件循环流程中各个任务队列的变化状况,对于执行上下文栈的行为暂不作分析。任务队列图中左边表明队头,右边表明队尾。
为了可以实现该例子中有多个宏任务队列和多个微任务队列的状况,我加入了 node 中的 setImmediate 和 process.nextTick ,node 中的事件循环机制与 JavaScript 相似,只是其实现机制有所不一样,这里咱们不须要关心。加入 node 两个属性后,其优先级以下
在宏任务队列中,各个队列的优先级为 setTimeout > setInterval > setImmediate > I/O 微任务队列中,各个队列的优先级为 process.nextTick > Promise > Object.observe > MutationObserver
因此上述例子只可以在 node 环境中执行,不可以在浏览器中执行。那么让咱们来一步步分析上述代码的执行过程。
一,执行 Javascript 代码,全局上下文入栈,输出 global ,此时遇到第一个 setTimeout 任务源,因为其执行延迟时间为 0,因此可以当即接收到响应结果,将其指定的异步任务放入宏任务队列中;
二,遇到第一个 Promise 任务源,此时会执行 Promise 第一个参数中的代码,即输出 promise1,而后将其指定的异步任务(then 中函数)放入微任务队列中;
三,遇到 setImmediate 任务源,将其指定的异步任务放入宏任务队列中;
四,遇到 nextTick 任务源,将其指定的异步任务放入微任务队列中;
五,遇到第二个 Promise 任务源,输出 promise2,将其指定的异步任务放入微任务队列中;
六,遇到第二个 setTimeout 任务源,将其指定的异步任务放入宏任务队列中;
七,JavaScript 总体代码执行完毕,开始清空微任务队列,将微任务队列中的全部任务队列按优先级、单个任务队列的异步任务按先进先出的方式入栈并执行。此时咱们能够看到微任务队列中存在 Promise 和 nextTick 队列,nextTick 队列优先级比较高,取出 nextTick 异步任务入栈执行,输出 nextTick;
八,取出 Promise1 异步任务入栈执行,输出 promiseThen1;
九,取出 Promise2 异步任务入栈执行,输出 promiseThen2;
十,微任务队列清空完毕,执行宏任务队列,将宏任务队列中优先级最高的任务队列中的异步任务按先进先出的方式入栈并执行。此时咱们能够看到宏任务队列中存在 setTimeout 和 setImmediate 队列,setTimeout 队列优先级比较高,取出 setTimeout1 异步任务入栈执行,输出 setTimeout1,遇到 Promise 和 nextTick 任务源,输出 setTimeout1_promise,将其指定的异步任务放入微任务队列中;
十一,取出 setTimeout2 异步任务入栈执行,输出 setTimeout2;
十二,至此一个微任务宏任务事件循环完毕,开始下一轮循环。从微任务队列中的 nextTick 队列取出 setTimeout1_nextTick 异步任务入栈执行,输出 setTimeout1_nextTick;
十三,从微任务队列中的 Promise 队列取出 setTimeout1_promise 异步任务入栈执行,输出 setTimeout1_promiseThen;
十四,从宏任务队列中的 setImmediate 队列取出 setImmediate 异步任务入栈执行,输出 setImmediate;
十五,全局上下文出栈,代码执行完毕。最终输出结果为
global
promise1
promise2
nextTick
promiseThen1
promiseThen2
setTimeout1
setTimeout1_promise
setTimeout2
setTimeout1_nextTick
setTimeout1_promiseThen
setImmediate
复制代码
以为还不错的小伙伴,能够关注一波公众号哦。