JS的执行顺序:

就像上菜一样,厨师们总是将能直接上的先准备好,比如米饭,水,在JS中这就是同步任务,先执行完整个script中的同步代码再去执行异步,而异步中也分为两种,但大厨不能直接上饭和水因为还没有做菜呢,所以厨师就将菜单分为比较耗时的硬菜和耗时较短的软菜,厨师会先把软菜都做完,然后和米饭一起上桌让客人先吃着,在这段时间内再去做硬菜,在JS中软菜就是微任务,硬菜就是宏任务,上桌就是浏览器渲染。遵循先做软菜再做硬菜的原则,如果在做完一个硬菜后发现又有软菜来了,就先转去做软菜,把所有的软菜都做完上桌后,再去做硬菜。

总结一下就是: 同步任务(script整体) -> 宏任务加入宏任务队列 -> 微任务加入微任务队列 -> 执行全部的微任务 -> 执行宏任务 —-如果有微任务进入队列—> 执行全部微任务 -> 执行宏任务 —-循环—> ….
注意:在执行完微任务后,浏览器可能会渲染
补充:setTimeout等宏任务会等待script代码中的同步任务执行完毕后才会执行,如果script中有alet语句,则setTimeout的执行会被卡住,直到alert做出响应为止:alert(警示对话框,可以选择阻断当前执行)

概念4:Event Loop

JS到底是怎么运行的呢?
事件循环(event loop)
JS分为同步任务和异步任务,同步任务会在主线程上执行(形成执行栈,先进后出),异步任务会先放置在任务队列中(先进先出);
当主线程上的同步任务全部执行完成后,js会在任务队列中依次取出异步任务并执行。
JS主线程不断的循环往复的从任务队列中读取任务,执行任务,这中运行机制遍称为事件循环。
但事件循环中的任务队列并不是唯一的,每个事件循环都有一个微任务队列以及多个宏任务队列。

宏任务(Macrotasks)与微任务(Microtasks)
异步任务可分为 宏任务 和 微任务两类,不同的API注册的异步任务会依次进入自身对应的队列中,然后等待 Event Loop 将它们依次压入执行栈中执行。从本质上来说,宏任务是ES6语法规定的,微任务是浏览器规定的,微任务的优先级要高于宏任务。
宏任务包含的API有:script(整体代码)、setTimeout、setInterval等
微任务包含的API有:Promise等
因此一次事件循环的大致过程为:
image.png

JS引擎常驻于内存中,等待宿主将JS代码或函数传递给它。
也就是等待宿主环境分配宏观任务,反复等待 - 执行即为事件循环。
Event Loop中,每一次循环称为tick,每一次tick的任务如下:

  • 执行栈选择最先进入队列的宏任务(一般都是script),执行其同步代码直至结束;
  • 检查是否存在微任务,有则会执行至微任务队列为空;
  • 如果宿主为浏览器,可能会渲染页面;
  • 开始下一轮tick,执行宏任务中的异步代码(setTimeout等回调)。

    概念5:宏任务和微任务

    ES6 规范中,microtask 称为 jobs,macrotask 称为 task
    宏任务是由宿主发起的,而微任务由JavaScript自身发起。
    在ES3以及以前的版本中,JavaScript本身没有发起异步请求的能力,也就没有微任务的存在。在ES5之后,JavaScript引入了Promise,这样,不需要浏览器,JavaScript引擎自身也能够发起异步任务了。
    所以,总结一下,两者区别为:
宏任务(macrotask) 微任务(microtask)
谁发起的 宿主(Node、浏览器) JS引擎
具体事件 1. script (可以理解为外层同步代码)
2. setTimeout/setInterval
3. UI rendering/UI事件
4. postMessage,MessageChannel
5. setImmediate,I/O(Node.js)
1. Promise
2. MutaionObserver
3. Object.observe(已废弃;Proxy 对象替代)
4. process.nextTick(Node.js)
谁先运行 后运行 先运行
会触发新一轮Tick吗 不会

补充:setTimeout会等待script代码中的同步任务执行完毕后才会执行,如果script中有alet语句,则setTimeout的执行会被卡住,直到alert做出相应为止:alert(警示对话框,可以选择阻断当前执行)

拓展 2:setTimeout,setImmediate谁先执行?

