这是我参与「第四届青训营 」笔记创作活动的第2天
小白一枚,欢迎指正
话不多说,直接上题
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
要想理解答案,需要具备以下小知识,没明白的不妨往下挖挖 ^.^
一、V8 引擎内的 栈
也被称为“调用栈、执行栈”。栈是一种LIFO(last input first output)后进先出的数据结构。
当函数被推入执行栈,JS引擎就开始解析函数体,在堆里创建变量、把新的函数调用推入栈顶、或者把函数自身分发给Web API调用的专属容器。
当函数有了返回值,或者被分发到Web API容器,它就会被弹出栈,同时下一个函数调用会被推入栈顶。
如果JS引擎执行完一个函数,并且该函数没有明确的指明返回值,JS引擎会默认的返回undefined然后再将之弹出栈。人们通常说的JS同步运行指的就是JS引擎解析函数然后弹出栈(再运行下一个函数)的运行流程。简言之,在单线程下同一时间只做一件事。
二、Web API
从栈发送到Web API容器内的Web API调用(比如事件监听函数、HTTP/AJAX请求、或者是定时器函数)会一直在Web API容器内,直到触发操作为止。这些触发操作可能是一个点击事件被触发、或者是HTTP请求完成从数据源获取数据、或者是定时器达到触发的时间点,一旦达到触发条件,一个回调函数就会被推入第四个也是最后一个容器:回调队列。
回调队列将按添加的顺序存储所有回调函数。当调用栈函数任务为空时,它会将队列开头的回调函数发送到调用栈。当调用栈函数任务再次清空时,它将发送下一个队列首位的回调函数。
回调队列是一种FIFO(first input first output)先进先出的数据结构。
三、渲染进程
WebAPI处理由栈中弹出后发送来的任务 所使用的线程和JS引擎所使用的的线程是不同的,浏览器有多个进程,其中渲染进程是我们主要关心的,里面又包含了多个线程。
页面的渲染,JS的执行,事件的循环,都在这个渲染进程内进行。
浏览器的渲染进程是多线程的, 主要包含的线程有:
- GUI渲染线程
- 负责渲染浏览器界面,解析HTML,CSS,构建DOM树和RenderObject树,布局和绘制等。
- 当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行
- 注意,GUI渲染线程与JS引擎线程是互斥的,当JS引擎执行时GUI线程会被挂起(相当于被冻结了),GUI更新会被保存在一个队列中等到JS引擎空闲时立即被执行。
- JS引擎线程
- 也称为JS内核,负责处理Javascript脚本程序。(例如V8引擎)
- JS引擎线程负责解析Javascript脚本,运行代码。
- JS引擎一直等待着任务队列中任务的到来,然后加以处理,一个Tab页(renderer进程)中无论什么时候都只有一个JS线程在运行JS程序
- 同样注意,GUI渲染线程与JS引擎线程是互斥的,所以如果JS执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞。
- 事件触发线程
- 归属于浏览器而不是JS引擎,用来控制事件循环(可以理解,JS引擎自己都忙不过来,需要浏览器另开线程协助)
- 当JS引擎执行代码块如setTimeOut时(也可来自浏览器内核的其他线程,如鼠标点击、AJAX异步请求等),会将对应任务添加到事件线程中
- 当对应的事件符合触发条件被触发时,该线程会把事件添加到待处理队列的队尾,等待JS引擎的处理
- 注意,由于JS的单线程关系,所以这些待处理队列中的事件都得排队等待JS引擎处理(当JS引擎空闲时才会去执行)
- 定时触发器线程
setInterval与setTimeout所在线程- 浏览器定时计数器并不是由JavaScript引擎计数的,(因为JavaScript引擎是单线程的, 如果处于阻塞线程状态就会影响记计时的准确)
- 因此通过单独线程来计时并触发定时(计时完毕后,添加到事件队列中,等待JS引擎空闲后执行)
- 注意,W3C在HTML标准中规定,规定要求setTimeout中低于4ms的时间间隔算为4ms。
- 异步http请求线程
- 在XMLHttpRequest在连接后是通过浏览器新开一个线程请求
- 将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将这个回调再放入事件队列中。再由JavaScript引擎执行。
至此,我们可以得到整个任务执行流程,这个流程也叫事件循环
四、最后一步,宏任务和微任务
JS中分为两种任务类型:macrotask和microtask,在ECMAScript中,microtask称为jobs,macrotask可称为task
-
macrotask(又称之为宏任务),可以理解是每次执行栈执行的代码就是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行)
- 每一个task会从头到尾将这个任务执行完毕,不会执行其它
- 浏览器为了能够使得JS内部task与DOM任务能够有序的执行,会在一个task执行结束后,在下一个 task 执行开始前,对页面进行重新渲染
- (
task->渲染->task->...)
-
microtask(又称为微任务),可以理解是在当前 task 执行结束后立即执行的任务
- 也就是说,在当前task任务后,下一个task之前,在渲染之前
- 所以它的响应速度相比setTimeout(setTimeout是task)会更快,因为无需等渲染
- 也就是说,在某一个macrotask执行完后,就会将在它执行期间产生的所有microtask都执行完毕(在渲染前)
什么样的场景会形成macrotask和microtask呢?
- macrotask:主代码块,setTimeout,setInterval等(可以看到,事件队列中的每一个事件都是一个macrotask)
- microtask:Promise,process.nextTick等
macrotask和microtask是由什么控制维护的呢?
- macrotask中的事件都是放在一个事件队列中的,而这个队列由事件触发线程维护
- microtask中的所有微任务都是添加到微任务队列(Job Queues)中,等待当前macrotask执行完毕后执行,而这个队列由JS引擎线程维护
五、代入解释答案
-
执行一个宏任务(栈中没有就从事件队列中获取)
这个时候打印出执行栈中的同步代码,也就是script start 和script end
-
执行过程中如果遇到微任务,就将它添加到微任务的任务队列中
这个时候,确实在过程中发现了微任务,也就是promise,将这个任务放到微任务队列,此时执行栈中只剩下一个定时器任务,这个任务不会马上执行,他会被放入宏任务队列,执行栈空闲
-
宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)
此时短暂空闲的执行栈又插入promise,输出promise1和promise2,然后继续空闲
-
当前宏任务执行完毕,开始检查渲染,然后GUI线程接管渲染
-
渲染完毕后,JS线程继续接管,开始下一个宏任务(从事件队列中获取)
因为执行栈空闲,从宏任务队列中取出已经满足条件的定时器任务,这个时候输出了最后一行setTimeout