Event Loop是個什麼玩意:從 Vue 的 nextTick 說起

blackandgray發表於2018-05-24

熟悉 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>
複製程式碼

隨便給了點樣式之後,頁面是這樣的:

Event Loop是個什麼玩意:從 Vue 的 nextTick 說起
看起來似乎一切正常,我們在給陣列新增了一條資料之後,頁面也確實對應的更新了。可是,當我們在列印這個 ul 元素裡 li 的 length 時,問題出現了:

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

Event Loop是個什麼玩意:從 Vue 的 nextTick 說起
這時候如果我們有需求需要通過 li 的個數來計算出 ul 容器的高度來進行佈局,顯然就有問題了。而這時候 Vue 的 nextTick 就可以幫助我們解決這個問題:

    mounted: function () {
        this.list.push('DDDDD')
        Vue.nextTick(function() {
            console.log(this.$el.querySelectorAll('.item').length)  // 4
            // ... 計算
        })
複製程式碼

Event Loop是個什麼玩意:從 Vue 的 nextTick 說起
關於 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是個什麼玩意:從 Vue 的 nextTick 說起
所以結果輸出是這樣就很好理解了:

Event Loop是個什麼玩意:從 Vue 的 nextTick 說起

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)

複製程式碼

根據執行順序,上面程式碼的輸出結果很容易就能得出了:

Event Loop是個什麼玩意:從 Vue 的 nextTick 說起

nextTick

讓我們回到上面的主題,Vue 的 nextTick方法, 從 原始碼 不難發現,Vue 在內部嘗試對非同步佇列使用原生的setImmediate Promise.thenMessageChannel,如果當前執行環境不支援,就採用setTimeout(fn, 0)代替。

Nodejs

node原生就支援 process.nextTick(fn)setImmediate(fn)方法,並且process.nextTick(fn)會被當做microtask順序執行。

相關文章