Event Loop是个什么玩意:从 Vue 的 nextTick 说起

9,438 阅读4分钟

熟悉 Vue 的同学们都知道,Vue 有个 nextTick 方法,用来异步更新数据。

来看看这个栗子:

<body>
    <div id="main">
        <ul class="list">
            <li class="item" v-for="item in list">{{ item }}</li>
        </ul>
    </div>
    
    <script>
        new Vue({
            el: '#main',
            data: {
                list: [
                    'AAAAAAAAAA',
                    'BBBBBBBBBB',
                    'CCCCCCCCCC'
                ]
            },
            mounted: function () {
                this.list.push('DDDDD')
            }
        })
    </script>
</body>

随便给了点样式之后,页面是这样的:

看起来似乎一切正常,我们在给数组添加了一条数据之后,页面也确实对应的更新了。可是,当我们在打印这个 ul 元素里 li 的 length 时,问题出现了:

    mounted: function () {
        this.list.push('DDDDD')
        console.log(this.$el.querySelectorAll('.item').length)  // 3
    }

这时候如果我们有需求需要通过 li 的个数来计算出 ul 容器的高度来进行布局,显然就有问题了。而这时候 Vue 的 nextTick 就可以帮助我们解决这个问题:

    mounted: function () {
        this.list.push('DDDDD')
        Vue.nextTick(function() {
            console.log(this.$el.querySelectorAll('.item').length)  // 4
            // ... 计算
        })

关于 Vue 的异步更新队列,官网是这么说的: 当你设置 vm.someData = 'new value' ,该组件不会立即重新渲染。当刷新队列时,组件会在事件循环队列清空时的下一个“tick”更新。多数情况我们不需要关心这个过程,但是如果你想在 DOM 状态更新后做点什么,这就可能会有些棘手。虽然 Vue.js 通常鼓励开发人员沿着“数据驱动”的方式思考,避免直接接触 DOM,但是有时我们确实要这么做。为了在数据变化之后等待 Vue 完成更新 DOM ,可以在数据变化之后立即使用 Vue.nextTick(callback) 。这样回调函数在 DOM 更新完成后就会调用。

简单说,因为 DOM 至少会在当前线程里面的代码全部执行完毕再更新。所以不可能做到在修改数据后并且 DOM 更新后再执行,要保证在 DOM 更新以后再执行某一块代码,就必须把这块代码放到下一次事件循环里面,比如 setTimeout(fn, 0),这样 DOM 更新后,就会立即执行这块代码。

划重点: 队列、事件循环

js 是单线程语言

我们都知道,js 执行的所有任务都需要排队,一个任务必须要等它前面的一个任务执行完之后才能执行。如果前一个任务需要花费大量的时间来计算,那么后一个任务就必须一直等它执行完才会轮到它执行,这就是单线程的特性。 而 js 的任务分为两种,同步任务和异步任务:

  • 同步任务就是按照顺序一个一个的执行任务,后一个任务要执行必须等它前一个任务完成
  • 异步任务(比如回调)不会占用主线程,会被塞到一个任务队列,等主线程的任务执行完毕,就会把这个异步任务队列里的任务放回主线程依次执行

用一个丑但易懂的图来表示:

所以结果输出是这样就很好理解了:

Event Loop(事件循环)

被称作事件循环的原因在于,同步的任务可能会生成新的任务,因此它一直在不停的查找新的事件并执行。一次循环的执行称之为 tick,在这个循环里执行的代码被称作 task,而整个过程是不断重复的。

console.log(1);

setTimeout(()=>{
  console.log(2);
},1000);

while (true){}

上面代码在输出 1 之后(谨慎使用!我的浏览器就被卡死了~),定时器被塞到任务队列里,然后主线程继续往下执行,碰到一个死循环,导致任务队列里的任务永远不会被执行,因此不会输出 2

事件队列

除了我们的主线程之外,任务队列分为 microtaskmacrotask,通常我们会称之为微任务和宏任务。 microtask 这一名词在js中是个比较新的概念,我们通常是在学习 ES6 的 Promise 时才初次接触到。

  • 执行优先级上,主线程任务 > microtask > macrotask。
  • 典型的 macrotask 有 setTimeout 和 setInterval,以及只有 IE 支持的 setImmediate,还有 MessageChannel等,ES6的 Promise 则是属于 microtask

console.log(1)

setTimeout(function(){
	console.log(2)
})

Promise.resolve().then(function(){
	console.log('promise1')
}).then(function(){
	console.log('promise2')
})

console.log(4)

根据执行顺序,上面代码的输出结果很容易就能得出了:

nextTick

让我们回到上面的主题,Vue 的 nextTick方法, 从 源码 不难发现,Vue 在内部尝试对异步队列使用原生的setImmediate Promise.thenMessageChannel,如果当前执行环境不支持,就采用setTimeout(fn, 0)代替。

Nodejs

node原生就支持 process.nextTick(fn)setImmediate(fn)方法,并且process.nextTick(fn)会被当做microtask顺序执行。