天天看點

html5怎麼改為vue_Vue.nextTick 的原理和用途

html5怎麼改為vue_Vue.nextTick 的原理和用途
一、原理

1.異步說明

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

2.事件循環說明

簡單來說,Vue在修改資料後,視圖不會立刻更新,而是等

同一事件循環

中的所有資料變化完成之後,再統一進行視圖更新。

html5怎麼改為vue_Vue.nextTick 的原理和用途

二、Vue.nextTick 的機制

1、為什麼用Vue.nextTick()

首先來了解一下JS的運作機制。

JS運作機制(Event Loop)

JS執行是單線程的,它是基于事件循環的。
  1. 所有同步任務都在主線程上執行,形成一個執行棧。
  2. 主線程之外,會存在一個任務隊列,隻要異步任務有了結果,就在任務隊列中放置一個事件。
  3. 當執行棧中的所有同步任務執行完後,就會讀取任務隊列。那些對應的異步任務,會結束等待狀态,進入執行棧。
  4. 主線程不斷重複第三步。

這裡主線程的執行過程就是一個

tick

,而所有的異步結果都是通過任務隊列來排程。

Event Loop

分為宏任務和微任務,無論是執行宏任務還是微任務,完成後都會進入到一下

tick

并在兩個

tick

之間進行UI渲染

由于Vue DOM更新是異步執行的,即修改資料時,視圖不會立即更新,而是會監聽資料變化,并緩存在同一事件循環中,等同一資料循環中的所有資料變化完成之後,再統一進行視圖更新。為了確定得到更新後的DOM,是以設定了

Vue.nextTick()

方法。

2、什麼是Vue.nextTick()

是Vue的核心方法之一,官方文檔解釋如下:

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

MutationObserver

先簡單介紹下

MutationObserver

:MO是HTML5中的API,是一個用于監視DOM變動的接口,它可以監聽一個DOM對象上發生的子節點删除、屬性修改、文本内容修改等。

調用過程是要先給它綁定回調,得到MO執行個體,這個回調會在MO執行個體監聽到變動時觸發。這裡MO的回調是放在

microtask

中執行的。

// 建立MO執行個體
const observer = new MutationObserver(callback)

const textNode = '想要監聽的Don節點'

observer.observe(textNode, {
    characterData: true // 說明監聽文本内容的修改
})
           

源碼淺析

nextTick

的實作單獨有一個JS檔案來維護它,在

src/core/util/next-tick.js

中。

nextTick

源碼主要分為兩塊:能力檢測和根據能力檢測以不同方式執行回調隊列。

能力檢測

由于宏任務耗費的時間是大于微任務的,是以在浏覽器支援的情況下,優先使用微任務。如果浏覽器不支援微任務,再使用宏任務。

// 空函數,可用作函數占位符
import { noop } from 'shared/util' 

 // 錯誤處理函數
import { handleError } from './error'

 // 是否是IE、IOS、内置函數
import { isIE, isIOS, isNative } from './env'

// 使用 MicroTask 的辨別符,這裡是因為火狐在<=53時 無法觸發微任務,在modules/events.js檔案中引用進行安全排除
export let isUsingMicroTask = false 

 // 用來存儲所有需要執行的回調函數
const callbacks = []

// 用來标志是否正在執行回調函數
let pending = false 

// 對callbacks進行周遊,然後執行相應的回調函數
function flushCallbacks () {
    pending = false
    // 這裡拷貝的原因是:
    // 有的cb 執行過程中又會往callbacks中加入内容
    // 比如 $nextTick的回調函數裡還有$nextTick
    // 後者的應該放到下一輪的nextTick 中執行
    // 是以拷貝一份目前的,周遊執行完目前的即可,避免無休止的執行下去
    const copies = callbcks.slice(0)
    callbacks.length = 0
    for(let i = 0; i < copies.length; i++) {
        copies[i]()
    }
}

let timerFunc // 異步執行函數 用于異步延遲調用 flushCallbacks 函數

// 在2.5中,我們使用(宏)任務(與微任務結合使用)。
// 然而,當狀态在重新繪制之前發生變化時,就會出現一些微妙的問題
// (例如#6813,out-in轉換)。
// 同樣,在事件處理程式中使用(宏)任務會導緻一些奇怪的行為
// 是以,我們現在再次在任何地方使用微任務。
// 優先使用 Promise
if(typeof Promise !== 'undefined' && isNative(Promise)) {
    const p = Promise.resolve()
    timerFunc = () => {
        p.then(flushCallbacks)
        
        // IOS 的UIWebView, Promise.then 回調被推入 microTask 隊列,但是隊列可能不會如期執行
        // 是以,添加一個空計時器強制執行 microTask
        if(isIOS) setTimeout(noop)
    }
    isUsingMicroTask = true
} else if(!isIE && typeof MutationObserver !== 'undefined' && (isNative(MutationObserver) || MutationObserver.toString === '[object MutationObserverConstructor]')) {
    // 當 原生Promise 不可用時,使用 原生MutationObserver
    // e.g. PhantomJS, iOS7, Android 4.4
 
    let counter = 1
    // 建立MO執行個體,監聽到DOM變動後會執行回調flushCallbacks
    const observer = new MutationObserver(flushCallbacks)
    const textNode = document.createTextNode(String(counter))
    observer.observe(textNode, {
        characterData: true // 設定true 表示觀察目标的改變
    })
    
    // 每次執行timerFunc 都會讓文本節點的内容在 0/1之間切換
    // 切換之後将新值複制到 MO 觀測的文本節點上
    // 節點内容變化會觸發回調
    timerFunc = () => {
        counter = (counter + 1) % 2
        textNode.data = String(counter) // 觸發回調
    }
    isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
    timerFunc = () => {
        setImmediate(flushCallbacks)
    }
} else {
    timerFunc = () => {
        setTimeout(flushCallbacks, 0)
    }
}
           