setImmediate和process.nextTick为Node环境下常用的方法(IE11支持setImmediate),所以,后续的分析都基于Node宿主。
Node.js是运行在服务端的js,虽然用到也是V8引擎,但由于服务目的和环境不同,导致了它的API与原生JS有些区别,其Event Loop还要处理一些I/O,比如新的网络连接等,所以与浏览器Event Loop不太一样。
执行顺序如下:

  1. timers: 执行setTimeout和setInterval的回调
  2. pending callbacks: 执行延迟到下一个循环迭代的 I/O 回调
  3. idle, prepare: 仅系统内部使用
  4. poll: 检索新的 I/O 事件;执行与 I/O 相关的回调。事实上除了其他几个阶段处理的事情,其他几乎所有的异步都在这个阶段处理。
  5. check: setImmediate在这里执行
  6. close callbacks: 一些关闭的回调函数,如:socket.on(‘close’, …)

一般来说,setImmediate会在setTimeout之前执行,如下:

  1. console.log('outer');
  2. setTimeout(() => {
  3. setTimeout(() => {
  4. console.log('setTimeout');
  5. }, 0);
  6. setImmediate(() => {
  7. console.log('setImmediate');
  8. });
  9. }, 0);

其执行顺序为:

  1. 外层是一个setTimeout,所以执行它的回调的时候已经在timers阶段了
  2. 处理里面的setTimeout,因为本次循环的timers正在执行,所以其回调其实加到了下个timers阶段
  3. 处理里面的setImmediate,将它的回调加入check阶段的队列
  4. 外层timers阶段执行完,进入pending callbacks,idle, prepare,poll,这几个队列都是空的,所以继续往下
  5. 到了check阶段,发现了setImmediate的回调,拿出来执行
  6. 然后是close callbacks,队列是空的,跳过
  7. 又是timers阶段,执行console.log(‘setTimeout’)

但是,如果当前执行环境不是timers阶段,就不一定了。。。。顺便科普一下Node里面对setTimeout的特殊处理:setTimeout(fn, 0)会被强制改为setTimeout(fn, 1)。
看看下面的例子:

  1. setTimeout(() => {
  2. console.log('setTimeout');
  3. }, 0);
  4. setImmediate(() => {
  5. console.log('setImmediate');
  6. });

其执行顺序为:

  1. 遇到setTimeout,虽然设置的是0毫秒触发,但是被node.js强制改为1毫秒,塞入times阶段
  2. 遇到setImmediate塞入check阶段
  3. 同步代码执行完毕,进入Event Loop
  4. 先进入times阶段,检查当前时间过去了1毫秒没有,如果过了1毫秒,满足setTimeout条件,执行回调,如果没过1毫秒,跳过
  5. 跳过空的阶段,进入check阶段,执行setImmediate回调

可见,1毫秒是个关键点,所以在上面的例子中,setImmediate不一定在setTimeout之前执行了。

拓展 3:Promise,process.nextTick谁先执行?

因为process.nextTick为Node环境下的方法,所以后续的分析依旧基于Node。
process.nextTick() 是一个特殊的异步API,其不属于任何的Event Loop阶段。事实上Node在遇到这个API时,Event Loop根本就不会继续进行,会马上停下来执行process.nextTick(),这个执行完后才会继续Event Loop。
所以,nextTick和Promise同时出现时,肯定是nextTick先执行,原因是nextTick的队列比Promise队列优先级更高。

拓展 4:应用场景 - Vue中的vm.$nextTick

vm.$nextTick 接受一个回调函数作为参数,用于将回调延迟到下次DOM更新周期之后执行。
这个API就是基于事件循环实现的。
“下次DOM更新周期”的意思就是下次微任务执行时更新DOM,而vm.$nextTick就是将回调函数添加到微任务中(在特殊情况下会降级为宏任务)。
因为微任务优先级太高,Vue 2.4版本之后,提供了强制使用宏任务的方法。
vm.$nextTick优先使用Promise,创建微任务。
如果不支持Promise或者强制开启宏任务,那么,会按照如下顺序发起宏任务:

  1. 优先检测是否支持原生 setImmediate(这是一个高版本 IE 和 Edge 才支持的特性)
  2. 如果不支持,再去检测是否支持原生的MessageChannel
  3. 如果也不支持的话就会降级为 setTimeout。

小结

下面是道加强版的考题,大家可以试一试。