目录
目录
宏任务和微任务
常见的宏任务和常见的微任务
编辑
扩展:什么是同步编程:
一些理论知识:
堆,栈,队列
先看一段程序:
<script>
function fn(){
for(var i=0;i<100;i++){}
console.log(3);
}
console.log(1);
fn();
setTimeout(function(){console.log(4);},1000);
console.log(2);
</script>
分析:程序结果是 1 3 4 2 么?是就错了!!! 正确答案是 1 3 2 4 那为什么是这样呢?
这就要涉及到 JS的异步编程
JS代码运行机制:事件循环。
理解JavaScript的事件循环往往伴随着宏任务和微任务、JavaScript 单线程执行过程及浏览器异步机制等相关问题。
JavaScript 引擎负责解析,执行 JavaScript 代码,但它并不能单独运行,通常都得有一个宿主环境,一般如浏览器或 Node 服务器(不同宿主事件循环机制是不一样的)
1.浏览器JS异步执行原理
JS是单线程的,也就是同一个时刻只能做一件事情, 那么请你思考:为什么浏览器可以同时执行异步任务呢?
因为浏览器是多线程的,当JS要执行异步任务时,浏览器会另外启动一个线程去执行该任务。也就是说,“JS 单线程的”指的是执行JS代码的线程只有一个,是浏览器提供的JS引擎线程(主线程)。浏览器中还有定时器线程和HTTP请求线程等,这些线程主要不是来跑JS代码的。
比如主线程中需要发一个AJAX求,就把这个任务交给另一个浏览器线程(HTTP 请求线程)真正发送请求,待请求回来了,再将callback里需要执行的JS回调交给JS引擎线程去执行。即浏览器才是真正执行发送请求这个任务的角色,而JS只负责执行最后的回调处理。所以这里的异步不是JS自身实现的,其实是浏览器为其提供的能力。
以Chrome为例,浏览器不仅有多个线程,还有多个进程,如渲染进程、GPU进程和插件进程等。
而每个tab标签页都是一个独立的渲染进程,所以一个tab异常崩溃后,其他tab基本不会被影响。
作为前端开发者,要重点关注其渲染进程,渲染进程下包含了JS引擎线程、HTTP 请求线程和定
时器线程等,这些线程为JS在浏览器中完成异步任务提供了基础。
2.浏览器中的事件循环
执行栈和任务队列
JS在解析一段代码时,会将同步任务按顺序排执行栈里面,然后依次执行里面的同步任务。当遇到异步任务时就交给其他线程处理,待当前执行栈所有同步代码执行完成后,会从一个队列中去取
出已完成的异步任务的回调加入执行栈继续执行,遇到异步任务时又交给其他线程,... 如此循环往
复。而其他异步任务完成后,将回调放入任务队列中待执行栈来取出执行。
JS按顺序执行执行栈中的方法,每次执行一个方法时,会为这个方法生成独有的执行环境(. 上下文
context),待这个方法执行完成后,销毁当前的执行环境,并从栈中弹出此方法(即消费完成), 然
后继续下一个方法。
可见,在事件驱动的模式下,至少包含了一个执行循环来检测任务队列是否有新的任务。通过不断循环去取出异步回调来执行,这个过程就是事件循环,每一次循环就是一个事件周期或称为一次
tick。
宏任务和微任务
任务队列不只一个,根据任务的种类不同,可以分为微任务(micro task)队列和宏任务(macro
task)队列。事件循环的过程中,执行栈在同步代码执行完成后,优先检查微任务队列是否有任务需要执行,如果没有,再去宏任务队列检查是否有任务执行,如此往复。微任务一般在当前循环就会优先执行, 而宏任务会等到下一次循环,因此,微任务一般比宏任务先执行, 且微任务队列只有一个, 宏任务队列可能有多个。另外我们常见的点击和键盘等事件也属于宏任务。
常见的宏任务和常见的微任务
案例分析:
<script>
console.log('同步代码1');
setTimeout(() => {
console.log('setTimeout')
}, 0)
new Promise((resolve) => {
console.log('同步代码2')
resolve()
}).then(() => {
console.log('promise.then')
})
console.log('同步代码3');
// 最终输出"同步代码1"、"同步代码2"、"同步代码3"、"promise.then"、"setTimeout"
</script>
分析:上面的代码将按如下顺序输出为: "同步代码1"、"同步代码2"、 "同步代码3"、 "promise.then"、 "setTimeout", 具体分析如下。
(1) setTimeout 回调和promise.then 都是异步执行的,将在所有同步代码之后执行;
顺便提一下,在浏览器中setTimeout的延时设置为0的话,会默认为4ms,在Node.js中为1ms。
(2)虽然promise.then写在后面,但是执行顺序却比setTimeout优先,因为它是微任务;
(3) new Promise同步执行的,promise.then 面的回调才异步的。
现在回头看最前面的题就很简单了。
也有人这样去理解:微任务是在当前事件循环的尾部去执行;宏任务是在下一次事件循环的开始去执行。我们来看看微任务和宏任务的本质区别是什么。
我们已经知道,JS遇到异步任务时会将此任务交给其他线程去处理,自己的主线程继续往后执行同步任务。比如setTimeout的计时会由浏览器的定时器线程来处理,待计时结束,就将定时器回调任务放入任务队列等待主线程来取出执行。前面我们提到,因为JS是单线程执行的,所以要执行异步任务,就需要浏览器其他线程来辅助,即多线程是JS异步任务的一个明显特征。
我们再来分析下promise.then (微任务)的处理。当执行到promise.then时,V8引擎不会将异步任务交给浏览器其他线程,而是将回调存在自己的一个队列中,待当前执行栈执行完成后,立马去执行promise.then存放的队列,promise.then 微任务没有多线程参与,甚至从某些角度说,微任务都不能完全算是异步,它只是将书写时的代码修改了执行顺序而已。
setTimeout有“定时等待这个任务,需要定时器线程执行; ajax 请求有‘发送请求”这个任务,需要HTTP线程执行,而promise.then它没有任何异步任务需要其他线程执行,它只有回调,即使有,也只是内部嵌套的另一个宏任务。
简单小结一下:微任务和宏任务的本质区别:
宏任务特征:有明确的异步任务需要执行和回调;要其他异步线程支持。
微任务特征:没有明确的异步任务需要执行,只有回调;不需要其他异步线程支持。
视图更新渲染
定时器误差
事件循环中,总是先执行同步代码后,才会去任务队列中取出异步回调来执行。当执行setTimeout
时,浏览器启动新的线程去计时,计时结束后触发定时器事件将回调存入宏任务队列,等待JS主线程来取出执行。如果这时主线程还在执行同步任务的过程中,那么此时的宏任务就只有先挂起,这就造成了计时器不准确的问题。同步代码耗时越长,计时器的误差就越大。不仅同步代码,由于微任务会优先执行,所以微任务也会影响计时,假设同步代码中有一个死循环或者微任务中递归不断在启动其他微任务,那么宏任务里面的代码可能永远得不到执行。所以主线程代码的执行效率提升是一件很重要的事情。
扩展:什么是同步编程:
就是计算机一行一行按顺序依次执行代码,当前代码任务耗时执行时会阻塞后 续代码的执行。 同步编程,是一种典型的请求-响应模型,当请求调用一个函数或方法 后,需等待其响应返回,然后执行后续代码。
优点:
同步编程,代码按序依次执行,能很好的保证程序的执行。
缺点:
在某些场景下,比如读取文件内容,或请求服务器接口数据,需要根据返回的数据内容执行后续操作,读取文件和请求接口直到数据返回这一 过程是需要时间的,网络越差,耗费时间越长,如果按照同步编程方式实 现,在等待数据返回这段时间,JavaScript 是不能处理其他任务的,此时 页面的交互,滚动等任何操作也都会被阻塞,这显然是及其不友好,不可接受的。比如:我们想通过 Ajax 请求数据来渲染页面,这是一个在我们前端当中很常见渲染页面的方式。基本每 个页面都会都这样的过程。在这里用同步的方式请求页面会怎么样?浏览 器锁死,不能进行其他操作。而且每当发送新的请求,浏览器都会锁死, 用户体验极差。
一些理论知识:
堆,栈,队列
堆(heap):内存中某一未被阻止的区域,通常存储对象(引用类型);
栈(stack):先进后出,后进先出(想到子弹夹) 的顺序存储数据结构,通常存储函数参数和基本类型值变量(按值访问);
队列(queue):先进先出顺序存储数据结构
事件循环顺序
1. js在执行上下文栈的同步任务执行完后
2. 首先会去执行微任务队列,按照队列先进先出的原则,一次执行完所有Microtask微任务队列任务
3. 当前微任务执行完后,判断是否有UI渲染如果有就执行渲染过程,没有就跳过
4. 开始执行宏任务队列,一次只执行一个。执行完后检查当前微任务队列是否有任务
有,执行微任务队列,直至清空微任务
没有,执行以一个宏任务
重复循环上述步骤形成一个事件循环,可以看出各任务的执行先后关系:同步任务 > 微任务 > UI渲染 > 宏任务