事件循环机制 Event Loop
文章目录
一.JavaScript是单线程
Javascript语言是的一大特点是单线程,也就是说每次只能执行一项任务,其他任务都得按照顺序排队等待被执行,只有当前的任务执行完成之后才会执行下一个任务.
1.为什么JavaScript是单线程
为什么JavaScript是单线程的?根据阮一峰大神介绍,作为浏览器的脚本语言,JavaScript的主要用途是与用户互动,以及进行DOM操作.这决定了它只能是单线程.否则会带来很多复杂的同步问题.比如,JavaScript同时有2个线程,一个线程在DOM节点上添加内容,另一个线程删除了这个DOM节点,那么请问,这个时候浏览器应该以哪个线程为准呢?
2.让JavaScript拥有多线程
为了利用多核CPU的计算能力,HTML5提出了Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制.并且还不能操作DOM,所以,这个新标准,并没有改变JavaScript是单线程的本质.
二.任务队列
单线程就意味着,所有的任务都需要排队,前面一个任务结束,才会执行后一个任务.如果前一个任务耗时很长,后一个任务就不得不一直等着
任务分为2种:
1.同步任务(synchronous)
同步任务是指:在主线程上排队执行的任务,只有当前一个任务执行完毕,才能执行后一个任务
var num = 0;
console.log('任务一');
for (let index = 0; index < 100000000; index++) {
num += index;
}
console.log(num);
console.log('任务二');
以上代码是一个同步任务.当任务一执行之后,进入for循环去计算,但是这个for循环计算需要很长的时间,所以不得不等着,只有当for循环计算完成之后,才可以去执行任务二,这种形式的任务,就是同步任务.
2 异步任务(asynchronous)
异步任务是指:不进入主线程,而是进入
任务队列
,只有任务队列
通知主线程,某个异步任务可以执行了,那么该任务才会进入主线程执行.
console.log('任务一');
setTimeout(() => {
console.log('任务二');
}, 3000);
console.log('任务三');
//打印结果:任务一 > 任务三 > 任务二
以上代码是一个异步任务.不按顺序执行,同时执行多个任务.因为setTimeout
是一个异步任务,所以会先执行同步任务,然后再执行异步任务
所以呢,可以得到一个结论:同步任务和异步任务同时存在时,一定先执行完同步任务再执行异步任务
.
setTimeout(() => {
console.log('任务一');
}, 0);
console.log('任务二');
var num = 0;
for (let index = 0; index < 100000000; index++) {
num += index;
}
console.log(num);
console.log('任务三');
执行结果:
任务二
4999999950000000
任务三
任务一
从上面代码可以看出,setTimeout这个异步任务不管写在哪里,都会先执行同步任务,再执行异步任务
2.1 异步任务的执行机制
同步任务执行也可以这么认为,因为它可以被视为没有异步任务的异步执行
- 所有同步任务都在主线程上执行,形成一个执行栈
- 主线程之外,还存在一个"任务队列",只要异步任务有了运行结果,就在"任务队列"之中放置一个事件
- 一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件.哪些对应的异步任务,于是结束等待状态,进入执行栈,开始执行
- 主线程不断的重复上面这一步
3.Javascript中的异步任务
3.1 setTimeout和 setInterval
setTimeout(function() { console.log(‘b’); }, 10)
3.2 dom事件
console.log("1")
dom.onclick = function () { alert(123) }
console.log("2")
Javascript中的事件基本上都是异步的,上面代码,会先执行同步代码,打印结果1,2之后,等待触发点击事件后,才会执行,所以也是个异步任务
3.3 ajax
ajax请求,不用说了,都知道,它是异步请求数据
3.4 promise
var promise = new Promise(function(resolve, reject) {//这里是同步任务
console.log(3);
resolve();
})
promise.then(function() {//这里是异步任务
console.log(4);
})
4.调用栈 Call Stack(执行栈)
调用栈是一种后进先出的数据结构,当一个脚本执行的时候,js引擎会解析这段代码,并将其中同步代码按照执行顺序加入调用栈中,然后从头开始执行.
在谷歌浏览器中,我们F12调试的时候,可以看到右边的 Call Stack,也就是调用栈,所有的代码都会进出于这个调用栈.
后进先出的意思是:就像子弹壳装弹,一粒一粒的进去,但是打出来的时候,是从上面打出来的,最先压进去的最后弹出来,也就是说进去的顺序的123,打出来的顺序是321,这就是后进先出.
5.事件队列(Task Queue)
js引擎遇到一个异步任务之后,并不会一直等待其返回结果,而是会将这个任务交给浏览器的其他模块进行处理(以谷歌浏览器的webkit为例,是webcore模块) 继续执行调用栈中的其他任务.当一个异步任务返回结果后,js引擎会将这个任务加入与当前调用栈不同的另一个队列,我们称之为事件队列
也有叫"任务队列".
三.事件循环机制
我们来解读下这个图:
call stack :当一个脚本执行的时候,js引擎会解析这段代码,并且将其中的同步代码按照执行顺序加入调用栈中,然后从头开始执行.
webcore module: js引擎遇到一个异步事件后并不会一直等待其返回结果,而是将这个事件挂起(其他模块进行处理),继续执行调用栈中的其他任务.一个异步事件返回结果后,js会将这个事件加入到事件队列.
task queue:被放入事件队列不会立刻执行其回调.而是等待当前执行栈中的所有任务都执行完毕,主线程处于闲置状态时,然后主线程会去查找事件队列中是否有任务,如果有,那么主线程会从中取出排在第一位的事件,并把这个事件对应的回调放入执行栈中,然后执行其中的同步代码,如此反复,这样就形成了一个无线的循环,这个过程被称为**事件循环(Event Loop)**
- 整体的script(作为第一个宏任务),开始执行的时候,会把所有代码分为两部分:同步任务和异步任务
- 同步任务会直接进入主线程依次执行
- 异步任务会再分为宏任务和微任务
- 宏任务进入到Event Table中,并在里面注册回调函数,每当指定的事件完成时,Event Table会将这个函数移入大Event Queue中
- 微任务也会进入到另一个Event Table中,并在里面注册回调函数,每当指定的事件完成时,Event Table会将这个函数移到Event Queue中
- 当主线程内的任务执行完毕,主线程为空时,会检查微任务的Event Queue,如果有任务,就全部执行,如果没有就执行下一个宏任务.所以,
先执行微任务在执行宏任务
上述过程会不断重复,这就是Event Loop事件循环
四.微任务和宏任务
上面说的事件循环过程是一个宏观的表述,实际上因为异步任务之间并不相同,因此他们的执行优先级也有区别.
不同的异步任务被分为两类:
微任务(micro task):
promise.then、promise.nextTick(node),MutationObserver(html5 新特性)
宏任务(macro task)
整体代码script、setTimeout、setInterval......
需要注意的是:new Promise是会进入到主线程中立刻执行,而promise.then则属于微任务
先执行整体的宏任务,再执行异步任务中的微任务,然后执行宏任务
五.示例代码解读
1.示例一
console.log(1);
var timer = setTimeout(function () {//异步任务的宏任务
console.log(2);
}, 0)
console.log(timer);//延时器的id 值为1
var promise = new Promise(function (resolve, reject) {//同步任务
console.log(3);
resolve();
})
promise.then(function () { //异步任务的微任务
console.log(4);
})
console.log(5);
//1,1,3,5,4,2
2.示例二
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2() {
console.log('async2');
}
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0)
async1();
new Promise(function(resolve) {
console.log('promise1');
resolve();
}).then(function() {
console.log('promise2');
});
console.log('script end');
先执行宏任务(当前代码块也算是宏任务),然后执行当前宏任务产生的微任务,然后接着执行宏任务
- 从上往下执行代码,先执行同步代码,输出
script start
- 遇到setTimeout,现把 setTimeout 的代码放到宏任务队列中
- 执行 async1(),输出
async1 start
, 然后执行 async2(), 输出async2
,把 async2() 后面的代码console.log('async1 end')
放到微任务队列中 - 接着往下执行,输出
promise1
,把 .then()放到微任务队列中;注意Promise本身是同步的立即执行函数,.then是异步执行函数 - 接着往下执行, 输出
script end
。同步代码(同时也是宏任务)执行完成,接下来开始执行刚才放到微任务中的代码 - 依次执行微任务中的代码,依次输出
async1 end
、promise2
, 微任务中的代码执行完成后,开始执行宏任务中的代码,输出setTimeout
最后的执行结果如下
- script start
- async1 start
- async2
- promise1
- script end
- async1 end
- promise2
- setTimeout
3.示例三
console.log('start');
setTimeout(() => {
console.log('children2');
Promise.resolve().then(() => {
console.log('children3');
})
}, 0);
new Promise(function(resolve, reject) {
console.log('children4');
setTimeout(function() {
console.log('children5');
resolve('children6')
}, 0)
}).then((res) => {
console.log('children7');
setTimeout(() => {
console.log(res);
}, 0)
})
这道题跟上面题目不同之处在于,执行代码会产生很多个宏任务,每个宏任务中又会产生微任务
- 从上往下执行代码,先执行同步代码,输出
start
- 遇到setTimeout,先把 setTimeout 的代码放到宏任务队列①中
- 接着往下执行,输出
children4
, 遇到setTimeout,先把 setTimeout 的代码放到宏任务队列②中,此时.then并不会被放到微任务队列中,因为 resolve是放到 setTimeout中执行的 - 代码执行完成之后,会查找微任务队列中的事件,发现并没有,于是开始执行宏任务①,即第一个 setTimeout, 输出
children2
,此时,会把Promise.resolve().then
放到微任务队列中。 - 宏任务①中的代码执行完成后,会查找微任务队列,于是输出
children3
;然后开始执行宏任务②,即第二个 setTimeout,输出children5
,此时将.then放到微任务队列中。 - 宏任务②中的代码执行完成后,会查找微任务队列,于是输出
children7
,遇到 setTimeout,放到宏任务队列中。此时微任务执行完成,开始执行宏任务,输出children6
;
最后的执行结果如下
- start
- children4
- children2
- children3
- children5
- children7
- children6
4.示例四
const p = function() {
return new Promise((resolve, reject) => {
const p1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(1)
}, 0)
resolve(2)
})
p1.then((res) => {
console.log(res);
})
console.log(3);
resolve(4);
})
}
p().then((res) => {
console.log(res);
})
console.log('end');
- 执行代码,Promise本身是同步的立即执行函数,.then是异步执行函数。遇到setTimeout,先把其放入宏任务队列中,遇到
p1.then
会先放到微任务队列中,接着往下执行,输出3
- 遇到
p().then
会先放到微任务队列中,接着往下执行,输出end
- 同步代码块执行完成后,开始执行微任务队列中的任务,首先执行
p1.then
,输出2
, 接着执行p().then
, 输出4
- 微任务执行完成后,开始执行宏任务,setTimeout,
resolve(1)
,但是此时p1.then
已经执行完成,此时1
不会输出。
最后的执行结果如下
- 3
- end
- 2
- 4