前言
大家都知道,Javascript是单线程、顺序执行的,通过事件循环来处理异步。而且稍有开发经验的同学也知道,利用setTimeout
、setInterval
以及Promise
可以延时代码的执行。如果在Node.js中,大家会用process.nextTick
来让代码在下一个周期执行;或者在Vue中,会利用Vue.nextTick
保证DOM全部更新完毕后再执行回调函数。但是,如果他们都放在一起呢?执行顺序又会是怎么样的?
来个样例
聪明的你知道下面代码的执行顺序么?
1 | console.log('script start') |
看到这个问题,有经验的同学会脱口而出:太简单了,会输出如下内容:
1 | script start |
因为setTimeout
会加入到队列,延时执行。的确没错。那我们再看看下面的例子呢?
1 | console.log('script start') |
呃,这个问题好像难住了一部分同学,因为他们会有这样的想法:
setTimeout
和Promise
到底谁先执行呢?听说Promise
是异步的,但是setTimeout
也是异步的,而且延时为0, 这可怎么好?
想不明白?没关系,先执行下看看结果:
1 |
|
结果是不是蛮有意思的?为啥Promise
会先执行呢?我尝试着解释下,如果解释的不对,希望各位大牛多多指导。
大家知道,Javascript是基于事件循环(event loop)来处理事件的,用户的一些操作会放到事件队列里面,Javascript引擎会在合适的时候执行队列里面的操作。注意我们这里用到了“合适的时候“这个限定词,是因为Javascript单线程的,如果某段Javascript执行时间过长,那么它会阻塞主线程的执行。所以setTimeout
也并不说是一定会精确的执行。
在Javascript引擎里面,队列还分为Task
队列(也有人叫做MacroTask
)和MicroTask
队列,MicroTask
会优先于Task
执行。比如常见的点击事件、setImmediate
、setTimeout
、MessageChannel
等会放入Task
队列,但是Promise
以及MutationObserver
会放到Microtask
队列。同时,Javascript引擎在执行Microtask
队列的时候,如果期间又加入了新的Microtask
,则该Microtask
会加入到之前的Microtask
队列的尾部,保证Microtask
先于Task
队列执行。
这样,大家就清楚了为啥Promise
先执行吧,因为它是一个Microtask
呀!优先级高,真是没办法 :-)。大家也许会问,优先级高,会高到什么程度呢?我们可以简单量度下:
1 | const checkDuration = () => { |
我在Chrome的console里面执行多次,会输出:
1 | setTimeout耗时: 1 |
1 | setTimeout耗时: 4 |
当然,如果这个结果不是固定的,测试多次, setTimeout
执行大慨在4ms左右,Promise
大慨在1ms左右。哈哈,其实就快了3ms,前端同学为了争取这3ms真是不懈努力而且煞费苦心呀,不过真的为他们爱专研的态度点赞!!!
值得说明的是, Vue中Vue.nextTick
也利用了该原理来保证在下次DOM更新循环结束之后执行延迟回调。如Vue 2.5.2里面就有这样的代码逻辑:
1 |
|
在Vue,用MacroTask
就是我们上文说的Task
。可见执行的时机是:
Task(MacroTask)
队列中: setImmediate
> MessageChannel
> setTimeout
MicroTask
队列中: 直接用了Promise
,新版本中弃用了MutationObserver
,因为其兼容性不好
扯了这么多,大家应该知道原因了吧?
再来个样例
为了巩固大家对MicroTask
的列举,我们再看一个例子
1 | <div class="outer"> |
1 | // 获取DOM |
如果我们点击 inneer区域, 输出内容为什么呢? 如果你理解了上文的内容,就会知道输出结果为:
1 | click |
好,今天就分享在这里。下篇文章,我们聊聊Node.js里面的事件。比如上文我们还没提到setImmediate
呢?这个东西只在IE里面支持,但是在Node.js里面是支持的,而且Node.js里面还有一个Process.nextTick
。下次我们再聊聊。