1 关于javascript语言
JavaScript语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。
作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?所以,为了避免复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,将来也不会改变。
2 Event Loop(事件循环)
事件循环时js的执行机制。由于Js是单线程语言,这就意味着所有执行任务都需要排队执行,前一个执行完,后一个才能够执行。如果当前任务是比较耗时的操作(I/O等)那么后面的任务就得慢慢等,此时js引擎应该是空闲的,这样执行效率大大降低。为了提高效率,js语言设计者将所有任务分为同步和异步两种。
执行过程中,所有同步任务在主线程中顺序执行形成执行栈(execution context stack),遇到异步任务,则抛出给其他线程,继续往下执行同步任务。与此同时其他线程执行完后异步任务后会将对应回调函数推入消息队列(任务队列),待执行栈上所有同步任务执行完后(执行栈为空)。消息队列中任务将会被推入执行栈中执行。不断循环往复,直到所有任务执行完。这就是事件循环机制,也就是js的执行机制。
下面将仔细拆分上面一段话中的每个关键点。
栈和队列是什么
队列的特点就是先进先出。消息队列中的任务将会按照顺序被推入执行栈中执行,其他线程执行完后对应的回调函数放入消息队列也会按顺序放。
栈的特点是后进先出。(注意不要理解成同步代码进入执行栈中,按栈的出栈顺序来执行)。还需要讲清楚
同步任务与异步任务
js中异步任务有哪些呢?
I/O操作,Promise, setTimeout, setInterval等。
其他线程
- GUI渲染线程:
- 用于解析html为DOM树,解析css为CSSOM树,布局layout,绘制paint
- 当页面需要重排reflow,重绘repaint时,使用该线程
- 与js引擎线程互斥
- 事件触发线程
- 当对应事件触发(不论是WebAPIs完成事件触发,还是页面交互事件触发)时,该线程会将事件对应的回调函数放入callback queue(任务队列)中,等待js引擎线程的处理
- 定时触发线程
- 对应于setTimeout,setInterval API,由该线程来计时,当计时结束,将事件对应的回调函数放入任务队列中
- 当setTimeout的定时的时间小于4ms,一律按4ms来算
- http请求线程
- 每有一个http请求就开一个该线程
- 当检测到状态变更的话,就会产生一个状态变更事件,如果该状态变更事件对应有回调函数的话,则放入任务队列中
- 任务队列轮询线程
- 用于轮询监听任务队列,以知道任务队列是否为空
示例1
console.log("aaaaaa")
setTimeout(()=>console.log("bbbbbb"),0)
console.log("cccccc")
/*输出结果
aaaaaa
cccccc
bbbbbb
*/
由于setTimeout是异步任务,所以会被交给定时触发线程执行,主线程继续往下执行`console.log("cccccc")`,与此同时,定时触发器线程也在执行,当有了执行结果后,会将回调函数推入消息队列中去。执行栈为空时,消息队列中console.log("bbbbbb")任务会被推入执行栈中,因此最后打印bbbbbb。
我们进入正题,除了广义的同步任务和异步任务,我们对任务有更精细的定义
- macro-task(宏任务):包括整体代码script,setTimeout,setInterval
- micro-task(微任务):Promise,process.nextTick
所以上面讲到的队列可以细分为宏任务队列和微任务队列
事件循环,宏任务,微任务的关系如图所示: