![c6254f0ed04f5a69b82a280b5b6c86c3.png](https://img-blog.csdnimg.cn/img_convert/c6254f0ed04f5a69b82a280b5b6c86c3.png)
其实这一块的理解特别重要, 前端的同学们千万要重视
为什么我要说事件循环机制特别重要呢?
举个栗子,相信很多同学面试的都看过这道题:
for(var i = 0; i< 10 ; i++) {
setTimeout(function() {
console.log(i);
}, 0);
}
相信很多同学都知道答案,代码执行完之后就会出现10个10,但是如果同学们面的是中高级前端岗位,面试官基本上还会问一句: 为什么会是10个10?
这个很多同学,都只会回答异步的问题.
虽然,其实这种回答方式虽然没有什么错误,但是也肯定不是面试官想要听到的回答,因为作为中高级的前端岗位,对js内置的事件循环机制是必须掌握的.
对于部分大厂,这个部分也是实习生必须掌握的一个知识点
而且,解释完这个,你还需要解释下面这段代码.
如果不了解事件循环机制,只能呜噜呜噜,说几句块级作用域云云的就混过去了.
你的水平在面试官看来一下子就低了很多很多
for(let i = 0; i< 10 ; i++) {
setTimeout(function() {
console.log(i);
}, 0); // 答案是 0,1,2,3,4,5,6,7,8,9
}
以上的所有内容只是为了让同学们知道,js事件循环机制的重要性
下面开始介绍正题:
js事件循环机制
首先先聊一下js这种语言,很多人说js是一种单线程语言,其实这不是特别准确.
准确的说法应该是js是一种单个主线程的浏览器语言.
而除了主线程,还包括诸如h5提供的web-worker多线程(不懂的同学可以看一下我之前的文章),还有就是今天我们主要要学习的调用栈(call stack)
- 调用栈(call stack)
说起栈,相信大家都不会觉得陌生,在学习七大基本数据类型的时候.
我们都有了解过,栈(stack)负责存贮简单数据本身和复杂数据类型的地址指针,堆(heap)则负责存储复杂数据类型数据本身.
栈有一个特点: 入口和出口只有一个, 我们可以将其想象成一个水桶.
入栈也称压栈,出栈也称弹栈.
而我们今天要介绍的调用栈(call stack)也具有同样的特征.
举个例子
function a() {
console.log("I'm a!");
};
function b() {
a();
console.log("I'm b!");
};
b();
假设我们这个js文件名字叫做main.js
此时call stack里面的情况就是
函数a;
函数b;
main.js;
换句话说,main.js位于栈底,上面压着函数b,函数b上面又压着函数a;
随后,函数a执行完,打印了"I'm a!"后,出栈.
紧接着,函数b执行完,打印了"I'm b!"后,出栈.
最后,整个mian,js执行完,main.js出栈.
整个main.js就在这样的调用栈机制下完成了运行.
但是!!! 如果我们有异步任务(async task)的时候,这种栈机制就很那满足我们的需求了
我们要怎么办呢?
没关系,因为js还为我们提供了一个任务队列(task queue)机制
2. 任务队列(task queue)
拿我们最常见的异步任务setTimeout来举例.
当我们的js文件从上往下解析的时候,碰到类似setTimeout这种异步任务的时候并不会直接执行,而是将其暂时挂起在webcore模块上.
异步任务在webcore中执行,完成后会通知调用栈(call stack),并将其同步部分(一般是回调)放入任务队列(task queue)里面去.
而主线程的任务完成后,就会去执行任务队列(task queue)里面的内容.
而整个过程叫做 事件循环
上张图来理解一下:
![17f16a1f792eaecc16463a8383518747.png](https://img-blog.csdnimg.cn/img_convert/17f16a1f792eaecc16463a8383518747.png)
这个时候,我们再回头看一下之前的那个问题,你知道要怎么回答了吗?
for(var i = 0; i< 10 ; i++) {
setTimeout(function() {
console.log(i);
}, 0);
}
没错,你该这么说:
首先,script部分入栈,for循环作为同步任务,率先执行,而setTimeout作为异步任务被webcore中的timer模块暂时挂起,等到主进程执行完,由于var出来的变量位于他自身的函数作用域当中,是会被for循环重复赋值.
换句话说,主线程执行完后i已经变成了10,此时才会开始执行任务队列中的console.log部分,这个时候当然会打印出10个10;
而这个问题,你又需要怎么回答呢?
for(let i = 0; i< 10 ; i++) {
setTimeout(function() {
console.log(i);
}, 0); // 答案是 0,1,2,3,4,5,6,7,8,9
}
首先,你需要回答出es6提供的let声明方式,存储的变量会单独开辟出一个作用域,被我们称之为"块级作用域";
每个块级作用域依次入栈出栈(进去了执行完,立即出,不会出现堆栈的情况),每次执行完都会执行一次任务队列,此时的i还保持着for循环当时的赋值,所以答案变成了0,1,2,3,4,5,6,7,8,9
相信各位同学应该已经明白了,面试官究竟想要问你什么了吧~
当然,只懂了这些还远远不够~
很多同学在面试的时候也有面试官会问promise和setTimeout的执行顺序问题
比如这个面试题:
setTimeout(() => {
console.log(4)
}, 0);
new Promise((resolve) =>{
console.log(1);
for (var i = 0; i < 10000000; i++) {
i === 9999999 && resolve();
}
console.log(2);
}).then(() => {
console.log(5);
});
console.log(3);
这个题的答案是1, 2, 3, 5, 4
是不是很神奇?
相信很多同学已经开始不清楚为什么会这样了
因为我们还需要学习宏任务和微任务
3. 宏任务(macro task) 和 微任务(micro task)
任务队列又分为 macro-task(宏任务)
与 micro-task(微任务)
,在最新标准中,它们被分别称为 task
与 jobs
。
macro-task(宏任务)
大概包括:script(整体代码)
,setTimeout
,setInterval
,setImmediate(NodeJs)
,I/O
,UI rendering
。micro-task(微任务)
大概包括:process.nextTick(NodeJs)
,Promise
,Object.observe(已废弃)
,MutationObserver(html5新特性)
- 来自不同任务源的任务会进入到不同的任务队列。其中
setTimeout
与setInterval
是同源的。
事实上,事件循环决定了代码的执行顺序,从全局上下文进入函数调用栈开始,直到调用栈清空,然后执行所有的micro-task(微任务)
,当所有的micro-task(微任务)
执行完毕之后,再执行macro-task(宏任务)
,其中一个macro-task(宏任务)
的任务队列执行完毕(例如setTimeout
队列),再次执行所有的micro-task(微任务)
,一直循环直至执行完毕。
解析
现在我就开始解析上面的代码。
- 第一步,整体代码
script
入栈,并执行setTimeout
后,执行Promise
:
![a2c5a97b0db9e0f375abdd000e9fec47.png](https://img-blog.csdnimg.cn/img_convert/a2c5a97b0db9e0f375abdd000e9fec47.png)
- 第二步,执行时遇到
Promise
实例,Promise
构造函数中的第一个参数,是在new
的时候执行,因此不会进入任何其他的队列,而是直接在当前任务直接执行了,而后续的.then
则会被分发到micro-task
的Promise
队列中去。
![1c4c186682b663b1371eb27350199c1a.png](https://img-blog.csdnimg.cn/img_convert/1c4c186682b663b1371eb27350199c1a.png)
![86aa402610d8f90d851c3e4a3d10635d.png](https://img-blog.csdnimg.cn/img_convert/86aa402610d8f90d851c3e4a3d10635d.png)
- 第三步,调用栈继续执行宏任务
app.js
,输出3
并弹出调用栈,app.js
执行完毕弹出调用栈:
![793118fab0aab2c6f2db3198683415da.png](https://img-blog.csdnimg.cn/img_convert/793118fab0aab2c6f2db3198683415da.png)
![13e6d27c2897d9a0e544c383bbacf49a.png](https://img-blog.csdnimg.cn/img_convert/13e6d27c2897d9a0e544c383bbacf49a.png)
- 第四步,这时,
macro-task(宏任务)
中的script
队列执行完毕,事件循环开始执行所有的micro-task(微任务)
:
![6f08dad0ed6326ab3c36372acac38975.png](https://img-blog.csdnimg.cn/img_convert/6f08dad0ed6326ab3c36372acac38975.png)
- 第五步,调用栈发现所有的
micro-task(微任务)
都已经执行完毕,又跑去macro-task(宏任务)
调用setTimeout
队列:
![b57d0e0f4502815756bb6979dab15305.png](https://img-blog.csdnimg.cn/img_convert/b57d0e0f4502815756bb6979dab15305.png)
- 第六步,
macro-task(宏任务)
setTimeout
队列执行完毕,调用栈又跑去微任务进行查找是否有未执行的微任务,发现没有就跑去宏任务执行下一个队列,发现宏任务也没有队列执行,此次调用结束,输出内容1,2,3,5,4
。
那么上面这个例子的输出结果就显而易见。大家可以自行尝试体会。
总结
- 不同的任务会放进不同的任务队列之中。
- 先执行
macro-task
,等到函数调用栈清空之后再执行所有在队列之中的micro-task
。 - 等到所有
micro-task
执行完之后再从macro-task
中的一个任务队列开始执行,就这样一直循环。 - 宏任务和微任务的队列执行顺序排列如下:
macro-task(宏任务)
:script(整体代码)
,setTimeout
,setInterval
,setImmediate(NodeJs)
,I/O
,UI rendering
。micro-task(微任务)
:process.nextTick(NodeJs)
,Promise
,Object.observe(已废弃)
,MutationObserver(html5新特性)