JS执行机制(eventloop)
js是单线程
js
作为主要运行在浏览器的脚本语言,js
主要用途之一是操作DOM
。
假设如果js
同时有两个线程,同时对同一个DOM
进行操作,这时浏览器应该听哪个线程的,如何判断优先级?
为了避免这种问题,js
必须是一门单线程语言
js执行机制
执行栈
当执行某个函数、用户点击一次鼠标,Ajax完成,一个图片加载完成等事件发生时,只要指定过回调函数,这些事件发生时就会进入任务队列中,然后执行栈再去任务队列读取、执行。等待主线程读取,遵循先进先出原则。
执行任务队列中的某个任务,这个被执行的任务就称为执行栈。
主线程
主线程跟执行栈是不同概念,主线程规定现在执行执行栈中的哪个事件。
主线程循环:即主线程会不停的从执行栈中读取事件,会执行完所有栈中的同步代码。
当遇到一个异步事件后,并不会一直等待异步事件返回结果,而是会将这个事件挂在与执行栈不同的队列中,我们称之为任务队列(Task Queue)。
js 异步执行的运行机制。
console.log('开始执行1');
setTimeout(() => {
console.log('2秒后执行的内容');
}, 2000);
setTimeout(() => {
console.log('0秒后执行的内容');
}, 0);
console.log('结束执行4');//1 4 0 2
先将所有的同步代码压入执行栈依次执行,期间将异步代码放入任务队列。同步代码执行完毕后,在回调函数队列中根据定时器的时间来依次执行相应的代码,延时少的先执行,延时多的后执行,否则默认从队头压入执行栈
- 所有任务都在主线程上执行,形成一个执行栈。
- 主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
- 一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列"。那些对应的异步任务,结束等待状态,进入执行栈并开始执行。
- 主线程不断重复上面的第三步。
异步任务的宏与微
异步
加载图片音乐之类占用资源大耗时久的任务,就是异步任务。
JS 的异步是通过回调函数实现的。
一般而言,异步任务有以下三种类型:
- 普通事件,如
click
、resize
等 - 资源加载,如
load
、error
等 - 定时器,包括
setInterval
、setTimeout
等
异步任务相关回调函数添加到任务队列中(任务队列也称为消息队列)。
不同的API注册的任务会依次进入自身对应的队列中,然后等待 Event Loop
将它们依次压入执行栈中执行。
Event Loop(事件循环)
浏览器的事件循环由一个宏任务队列+多个微任务队列组成。
首先,执行第一个宏任务:全局Script脚本。产生的的宏任务和微任务进入各自的队列中。执行完Script后,把当前的微任务队列清空。完成一次事件循环。
接着再取出一个宏任务,同样把在此期间产生的回调入队。再把当前的微任务队列清空。以此往复。
宏任务队列只有一个,而每一个宏任务都有一个自己的微任务队列,每轮循环都是由一个宏任务+多个微任务组成。
例题如下:
setTimeout(function () {
console.log(1)
})
new Promise(function (resolve, reject) {
console.log(2)
resolve(3)
}).then(function (val) {
console.log(val) // 微任务1
})
new Promise(function (resolve, reject) {
console.log(4)
resolve(5)
}).then(function (val) {
console.log(val) // 微任务2
})
console.log(6)
第一步:宏任务A
(直接整体js代码就是第一个宏任务)
1、script
中的同步代码依次执行
2、第一个new Promise
中的console.log(2)
,得到2
3、第二个new Promise
中的console.log(4)
,得到4
4、外面的script
同步代码console.log(6)
,得到6
此次宏任务执行完毕;
第二步:所有可执行的微任务
1、then
后面的属于Promise
微任务,全部依次执行,得到3, 5
第三步:宏任务B
1、执行新的宏任务,得到1
最后,可以很轻松猜出或者说是写出正确答案:2,4,6,3,5,1
例题2:
console.log('1');
setTimeout(function() {
console.log('2');
process.nextTick(function() {
console.log('3');
})
new Promise(function(resolve) {
console.log('4');
resolve();
}).then(function() {
console.log('5')
})
})
process.nextTick(function() {
console.log('6');
})
new Promise(function(resolve) {
console.log('7');
resolve();
}).then(function() {
console.log('8')
})
setTimeout(function() {
console.log('9');
process.nextTick(function() {
console.log('10');
})
new Promise(function(resolve) {
console.log('11');
resolve();
}).then(function() {
console.log('12')
})
})
完整的输出为1,7,6,8,2,4,3,5,9,11,10,12
eventloop再学习总结:
- JS引擎线程:负责JS的解析以及所有同步异步任务的执行。维护一个
执行栈
,先逐个处理同步代码,当遇到异步任务,就会借助事件触发线程。 - 事件触发线程:事件触发线程维护一个
任务队列
,其中又分为微任务队列和宏任务队列。任务队列里的异步任务被触发时,该任务就会被放到对应任务队列的队尾,等待js引擎线程的执行栈清空,再从任务队列中取任务进行处理。 - 计时器线程:
setTimeout
和setInterval
所在的线程
宏任务、微任务和事件循环机制
事件循环机制:
- JS引擎逐行扫描js代码,遇到同步任务加入执行栈。
- 遇到异步任务如
setTimeout
,发送ajax请求
等的异步任务,就交给事件触发线程。当该异步任务被触发,就放到任务队列的末尾。该线程维护一个微任务队列
和一个宏任务队列
。 - JS引擎将执行栈里的同步任务执行完之后,就去从宏任务队列队首取一个宏任务,和对应的微任务到执行栈里,直到微任务队列为空。注意宏任务是一次事件循环取一个,而微任务是一直取一直取直到任务队列为空
- 再取一个宏任务,重复
3
的过程。
microtast(微任务):Promise.then, process.nextTick
macrotask(宏任务):script整体代码、setTimeout、 setInterval等
特别注意
当微任务嵌套微任务时,内层微任务先执行,外层微任务后执行
Promise和async中的立即执行
我们知道Promise中的异步体现在
then
和catch
中,所以写在Promise中的代码是被当做同步任务立即执行的。而在async/await中,在出现await出现之前,其中的代码也是立即执行的。那么出现了await时候发生了什么呢?await做了什么
从字面意思上看await就是等待,await 等待的是一个表达式,这个表达式的返回值可以是一个promise对象也可以是其他值。
很多人以为await会一直等待之后的表达式执行完之后才会继续执行后面的代码,实际上await是一个让出线程的标志。await后面的表达式会先执行一遍,将await后面的代码加入到microtask中,然后就会跳出整个async函数来执行后面的代码。
由于因为async await 本身就是promise+generator的语法糖。所以await后面的代码是microtask。
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
等价于
async function async1() {
console.log('async1 start');
Promise.resolve(async2()).then(() => {
console.log('async1 end');
})
}
变式一
在第一个变式中我将async2中的函数也变成了Promise函数,代码如下:
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2() {
//async2做出如下更改:
new Promise(function(resolve) {
console.log('promise1');
resolve();
}).then(function() {
console.log('promise2');
});
}
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0)
async1();
new Promise(function(resolve) {
console.log('promise3');
resolve();
}).then(function() {
console.log('promise4');
});
console.log('script end');
script start
async1 start
promise1
promise3
script end
promise2
async1 end
promise4
setTimeout
变式二
在第二个变式中,我将async1中await后面的代码和async2的代码都改为异步的,代码如下:
async function async1() {
console.log('async1 start');
await async2();
//更改如下:
setTimeout(function() {
console.log('setTimeout1')
},0)
}
async function async2() {
//更改如下:
setTimeout(function() {
console.log('setTimeout2')
},0)
}
console.log('script start');
setTimeout(function() {
console.log('setTimeout3');
}, 0)
async1();
new Promise(function(resolve) {
console.log('promise1');
resolve();
}).then(function() {
console.log('promise2');
});
console.log('script end');
script start
async1 start
promise1
script end
promise2
setTimeout3
setTimeout2
setTimeout1
变式三
变式三是我在一篇面经中看到的原题,整体来说大同小异,代码如下:
async function a1 () {
console.log('a1 start')
await a2()
console.log('a1 end')
}
async function a2 () {
console.log('a2')
}
console.log('script start')
setTimeout(() => {
console.log('setTimeout')
}, 0)
Promise.resolve().then(() => {
console.log('promise1')
})
a1()
let promise2 = new Promise((resolve) => {
resolve('promise2.then')
console.log('promise2')
})
promise2.then((res) => {
console.log(res)
Promise.resolve().then(() => {
console.log('promise3')
})
})
console.log('script end')
无非是在微任务那块儿做点文章,前面的内容如果你都看懂了的话这道题一定没问题的,结果如下:
script start
a1 start
a2
promise2
script end
promise1
a1 end
promise2.then
promise3
setTimeout
async function test() {
return new Promise((resolve)=>{
setTimeout(() => {
console.log('2秒后test');
resolve('test 1000');
}, 2000);
})
}
function fn() {
console.log('fn');
return 'fn';
}
async function next() {
let res0 = await fn(),
res1 = await test(),
res2 = await fn(),
res3 = await test();
console.log('被阻塞')
}
console.log('1')
next(); //
setTimeout(() => {
console.log('settime');
}, 2000);
setTimeout(() => {
console.log('settime2');
}, 2000);
console.log('1')
输出:
1
fn
1//整体代码同步任务
settime//2秒后几乎同时输出set,set2,2秒后test,fn,先进入宏任务队列 在队头
settime2//在队尾
2秒后test
fn
2秒后test//4秒后输出
被阻塞
建议精读这一次,彻底弄懂 JavaScript 执行机制便可
参考:
setTimeout+Promise+Async输出顺序?很简单呀!