1. 程式人生 > >VUE的nextTick

VUE的nextTick

一、定義[nextTick、事件迴圈]

  nextTick的由來:

    由於VUE的資料驅動檢視更新,是非同步的,即修改資料的當下,檢視不會立刻更新,而是等同一事件迴圈中的所有資料變化完成之後,再統一進行檢視更新。

  nextTick的觸發時機:

    在同一事件迴圈中的資料變化後,DOM完成更新,立即執行nextTick(callback)內的回撥。

  應用場景:

    需要在檢視更新之後,基於新的檢視進行操作。

  以上出現了事件迴圈的概念,其涉及到JS的執行機制,包括主執行緒的執行棧、非同步佇列、非同步API、事件迴圈的協作,此處不展開之後再總結。大致理解:主執行緒完成同步環境執行,查詢任務佇列,提取隊首的任務,放入主執行緒中執行;執行完畢,再重複該操作,該過程稱為事件迴圈。而主執行緒的每次讀取任務佇列操作,是一個事件迴圈的開始。非同步callback不可能處在同一事件迴圈中。

  簡單總結事件迴圈:

    同步程式碼執行 -> 查詢非同步佇列,推入執行棧,執行callback1[事件迴圈1] ->查詢非同步佇列,推入執行棧,執行callback2[事件迴圈2]...

  即每個非同步callback,最終都會形成自己獨立的一個事件迴圈。

  結合nextTick的由來,可以推出每個事件迴圈中,nextTick觸發的時機:

    同一事件迴圈中的程式碼執行完畢 -> DOM 更新 -> nextTick callback觸發

   tips:本文的任務佇列、訊息佇列、非同步佇列指同一個東西,均指macrotask queue。

二、例項理解nextTick的使用,並給出在頁面渲染上的優化巧用

  (tips:程式碼的正確閱讀方式:看template組成、跳過script程式碼、看程式碼後面的用例設計、看之後的程式碼分析、同時結合回頭結合script程式碼理解)

複製程式碼

<template>
    <div>
        <ul>
            <li v-for="item in list1">{{item}}</li>
        </ul>
        <ul>
            <li v-for="item in list2">{{item}}</li>
        </ul>
        <ol>
            <li v-for="item in list3">{{item}}</li>
        </ol>
        <ol>
            <li v-for="item in list4">{{item}}</li>
        </ol>
        <ol>
            <li v-for="item in list5">{{item}}</li>
        </ol>
    </div>
</template>
<script type="text/javascript">
export default {
    data() {
        return {
            list1: [],
            list2: [],
            list3: [],
            list4: [],
            list5: []
        }
    },
    created() {
        this.composeList12()
        this.composeList34()
        this.composeList5()
        this.$nextTick(function() {
            // DOM 更新了
            console.log('finished test ' + new Date().toString())
            console.log(document.querySelectorAll('li').length)
        })
    },
    methods: {
        composeList12() {
            let me = this
            let count = 10000

            for (let i = 0; i < count; i++) {
                Vue.set(me.list1, i, 'I am a 測試資訊~~啦啦啦' + i)
            }
            console.log('finished list1 ' + new Date().toString())

            for (let i = 0; i < count; i++) {
                Vue.set(me.list2, i, 'I am a 測試資訊~~啦啦啦' + i)
            }
            console.log('finished list2 ' + new Date().toString())

            this.$nextTick(function() {
                // DOM 更新了
                console.log('finished tick1&2 ' + new Date().toString())
                console.log(document.querySelectorAll('li').length)
            })
        },
        composeList34() {
            let me = this
            let count = 10000

            for (let i = 0; i < count; i++) {
                Vue.set(me.list3, i, 'I am a 測試資訊~~啦啦啦' + i)
            }
            console.log('finished list3 ' + new Date().toString())

            this.$nextTick(function() {
                // DOM 更新了
                console.log('finished tick3 ' + new Date().toString())
                console.log(document.querySelectorAll('li').length)
            })

            setTimeout(me.setTimeout1, 0)
        },
        setTimeout1() {
            let me = this
            let count = 10000

            for (let i = 0; i < count; i++) {
                Vue.set(me.list4, i, 'I am a 測試資訊~~啦啦啦' + i)
            }
            console.log('finished list4 ' + new Date().toString())

            me.$nextTick(function() {
                // DOM 更新了
                console.log('finished tick4 ' + new Date().toString())
                console.log(document.querySelectorAll('li').length)
            })
        },
        composeList5() {
            let me = this
            let count = 10000

            this.$nextTick(function() {
                // DOM 更新了
                console.log('finished tick5-1 ' + new Date().toString())
                console.log(document.querySelectorAll('li').length)
            })

            setTimeout(me.setTimeout2, 0)
        },
        setTimeout2() {
            let me = this
            let count = 10000

            for (let i = 0; i < count; i++) {
                Vue.set(me.list5, i, 'I am a 測試資訊~~啦啦啦' + i)
            }
            console.log('finished list5 ' + new Date().toString())

            me.$nextTick(function() {
                // DOM 更新了
                console.log('finished tick5 ' + new Date().toString())
                console.log(document.querySelectorAll('li').length)
            })
        }
    }
}
</script>