延遲調用優先級如下:

Promise > MutationObserver > setImmediate > setTimeout

export function nextTick(cb? Function, ctx: Object) {
    let _resolve
    // cb 回調函數會統一處理壓入callbacks數組
    callbacks.push(() => {
        if(cb) {
            try {
                cb.call(ctx)
            } catch(e) {
                handleError(e, ctx, 'nextTick')
            }
        } else if (_resolve) {
            _resolve(ctx)
        }
    })
    
    // pending 為false 說明本輪事件循環中沒有執行過timerFunc()
    if(!pending) {
        pending = true
        timerFunc()
    }
    
    // 當不傳入 cb 參數時,提供一個promise化的調用 
    // 如nextTick().then(() => {})
    // 當_resolve執行時,就會跳轉到then邏輯中
    if(!cb && typeof Promise !== 'undefined') {
        return new Promise(resolve => {
            _resolve = resolve
        })
    }
}
           

next-tick.js

對外暴露了

nextTick

這一個參數,是以每次調用

Vue.nextTick

時會執行:

  • 把傳入的回調函數

    cb

    壓入

    callbacks

    數組
  • 執行

    timerFunc

    函數,延遲調用

    flushCallbacks

    函數
  • 周遊執行

    callbacks

    數組中的所有函數

這裡的

callbacks

沒有直接在

nextTick

中執行回調函數的原因是保證在同一個

tick

内多次執行

nextTick

,不會開啟多個異步任務,而是把這些異步任務都壓成一個同步任務,在下一個

tick

執行完畢。

附加

noop

的定義如下

/**
 * Perform no operation.
 * Stubbing args to make Flow happy without leaving useless transpiled code
 * with ...rest (https://flow.org/blog/2017/05/07/Strict-Function-Call-Arity/).
 */
export function noop (a?: any, b?: any, c?: any) {}
           

3、怎麼用

文法

Vue.nextTick([callback, context])

參數

  • {Function} [callback]

    :回調函數,不傳時提供promise調用
  • {Object} [context]

    :回調函數執行的上下文環境,不傳預設是自動綁定到調用它的執行個體上。
//改變資料
vm.message = 'changed'

//想要立即使用更新後的DOM。這樣不行,因為設定message後DOM還沒有更新
console.log(vm.$el.textContent) // 并不會得到'changed'

//這樣可以,nextTick裡面的代碼會在DOM更新後執行
Vue.nextTick(function(){
    // DOM 更新了
    //可以得到'changed'
    console.log(vm.$el.textContent)
})

// 作為一個 Promise 使用 即不傳回調
Vue.nextTick()
  .then(function () {
    // DOM 更新了
  })
           

Vue執行個體方法

vm.$nextTick

做了進一步封裝,把context參數設定成目前Vue執行個體。

4、小結

使用

Vue.nextTick()

是為了可以擷取更新後的DOM 。

觸發時機:在同一事件循環中的資料變化後,DOM完成更新,立即執行

Vue.nextTick()

的回調。

同一事件循環中的代碼執行完畢 -> DOM 更新 -> nextTick callback觸發
html5怎麼改為vue_Vue.nextTick 的原理和用途

應用場景:

  • 在Vue生命周期的

    created()

    鈎子函數進行的DOM操作一定要放在

    Vue.nextTick()

    的回調函數中。 原因 :是

    created()

    鈎子函數執行時DOM其實并未進行渲染。
  • 在資料變化後要執行的某個操作,而這個操作需要使用随資料改變而改變的DOM結構的時候,這個操作應該放在

    Vue.nextTick()

    的回調函數中。 原因 :Vue異步執行DOM更新,隻要觀察到資料變化,Vue将開啟一個隊列,并緩沖在同一事件循環中發生的所有資料改變,如果同一個watcher被多次觸發,隻會被推入到隊列中一次。

版本分析

2.6 版本優先使用 microtask 作為異步延遲包裝器,且寫法相對簡單。而2.5 版本中,nextTick 的實作是 microTimerFunc、macroTimerFunc 組合實作的,延遲調用優先級是:Promise > setImmediate > MessageChannel > setTimeout,具體見源碼。

2.5 版本在重繪之前狀态改變時會有小問題(如 #6813)。此外,在事件處理程式中使用 macrotask 會導緻一些無法規避的奇怪行為(如 #7109,#7153等)。

microtask 在某些情況下也是會有問題的,因為 microtask 優先級比較高,事件會在順序事件(如#4521,#6690 有變通方法)之間甚至在同一事件的冒泡過程中觸發(#6566)。

繼續閱讀