1. 程式人生 > >【轉載】Vue.nextTick 的原理和用途

【轉載】Vue.nextTick 的原理和用途

 

對於 Vue.nextTick 方法,自己有些疑惑。在查詢了各種資料後,總結了一下其原理和用途,如有錯誤,請不吝賜教。

概覽

官方文件說明:
  • 用法:

在下次 DOM 更新迴圈結束之後執行延遲迴調。在修改資料之後立即使用這個方法,獲取更新後的 DOM。

疑問:
  1. DOM 更新迴圈是指什麼?
  2. 下次更新迴圈是什麼時候?
  3. 修改資料之後使用,是加快了資料更新進度嗎?
  4. 在什麼情況下要用到?

原理

非同步說明

Vue 實現響應式並 不是資料發生變化之後 DOM 立即變化,而是按一定的策略進行 DOM 的更新。

在 Vue 的文件中,說明 Vue 是非同步執行 DOM 更新的。關於非同步的解析,可以檢視阮一峰老師的這篇文章。擷取關鍵部分如下:

具體來說,非同步執行的執行機制如下。

(1)所有同步任務都在主執行緒上執行,形成一個執行棧(execution context stack)。
(2)主執行緒之外,還存在一個"任務佇列"(task queue)。只要非同步任務有了執行結果,就在"任務佇列"之中放置一個事件。
(3)一旦"執行棧"中的所有同步任務執行完畢,系統就會讀取"任務佇列",看看裡面有哪些事件。那些對應的非同步任務,於是結束等待狀態,進入執行棧,開始執行。
(4)主執行緒不斷重複上面的第三步。

下圖就是主執行緒和任務佇列的示意圖。

 

事件迴圈說明

簡單來說,Vue 在修改資料後,檢視不會立刻更新,而是等同一事件迴圈中的所有資料變化完成之後,再統一進行檢視更新。

知乎上的例子:

//改變資料
vm.message = 'changed'

//想要立即使用更新後的DOM。這樣不行,因為設定message後DOM還沒有更新
console.log(vm.$el.textContent) // 並不會得到'changed' //這樣可以,nextTick裡面的程式碼會在DOM更新後執行 Vue.nextTick(function(){ console.log(vm.$el.textContent) //可以得到'changed' }) 

圖解:

 

事件迴圈:

第一個 tick(圖例中第一個步驟,即'本次更新迴圈'):

  1. 首先修改資料,這是同步任務。同一事件迴圈的所有的同步任務都在主執行緒上執行,形成一個執行棧,此時還未涉及 DOM 。
  2. Vue 開啟一個非同步佇列,並緩衝在此事件迴圈中發生的所有資料改變。如果同一個 watcher 被多次觸發,只會被推入到佇列中一次。

第二個 tick(圖例中第二個步驟,即'下次更新迴圈'):

同步任務執行完畢,開始執行非同步 watcher 佇列的任務,更新 DOM 。Vue 在內部嘗試對非同步佇列使用原生的 Promise.then 和 MessageChannel 方法,如果執行環境不支援,會採用 setTimeout(fn, 0) 代替。

第三個 tick(圖例中第三個步驟):

此時就是文件所說的

下次 DOM 更新迴圈結束之後

此時通過 Vue.nextTick 獲取到改變後的 DOM 。通過 setTimeout(fn, 0) 也可以同樣獲取到。


簡單總結事件迴圈:

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

總之,非同步是單獨的一個tick,不會和同步在一個 tick 裡發生,也是 DOM 不會馬上改變的原因。

對於事件迴圈,可以在這裡檢視更詳細的內容:  https://segmentfault.com/a/11...

用途

應用場景:需要在檢視更新之後,基於新的檢視進行操作。

created、mounted

需要注意的是,在 created 和 mounted 階段,如果需要操作渲染後的試圖,也要使用 nextTick 方法。

官方文件說明:

注意 mounted 不會承諾所有的子元件也都一起被掛載。如果你希望等到整個檢視都渲染完畢,可以用 vm.$nextTick 替換掉 mounted
mounted: function () {
  this.$nextTick(function () {
    // Code that will run only after the
    // entire view has been rendered
  })
}

 

其他應用場景

其他應用場景如下三例:

例子1:

點選按鈕顯示原本以 v-show = false 隱藏起來的輸入框,並獲取焦點。

showsou(){
  this.showit = true //修改 v-show
  document.getElementById("keywords").focus()  //在第一個 tick 裡,獲取不到輸入框,自然也獲取不到焦點
}

 

修改為:

showsou(){
  this.showit = true
  this.$nextTick(function () {
    // DOM 更新了
    document.getElementById("keywords").focus()
  })
}

 

例子2:

點選獲取元素寬度。

<div id="app">
    <p ref="myWidth" v-if="showMe">{{ message }}</p>
    <button @click="getMyWidth">獲取p元素寬度</button>
</div>

getMyWidth() {
    this.showMe = true;
    //this.message = this.$refs.myWidth.offsetWidth;
    //報錯 TypeError: this.$refs.myWidth is undefined
    this.$nextTick(()=>{
        //dom元素更新後執行,此時能拿到p元素的屬性
        this.message = this.$refs.myWidth.offsetWidth;
  })
}

 

例子3:

使用 swiper 外掛通過 ajax 請求圖片後的滑動問題。

例項理解 nextTick 應用

下面的例子來自 https://www.cnblogs.com/hity-..., 稍有改動。各位可以複製執行一遍,加深理解。

<template>
    <div>
        <ul>
            <li class="example" v-for="item in list1">{{item}}</li>
        </ul>
        <ul>
            <li class="example" v-for="item in list2">{{item}}</li>
        </ul>
        <ol>
            <li class="example" v-for="item in list3">{{item}}</li>
        </ol>
        <ol>
            <li class="example" v-for="item in list4">{{item}}</li>
        </ol>
        <ol>
            <li class="example" 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(),document.querySelectorAll('.example').length)
        })
    },
    methods: {
        composeList12() {
            let me = this
            let count = 10000

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

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

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

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

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

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

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

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

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

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

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

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

 

 

結果:

 

 

參考文章

vue nextTick深入理解-vue效能優化、DOM更新時機、事件迴圈機制;
JavaScript 執行機制詳解:再談Event Loop
知乎:vue.js$nextTick的一個問題
JS 事件迴圈機制 - 任務佇列、web API、JS主執行緒的相互協同