JavaScript语言的一大特色就是单线程,也就是说,同一个时间只能作一件事。那么,为何JavaScript不能有多个线程呢?这样能提升效率啊。javascript
JavaScript的单线程,与它的用途有关。做为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操做DOM。这决定了它只能是单线程,不然会带来很复杂的同步问题。好比,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另外一个线程删除了这个节点,这时浏览器应该以哪一个线程为准?html
因此,为了不复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,未来也不会改变。java
为了利用多核CPU的计算能力,HTML5提出Web Worker标准,容许JavaScript脚本建立多个线程,可是子线程彻底受主线程控制,且不得操做DOM。因此,这个新标准并无改变JavaScript单线程的本质。面试
单线程就意味着,全部任务须要排队,前一个任务结束,才会执行后一个任务。若是前一个任务耗时很长,后一个任务就不得不一直等着。ajax
若是排队是由于计算量大,CPU忙不过来,倒也算了,可是不少时候CPU是闲着的,由于IO设备(输入输出设备)很慢(好比Ajax操做从网络读取数据),不得不等着结果出来,再往下执行。数据库
JavaScript语言的设计者意识到,这时主线程彻底能够无论IO设备,挂起处于等待中的任务,先运行排在后面的任务。等到IO设备返回告终果,再回过头,把挂起的任务继续执行下去。promise
因而,全部任务能够分红两种,一种是同步任务(synchronous),另外一种是异步任务(asynchronous)。同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务能够执行了,该任务才会进入主线程执行。浏览器
具体来讲,异步执行的运行机制以下。(同步执行也是如此,由于它能够被视为没有异步任务的异步执行。)bash
(1)全部同步任务都在主线程上执行,造成一个执行栈(execution context stack)。
(2)主线程以外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
(3)一但"执行栈"中的全部同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,因而结束等待状态,进入执行栈,开始执行。
(4)主线程不断重复上面的第三步。
复制代码
只要主线程空了,就会去读取"任务队列",这就是JavaScript的运行机制。这个过程会不断重复。网络
主线程从"任务队列"中读取事件,这个过程是循环不断的,因此整个的这种运行机制又称为Event Loop(事件循环)。为了更好地理解Event Loop,请看下图
函数入栈,当Stack中执行到异步任务的时候,就将他丢给WebAPIs,接着执行同步任务,直到Stack为空; 在此期间WebAPIs完成这个事件,把回调函数放入CallbackQueue中等待; 当执行栈为空时,Event Loop把Callback Queue中的一个任务放入Stack中,回到第1步。
接下来看一个异步函数执行的例子:
var start=new Date();
setTimeout(function cb(){
console.log("时间间隔:",new Date()-start+'ms');
},500);
while(new Date()-start<1000){};
复制代码
JS的异步有一个机制的,就是会分为宏任务和微任务。宏任务和微任务会放到不一样的event queue中,先将全部的宏任务放到一个event queue(macro-task),再将微任务放到一个event queue(micro-task)中。执行完宏任务以后,就会先从微任务中取这个回调函数执行。
讲的详细一点的话
最开始, 执行栈为空, 微任务队列为空, 宏任务队列有一个 script 标签(内含总体代码)
将第一个宏任务出队, 这里即为上述的 script 标签
总体代码执行过程当中, 若是是同步代码, 直接执行(函数执行的话会有入栈出栈操做), 若是是异步代码, 会根据任务类型推入不一样的任务队列中(宏任务或微任务)
当执行栈执行完为空时, 会去处理微任务队列的任务, 将微任务队列的任务一个个推入调用栈执行完
微任务执行完后,检查是否须要从新渲染 UI。
...往返循环直到宏任务和微任务队列为空
总结一下上述循环机制的特色:
出队一个宏任务 -> 调用栈为空后, 执行一队微任务 -> 更新界面渲染 -> 回到第一步
一个event loop有一个或者多个task队列。task任务源很是宽泛,好比ajax的onload,click事件,基本上咱们常常绑定的各类事件都是task任务源,还有数据库操做(IndexedDB ),须要注意的是setTimeout、setInterval、setImmediate也是task任务源。总结来讲task任务源:
microtask 队列和task 队列有些类似,都是先进先出的队列,由指定的任务源去提供任务,不一样的是一个 event loop里只有一个microtask 队列。另外microtask执行时机和Macrotasks也有所差别
下图是一个事件循环的流程
Promise.resolve().then(function promise1 () {
console.log('promise1');
})
setTimeout(function setTimeout1 (){
console.log('setTimeout1')
Promise.resolve().then(function promise2 () {
console.log('promise2');
})
}, 0)
setTimeout(function setTimeout2 (){
console.log('setTimeout2')
}, 0)
复制代码
运行过程:
script里的代码被列为一个task,放入task队列。
循环1:
【task队列:script ;microtask队列:】
【task队列:setTimeout1 setTimeout2;microtask队列:promise1】
循环2:
【task队列:setTimeout1 setTimeout2;microtask队列:】
【task队列:setTimeout2;microtask队列:promise2】
(循环2中的 setTimeout2为何不是跟在setTimeout1的后面输出?
这里我以为应该是setTimeout1和setTimeout2不是在同一个task队列中,
是两个task队列。在执行完setTimeout1的task队列后,
event loop去检查microtask队列是否有事件,而且把事推入到主栈。)
复制代码
【task队列:setTimeout2;microtask队列:】
【task队列:;microtask队列:】
注:有些文章说的一个事件循环的开始是先执行微任务再执行宏任务,有有些说的是先执行宏任务再执行微任务,我我的以为这两种只是见解的角度不一致
console.log('-------start--------');
setTimeout(() => {
console.log('setTimeout'); // 将回调代码放入另外一个宏任务队列
}, 0);
new Promise((resolve, reject) => {
for (let i = 0; i < 5; i++) {
console.log(i);
}
resolve()
}).then(()=>{
console.log('Promise'); // 将回调代码放入微任务队列
})
console.log('-------end--------');
复制代码
运行结果:
-------start--------
0
1
2
3
4
-------end--------
Promise
setTimeout
复制代码
由EXP1,咱们能够看出,当JS执行完主线程上的代码,会去检查在主线程上建立的微任务队列,执行完微任务队列以后才会执行宏任务队列上的代码
运行顺序:
主线程 => 主线程上建立的微任务 => 主线程上建立的宏任务
script里的代码被列为一个task,放入task队列。
循环1:
【task队列:script ;microtask队列:】
【task队列:setTimeout ;microtask队列:promise】
循环2:
【task队列:setTimeout ;microtask队列:】
【task队列:;microtask队列:】
setTimeout(_ => console.log('setTimeout4'))
new Promise(resolve => {
resolve()
console.log('Promise1')
}).then(_ => {
console.log('Promise3')
Promise.resolve().then(_ => {
console.log('before timeout')
}).then(_ => {
Promise.resolve().then(_ => {
console.log('also before timeout')
})
})
})
console.log(2)
复制代码
运行结果:
Promise1
2
Promise3
before timeout
also before timeout
setTimeout4
复制代码
由EXP2,咱们能够看出,在微任务队列执行时建立的微任务,仍是会排在主线程上建立出的宏任务以前执行(由于微任务只有一条,自增链不断的话 会一直往下执行微任务,不会被中断)
运行顺序:
主线程 => 主线程上建立的微任务1 => 微任务1上建立的微任务2 => 主线程上建立的宏任务
script里的代码被列为一个task,放入task队列。
循环1:
【task队列:script ;microtask队列:】
【task队列:setTimeout4;microtask队列:promise3】
【task队列:setTimeout4;microtask队列:before timeout】
【task队列:setTimeout4;microtask队列:also before timeout】
循环2:
【task队列:setTimeout4 ;microtask队列:before timeout】
【task队列:;microtask队列:】
// 宏任务队列 1
setTimeout(() => {
// 宏任务队列 1.1
console.log('timer_1');
setTimeout(() => {
// 宏任务队列 3
console.log('timer_3')
}, 0)
new Promise(resolve => {
resolve()
console.log('new promise')
}).then(() => {
// 微任务队列 1
console.log('promise then')
})
}, 0)
setTimeout(() => {
// 宏任务队列 2.2
console.log('timer_2')
}, 0)
console.log('========== Sync queue ==========')
复制代码
运行结果:
========== Sync queue ==========
timer_1
new promise
promise then
timer_2
timer_3
复制代码
运行顺序:
主线程(宏任务队列 1)=> 宏任务队列 1.1 => 微任务队列 1 => 宏任务队列 3=>宏任务队列2.2
循环1:
【task队列:script ;microtask队列:】
【task队列:timer_1,timer_2;microtask队列:】
循环2
【task队列::timer_1,timer_2;microtask队列:】
【task队列:timer_2,timer_3;microtask队列:promise then】
循环3
【task队列:timer_2,timer_3;microtask队列:】
【task队列:timer_3;microtask队列:】
循环4
【task队列:timer_3;microtask队列:】
【task队列:;microtask队列:】
// 宏任务1
new Promise((resolve) => {
console.log('new Promise(macro task 1)');
resolve();
}).then(() => {
// 微任务1
console.log('micro task 1');
setTimeout(() => {
// 宏任务3
console.log('macro task 3');
}, 0)
})
setTimeout(() => {
// 宏任务2
console.log('macro task 2');
}, 0)
console.log('========== Sync queue(macro task 1) ==========');
复制代码
运行结果:
========== Sync queue(macro task 1) ==========
new Promise(macro task 1)
micro task 1
macro task 2
macro task 3
复制代码
异步宏任务队列只有一个,当在微任务中建立一个宏任务以后,他会被追加到异步宏任务队列上(跟主线程建立的异步宏任务队列是同一个队列)
运行顺序:
主线程 => 主线程上建立的微任务 => 主线程上建立的宏任务 => 微任务中建立的宏任务
循环1:
【task队列:script ;microtask队列:】
【task队列:macro task 2;microtask队列:micro task 1】
循环2
循环2
【task队列:macro task 3;microtask队列:】
【task队列:;microtask队列:】
代码
<div class="outer">
<div class="inner"></div>
</div>
复制代码
var outer = document.querySelector('.outer');
var inner = document.querySelector('.inner');
function onClick() {
console.log('click');
setTimeout(function() {
console.log('timeout');
}, 0);
Promise.resolve().then(function() {
console.log('promise');
});
}
inner.addEventListener('click', onClick);
outer.addEventListener('click', onClick);
复制代码
点击 inner,最终打印结果为:
click
promise
click
promise
timeout
timeout
复制代码
分析
为何打印结果是这样的呢?咱们来分析一下: (0)将 script 标签内的代码(宏任务)放入执行栈执行,执行完后,宏任务微任务队列皆空。
(1)点击 inner,onClick 函数入执行栈执行,打印 "click"。执行完后执行栈为空,由于事件冒泡的缘故,事件触发线程会将向上派发事件的任务放入宏任务队列。
(2)遇到 setTimeout,在最小延迟时间后,将回调放入宏任务队列。遇到 promise,将 then 的任务放进微任务队列
(3)此时,执行栈再次为空。开始清空微任务,打印 "promise"
(4)此时,执行栈再次为空。从宏任务队列拿出一个任务执行,即前面提到的派发事件的任务,也就是冒泡。
(5)事件冒泡到 outer,执行回调,重复上述 "click"、"promise" 的打印过程。
(6)从宏任务队列取任务执行,这时咱们的宏任务队列已经累计了两个 setTimeout 的回调了,因此他们会在两个 Event Loop 周期里前后获得执行。
能够当作是:
function onClick() {
//模拟outer click事件
setTimeout(function(){onClick1()},0)
console.log('click');
setTimeout(function() {
console.log('timeout');
}, 0);
Promise.resolve().then(function() {
console.log('promise');
});
}
function onClick1() {
console.log('click1');
setTimeout(function() {
console.log('timeout1');
}, 0);
Promise.resolve().then(function() {
console.log('promise1');
});
}
//模拟inner click事件
onClick()
复制代码
代码
inner.click()
复制代码
打印结果为:
click
click
promise
promise
timeout
timeout
复制代码
分析
依旧分析一下:
(0)将 script(宏任务)放入执行栈执行,执行到 inner.click() 的时候,执行 onClick 函数,打印 "click"
(1)当执行完 onClick 后,此时的 script(宏任务)还没返回,执行栈不为空,不会去清空微任务,而是会将事件往上冒泡派发
...(关键步骤分析完后,续步骤就不分析了)
能够当作是:
function onClick() {
console.log('click');
setTimeout(function() {
console.log('timeout');
}, 0);
Promise.resolve().then(function() {
console.log('promise');
});
}
onClick();
onClick();
复制代码
总结
在通常状况下,微任务的优先级是更高的,是会优先于事件冒泡的,但若是手动 .click() 会使得在 script代码块 还没弹出执行栈的时候,触发事件派发。
浏览器进行事件循环工做方式
选择当前要执行的任务队列,选择任务队列中最早进入的任务,若是任务队列为空即null,则执行跳转到微任务(MicroTask)的执行步骤。
将事件循环中的任务设置为已选择任务。
执行任务。
将事件循环中当前运行任务设置为null。
将已经运行完成的任务从任务队列中删除。
microtasks步骤:进入microtask检查点。
更新界面渲染。
返回第一步
【执行进入microtask检查点时,浏览器会执行如下步骤:】
设置microtask检查点标志为true。
当事件循环microtask执行不为空时:选择一个最早进入的microtask队列的microtask,将事件循环的microtask设置为已选择的microtask,运行microtask,将已经执行完成的microtask为null,移出microtask中的microtask。
清理IndexDB事务
设置进入microtask检查点的标志为false。
重点
总结以上规则为一条通俗好理解的:
[总结]全部的异步都是为了按照必定的规则转换为同步方式执行。
以上是本人参考如下资料后的理解,若是有错误的地方,请各位大牛帮忙纠正,谢谢。
JavaScript 运行机制详解:再谈Event Loop