了解到vue更新dom的机制是异步的,就牵扯出了事件循环、任务队列、微任务、宏任务等,这几天开始查阅各种资料去学习这一块,把这些概念及JS的执行机制了解透彻了,遇到bug的时候,可以快速精准的定位问题,话不多说,一起去看看~
“JavaScript 是单线程、异步、非阻塞、解释型脚本语言。”
单线程与多线程
- 单线程
JavaScript语言的一大特点就是:单线程,同一时间只能做一件事。往细了讲,就是JS的这个线程用来负责解释和执行JavaScript代码,也就是我们说的主线程。主线程上的代码是按顺序执行的。
- 多线程
why?为什么JavaScript不设计成多线程的呢?大家一起齐心协力干活,这样不就缩短了执行的时间吗?
JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?
所以,为了避免复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,将来也不会改变。
为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。【摘自:阮一峰】
同步与异步
- 同步
JavaScript是单线程的,主线程上的代码是自上而下按顺序执行的:
console.log('a')
console.log('b')
console.log('b')
大家都知道上面的代码会依次打印出:a、b、c
console.log('a')
$.ajax({
url,
async:false,// 同步请求
success:function(){
console.log('b')
}
})
console.log('b')
那如果其中某项任务是耗时比较长的ajax请求呢,这种时候如果依然使用同步请求,那么后面的任务就会被阻塞,浏览器就会处于暂时‘卡死’状态,对用户是非常不友好的。
- 异步
然而JavaScript中代码执行的时候,大家发现定时器等并不会阻塞代码的执行,是因为主线程将遇到的计时器、DOM事件、ajax异步请求等直接交给了webapi,也就是浏览器提供的别的线程去处理了,主线程继续执行后面的任务,这样就实现了异步并且是非阻塞的。
异步一般包括:
- 定时器
- 事件绑定
- Ajax
- 回调函数
- ...
事件循环与任务队列
在讲述事件循环与任务队列前,需要讲述一下浏览器与JavaScript之间的关系:
JavaScript是单线程的,浏览器是多进程(大家自主去了解线程与进程的关系)。
-
浏览器包括哪些进程
- Browser进程
- 第三方插件进程
- GPU进程
- 浏览器渲染进程
-
浏览器渲染进程
对于前端来说,最重要的是理解了浏览器的渲染机制,整体也就梳理清楚了。浏览器的渲染进程是多线程的,包括哪些线程呢:
-
GUI渲染线程
- 负责渲染浏览器界面,解析HTML、CSS,构建DOM树和RenderObject树,布局与绘制等。
- 当界面需要重绘(repaint)或由于某种操作引发回流(reflow)时,该线程就会执行
- GUI渲染线程与JS引擎线程是互斥的,当JS引擎执行时,GUI线程会被挂起,GUI更新会被保存在一个队列中,等到JS引擎空闲时立即被执行。
-
JS引擎线程
- 成为JS内核,负责处理JavaScript脚本程序(我们所熟悉的引擎是chrom与nodejs中使用的v8引擎)
- 这个引擎由两个部分组成,内存堆与调用栈
- 内存堆:进行内存分配。如变量赋值
- 调用栈:调用栈中按顺序执行主线程的代码,当调用栈为空时,JS引擎就会读取任务队列,看看任务队列中有哪些事件,对应的异步任务结束等待状态,进入执行栈,开始执行。
- 只要主线程空了,就会读取‘任务队列’,这就是JavaScript的运行机制。
-
事件触发线程
- 归属于浏览器,而不是JS引擎,用来控制事件轮询
- 当JS引擎执行代码块如鼠标点击等,会将对应的任务添加到事件触发线程中
- 当对应的事件符合触发条件被触发时,该线程会把事件添加到待处理的任务队列中,等待JS引擎的处理
-
定时器触发线程
- setTimeout与setInterval所在线程
- 浏览器定时计数器并不是由JavaScript引擎计数的,是由定时触发线程来计数的,计时完毕后,添加到事件队列中,等待JS引擎空闲后执行
- HTML5标准规定了setTimeout()的第二个参数的最小值(最短间隔),不得低于4毫秒,如果低于这个值,就会自动增加。另外,对于那些DOM的变动(尤其是涉及页面重新渲染的部分),通常不会立即执行,而是每16毫秒执行一次。这时使用requestAnimationFrame()的效果要好于setTimeout()。
- 注意:setTimeout倒计时结束后,只是将事件插入到任务队列,并不会立马执行,必须等到主线程代码执行完,才会执行去执行它指定的回调函数。要是当前代码耗时很长,有可能要等很久,所以并没有办法保证,回调函数一定会在setTimeout()指定的时间执行。
-
异步http请求线程
- 在XMLHTTPRequest连接后,通过浏览器新开了一个线程请求
- 将检测到状态变更时,如果设置有回调函数,异步线程会产生状态变更事件,将这个回调放入任务队列中,等待JS引擎执行
-
-
任务队列
任务队列是需要排队的,前一个任务执行完了,才能执行下一个任务,这也就是JavaScript的单线程,前一个任务未执行结束,下一个任务就不会执行,需要一直等着。
所有的任务可以分为:同步任务与异步任务。同步任务就是在主线程上按顺序等待被执行;异步任务不会直接进入主线程,而是由栈中的代码调用各种外部API,由外部的API(由浏览器提供)去执行,将相应的事件(click、load、done)加入任务队列中。栈中的代码执行完毕后,主线程会去读取任务队列,依次执行那些事件所对应的回调函数。
- 事件循环(Event Loop)
主线程从"任务队列"中读取事件,这个过程是循环不断的,整个的这种运行机制称为 Event Loop(事件循环)
异步操作会将相关回调添加到任务队列中。而不同的异步操作添加到任务队列的时机也不同,如 onclick, setTimeout, ajax 处理的方式都不同,这些异步操作是由浏览器内核的 webcore 来执行的,webcore 包含上图中的3种 webAPI,分别是 DOM Binding、network、timer模块。
1. onclick 由浏览器内核的 DOM Binding 模块来处理,当事件触发的时候,回调函数会立即添加到任务队列中。
2. setTimeout 会由浏览器内核的 timer 模块来进行延时处理,当时间到达的时候,才会将回调函数添加到任务队列中。
3. ajax 则会由浏览器内核的 network 模块来处理,在网络请求完成返回之后,才将回调添加到任务队列中。
任务队列是在事件循环之上的,事件循环每次 tick 后会查看 ES6 的任务队列中是否有任务要执行,也就是 ES6 的任务队列比事件循环中的任务(事件)队列优先级更高。如 Promise 就使用了 ES6 的任务队列特性。
Event Loop只是负责告诉你该执行那些任务,或者说哪些回调被触发了,真正的逻辑还是在进程中执行的。
微任务与宏任务
- 微任务
microtask,可以理解是在当前 task 执行结束后立即执行的任务。也就是说,在当前task任务后,下一个task之前,在渲染之前。
注意:
new Promise在实例化的过程中所执行的代码都是同步进行的,而then中注册的回调才是异步执行的。
- 宏任务
(macro)task,可以理解是每次执行栈执行的代码就是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行)。
浏览器为了能够使得JS内部(macro)task与DOM任务能够有序的执行,会在一个(macro)task执行结束后,在下一个(macro)task 执行开始前,对页面进行重新渲染,流程如下:
注意:
requestAnimationFrame姑且也算是宏任务吧,requestAnimationFrame在MDN的定义为,下次页面重绘前所执行的操作,而重绘也是作为宏任务的一个步骤来存在的,且该步骤晚于微任务的执行 宏任务必然是在微任务之后执行的
- async与await属于什么
async/await相当于是promise的语法糖,promise属于微任务,那么await等同于promise.then.
- 示例
console.log('script start');
setTimeout(function () {
console.log('setTimeout');
}, 0);
Promise.resolve()
.then(function () {
console.log('promise1');
})
.then(function () {
console.log('promise2');
});
console.log('script end');
执行顺序为:script start、script end、promise1、promise2、setTimeout
这个弄明白了,那么也基本分得清什么是微任务,什么是宏任务了。
可以在微任务与宏任务测试网站去操作
总结
JavaScript是单线程的,主线程会按顺序执行同步任务,遇到异步任务会调用外部的API,由外部的API去执行,当异步任务处于可执行的状态时,外部的API会将这些任务加入任务队列中,等待JS引擎执行。当栈中的代码执行完毕后,主线程会主动去读取任务队列,依次执行对应的回调函数,这时候会先查看是否有微任务可以执行,有微任务先执行微任务。
本人才疏浅薄,欢迎大家指正!
参考
阮一峰:JavaScript 运行机制详解:再谈Event Loop