JS分为 同步任务 和 异步任务
同步任务都在主线程(这里的主线程就是JS引擎线程
)上执行,会形成一个执行栈
主线程之外,事件触发线程
管理着一个任务队列,只要异步任务有了运行结果,就在任务队列之中放一个事件回调
一旦执行栈中的所有同步任务执行完毕(也就是JS引擎线程
空闲了),系统就会读取任务队列,将可运行的异步任务(任务队列中的事件回调,只要任务队列中有事件回调,就说明可以执行)添加到执行栈中,开始执行
有关线程问题,回顾:JS(单线程)与浏览器(多进程)
事件循环图解(初始):
代码示例:
let setTimeoutCallBack = function() {
console.log('我是定时器回调');
};
let httpCallback = function() {
console.log('我是http请求回调');
}
// 同步任务
console.log('我是同步任务1');
// 异步定时任务
setTimeout(setTimeoutCallBack,1000);
// 异步http请求任务
ajax.get('/info',httpCallback);
// 同步任务
console.log('我是同步任务2');
上述代码执行过程:
JS是按照顺序从上往下依次执行的,可以先理解为这段代码时的执行环境就是主线程,也就是当前执行栈
定时器线程
,通知定时器线程 1s 后将setTimeoutCallBack 这个回调交给事件触发线程
处理,在 1s 后事件触发线程会收到 setTimeoutCallBack 这个回调并把它加入到事件触发线程所管理的事件队列中等待执行异步http请求线程
发送网络请求,请求成功后将 httpCallback 这个回调交由事件触发线程
处理,事件触发线程收到 httpCallback 这个回调后把它加入到事件触发线程所管理的事件队列中等待执行JS引擎线程
已经空闲,开始向事件触发线程
发起询问,询问事件触发线程的事件队列中是否有需要执行的回调函数,如果有将事件队列中的回调事件加入执行栈中,开始执行回调,如果事件队列中没有回调,JS引擎线程
会一直发起询问,直到有为止由此可以看出,浏览器上的所有线程的工作都很单一且独立,非常符合单一原则
但是这样的事件循环是不够详细的,请接着向下看。
它是一种数据结构,存放要执行的任务。然后事件循环系统再以先进先出原则按顺序执行队列中的任务。产生新任务时IO线程就将任务添加在队列尾部,要执行任务渲染主线程就会循环地从队列头部取出执行
可任务队列里的任务类型太多了,而且是多个线程操作同一个任务队列,比如鼠标滚动、点击、移动、输入、计时器、WebSocket、文件读写、解析DOM、计算样式、计算布局、JS执行…
所以为了处理高优先级的任务,和解决单任务执行过长的问题,所以需要将任务划分
浏览器页面是由 任务队列 和 事件循环 系统来驱动的,但是队列要一个一个执行,如果某个任务(http请求)是个耗时任务,那浏览器总不能一直卡着,所以为了防止主线程阻塞,JavaScript 又分为同步任务和异步任务
1、宏任务(macrotask)
在ECMAScript中,macrotask也被称为task
每次 执行栈 执行的代码可以当做是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行), 每一个宏任务会从头到尾执行完毕,不会执行其他
由于JS引擎线程
和GUI渲染线程
是互斥的关系,浏览器为了能够使 宏任务 和 DOM任务 有序的进行,会在一个宏任务执行结果后,在下一个宏任务执行前,GUI渲染线程开始工作,对页面进行渲染。
宏任务 -> GUI渲染 -> 宏任务 -> ...
常见的宏任务:
2、微任务(microtask)
ES6新引入了Promise标准,同时浏览器实现上多了一个microtask微任务概念,在ECMAScript中,microtask也被称为jobs
知道宏任务结束后,会执行渲染,然后执行下一个宏任务, 而微任务可以理解成 在当前宏任务执行后立即执行的任务
当一个宏任务执行完,会在渲染前,将执行期间所产生的所有微任务都执行完。
宏任务 -> 微任务 -> GUI渲染 -> 宏任务 -> ...
常见微任务:
代码示例:
document.body.style = 'background:blue'
console.log(1);
Promise.resolve().then(()=>{
console.log(2);
document.body.style = 'background:pink'
});
console.log(3);
最终结果为:控制台输出 1 3 2,浏览器页面背景直接变为粉色
3、微任务宏任务注意点:
1)浏览器会先执行一个宏任务,紧接着执行当前执行栈产生的微任务,再进行渲染,然后再执行下一个宏任务
2)微任务和宏任务不在一个任务队列,不在一个任务队列
例如:setTimeout是一个宏任务,它的事件回调在宏任务队列,Promise.then()是一个微任务,它的事件回调在微任务队列,二者并不是一个任务队列
3)消息队列中的任务都是宏任务:以Chrome 为例,有关渲染的都是在渲染进程中执行,渲染进程中的任务(DOM树构建,JS解析…等)需要主线程执行的任务都会在主线程中执行,而浏览器维护了一套事件循环机制,主线程上的任务都会放到消息队列中执行,主线程会循环消息队列,并从头部取出任务进行执行,如果执行过程中产生其他任务需要主线程执行的,渲染进程中的其他线程会把该任务塞入到消息队列的尾部,消息队列中的任务都是宏任务
4)微任务的产生:当执行到script脚本的时候,JS引擎会为全局创建一个执行上下文,在该执行上下文中维护了一个微任务队列,当遇到微任务,就会把微任务回调放在微任务队列中,当所有的JS代码执行完毕,在退出全局上下文之前引擎会去检查该队列,有回调就执行,没有就退出执行上下文,这也就是为什么 微任务要早于宏任务,也是大家常说的,每个宏任务都有一个微任务队列(由于定时器是浏览器的API,所以定时器是宏任务,在JS中遇到定时器会也是放入到浏览器的队列中)
举例子:
你和一个大爷在银行办业务,大爷排在你前面,大爷是要存钱,存完钱之后,工作人员问大爷还要不要办理其他业务,大爷说那我再改个密码吧,这时候总不能让大爷到队伍最后去排队再来改密码吧
这里面大爷要办业务就是一个宏任务,而在钱存完了又想改密码,这就产生了一个微任务,大爷还想办其他业务就又产生新微任务,直到所有微任务执行完,队伍的下一个人再来
这个队伍就是任务队列,工作人员就是单线程的JS引擎,排队的人只能一个一个来让他给你办事
也就是说当前宏任务里的微任务全部执行完,才会执行下一个宏任务
一句话概括就是入栈到出栈的循环。即:一个宏任务,所有微任务,渲染,一个宏任务,所有微任务,渲染…
2、Promise函数
new Promise(() => {}).then() ,来看这样一个Promise代码
前面的 new Promise() 这一部分是一个构造函数,这是一个同步任务
后面的 .then() 才是一个异步微任务,这一点是非常重要的
示例1:
new Promise((resolve) => {
console.log(1)
resolve()
}).then(()=>{
console.log(2)
})
console.log(3)
打印结果:1 3 2
3、async/await 函数
async/await 是 ES7 引入的重大改进的地方,可以在不阻塞主线程的情况下,使用同步代码实现异步访问资源的能力,让我们的代码逻辑更清晰
async/await本质上还是基于Promise的一些封装,而Promise是属于微任务的一种
所以在使用await关键字与Promise.then效果类似。可以理解为,await 以前的代码,相当于与 new Promise 的同步代码,await 以后的代码相当于 Promise.then的异步
示例1:
setTimeout(() => console.log(4))
async function test() {
console.log(1)
await Promise.resolve()
console.log(3)
}
test()
console.log(2)
执行结果:1 2 3 4
示例2:
async function fun() {
console.log(1)
let a = await 2
console.log(a)
console.log(3)
}
console.log(4)
fun()
console.log(5)
执行结果:4 1 5 2 3
用ES6的写法:(同上)
function fun(){
return new Promise(() => {
console.log(1)
Promise.resolve(2).then( a => {
console.log(a)
console.log(3)
})
})
}
console.log(4)
fun()
console.log(5)
回调是微任务,所以直接扔到微任务队列等着,自然就是最后执行
示例3:
function bar () {
console.log(2)
}
async function fun() {
console.log(1)
await bar()
console.log(3)
}
console.log(4)
fun()
console.log(5)
执行结果:4 1 2 5 3
因为await的意思就是等,等await后面的执行完。所以"await bar()",是从右向左执行,执行完bar(),然后遇到await,返回一个微任务(哪怕这任务里没东西),放到微任务队列让出主线程。
4、常见面试题:
代码示例1:
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——async1 start——async2——promise1——script end——async1 end——promise2——setTimeout
代码示例2:
function test() {
console.log(1)
setTimeout(function () { // timer1
console.log(2)
}, 1000)
}
test();
setTimeout(function () { // timer2
console.log(3)
})
new Promise(function (resolve) {
console.log(4)
setTimeout(function () { // timer3
console.log(5)
}, 100)
resolve()
}).then(function () {
setTimeout(function () { // timer4
console.log(6)
}, 0)
console.log(7)
})
console.log(8)
执行结果:1,4,8,7,3,6,5,2
执行顺序:JS是顺序从上而下执行
代码示例3:
console.log('1');
setTimeout(function() { //setTimeout1
console.log('2');
process.nextTick(function() { //process2
console.log('3');
})
new Promise(function(resolve) {
console.log('4');
resolve();
}).then(function() { //then2
console.log('5')
})
})
process.nextTick(function() { //process1
console.log('6');
})
new Promise(function(resolve) {
console.log('7');
resolve();
}).then(function() { //then1
console.log('8')
})
setTimeout(function() { //setTimeout2
console.log('9');
process.nextTick(function() {
console.log('10');
})
new Promise(function(resolve) { //process3
console.log('11');
resolve();
}).then(function() { //then3
console.log('12')
})
})
执行结果:1,7,6,8,2,4,3,5,9,11,10,12
第一轮事件循环流程分析如下:
整体script作为第一个宏任务进入主线程,遇到console.log,输出1
遇到setTimeout,其回调函数被分发到宏任务Event Queue中。暂且记为setTimeout1
遇到process.nextTick(),其回调函数被分发到微任务Event Queue中。记为process1
遇到Promise,new Promise直接执行,输出7。then被分发到微任务Event Queue中。记为then1
又遇到了setTimeout,其回调函数被分发到宏任务Event Queue中,记为setTimeout2
宏任务 | 微任务 |
---|---|
setTimeout1 | process1 |
setTimeout2 | then1 |
可知,process1和then1两个微任务
执行process1,输出6
执行then1,输出8
第二轮时间循环从setTimeout1宏任务开始:
宏任务 | 微任务 |
---|---|
setTimeout2 | process2 |
then2 |
第三轮事件循环开始从setTimeout2宏任务开始:
宏任务 | 微任务 |
---|---|
process3 | |
then3 |
5、最终图解:
参考优秀文章:
「硬核JS」一次搞懂JS运行机制
这一次,彻底弄懂 JavaScript 执行机制
看完还不懂JavaScript执行机制(EventLoop),你来捶我