複製程式碼

  2.1、用例設計

    用例1:通過list1、2、3驗證,處在同步程式碼中的DOM更新情況及nextTick的觸發時機;

    用例2:通過list3、list4驗證,同步程式碼及非同步程式碼中Dom更新及nextTick觸發的區別;

    用例3:通過list4、list5對比驗證,多個非同步程式碼中nextTick觸發的區別;

    用例4:通過在檢視更新後獲取DOM中<li>的數量,判斷nextTick序列渲染的時間點。

  2.2、程式碼分析

    函式執行步驟:

      事件迴圈1:

        step1: this.composeList12() -> update list1, update list2 -> 繫結tick’1&2’

        step2: this.composeList34() -> update list3, 設定非同步1setTimeout1 -> 繫結tick’3’

        step3: this.composeList5() -> 繫結tick’5-1’ -> 設定非同步2setTimeout2 

        step4: 繫結tick’test’

      事件迴圈2:

        將setTimeout1的callback推入執行棧 -> update list4 -> 繫結tick’4’

      事件迴圈3:

        將setTimeout2的callback推入執行棧 -> update list5 -> 繫結tick’5’

  2.3、推斷輸出訊息

    由於同一事件迴圈中的tick按執行順序,因此訊息輸出為即:

      [同步環境]update list1 -> update list2 -> update list3 -> tick‘1&2’ -> tick‘3’ -> tick’5-1’ -> tick’test'

      [事件迴圈1]->update list4 -> tick’4’ 

       [事件迴圈2] ->update list5 -> tick’5’

  2.4、實際執行結果如下圖

           

    該demo中,設定了5個size為10000的陣列,從而能從時間及訊息輸出兩個維度來了解nextTick的執行情況。另外,額外增加了一個引數,即更新後的檢視中<li>的數量,從這個數量,可以考察出同一事件迴圈中的nextTick執行情況。由執行結果圖可以看出實際的輸出與推導的輸出結果相符合。

  2.5、總結

    從用例1得出:

      a、在同一事件迴圈中,只有所有的資料更新完畢,才會呼叫nextTick;

      b、僅在同步執行環境資料完全更新完畢,DOM才開始渲染,頁面才開始展現;

      c、在同一事件迴圈中,如果存在多個nextTick,將會按最初的執行順序進行呼叫;

    從用例1+用例4得出:

      d、從同步執行環境中的四個tick對應的‘li’數量均為30000可看出,同一事件迴圈中,nextTick所在的檢視是相同的;

    從用例2得出:

      e、只有同步環境執行完畢,DOM渲染完畢之後,才會處理非同步callback

    從用例3得出:

      f、每個非同步callback最後都會處在一個獨立的事件迴圈中,對應自己獨立的nextTick;

    從用例1結論中可得出:

      g、這個事件環境中的資料變化完成,在進行渲染[檢視更新],可以避免DOM的頻繁變動,從而避免了因此帶來的瀏覽器卡頓,大幅度提升效能;

    從b可以得出:

      h、在首屏渲染、使用者互動過程中,要巧用同步環境及非同步環境;首屏展現的內容,儘量保證在同步環境中完成;其他內容,拆分到非同步中,從而保證效能、體驗。

  tips:

    1、可產生非同步callback的有:promise(microtask queue)、setTimeout、MutationObserver、DOM事件、Ajax等;

    2、 vue DOM的檢視更新實現,,使用到了ES6的Promise及HTML5的MutationObserver,當環境不支援時,使用setTimeout(fn, 0)替代。上述的三種方法,均為非同步API。其中MutationObserver類似事件,又有所區別;事件是同步觸發,其為非同步觸發,即DOM發生變化之後,不會立刻觸發,等當前所有的DOM操作都結束後觸發。關於非同步API、事件迴圈將在以後補充。