Vue中$nextTick源碼解析

  在作項目的時候,咱們常常會用到nextTick,簡單的理解就是它就是一個setTimeout函數,將函數放到異步後去處理;將它替換成setTimeout好像也能跑起來,但它僅僅這麼簡單嗎?那爲何咱們不直接用setTimeout呢?讓咱們深刻剖析一下。javascript

發現問題

  記得以前有一個需求,就是根據文字的行數來顯示展開更多的一個按鈕,所以咱們在Vue中給數據賦值以後須要獲取文字高度。html

<div id="app">
    <div class="msg">
        {{msg}}
    </div>
</div>
new Vue({
    el: '#app',
    data: function(){
        return {
            msg: ''
        }
    },
    mounted(){
        this.msg = '我是測試文字'
        console.log(document.querySelector('.msg').offsetHeight) //0
    }
})
複製代碼

  這時無論怎麼獲取,文字的Div高度都是0;可是直接獲取倒是有值:前端

problem.png

  一樣的狀況也發生在給子組件傳參上;咱們給子組件傳參數後,在子組件中調用函數查看參數。java

<div id="app">
    <div class="msg"> <form-report ref="child" :name="childName"></form-report> </div> </div>
Vue.component('form-report', {
    props: ['name'],
    methods: {
        showName(){
            console.log('子組件name:'+this.name)
        }
    },
    template: '<div>{{name}}</div>'
})
new Vue({
    el: '#app',
    data: function(){
        return {
            childName: '',
        }
    },
    mounted(){
        this.childName = '我是子組件名字'
        this.$refs.child.showName()
    }
})
複製代碼

  雖然頁面上展現了子組件的name,可是打印出來倒是空值:面試

problem1.png

異步更新

  咱們發現上述兩個問題的發生,無論子組件仍是父組件,都是在給data中賦值後立馬去查看數據致使的。因爲「查看數據」這個動做是同步操做的,並且都是在賦值以後;所以咱們猜想一下,給數據賦值操做是一個異步操做,並無立刻執行,Vue官網對數據操做是這麼描述的:數組

可能你尚未注意到,Vue 在更新 DOM 時是異步執行的。只要偵聽到數據變化,Vue 將開啓一個隊列,並緩衝在同一事件循環中發生的全部數據變動。若是同一個 watcher 被屢次觸發,只會被推入到隊列中一次。這種在緩衝時去除重複數據對於避免沒必要要的計算和 DOM 操做是很是重要的。而後,在下一個的事件循環「tick」中,Vue 刷新隊列並執行實際 (已去重的) 工做。Vue 在內部對異步隊列嘗試使用原生的 Promise.then、MutationObserver 和 setImmediate,若是執行環境不支持,則會採用 setTimeout(fn, 0) 代替。瀏覽器

  也就是說咱們在設置this.msg = 'some thing'的時候,Vue並無立刻去更新DOM數據,而是將這個操做放進一個隊列中;若是咱們重複執行的話,隊列還會進行去重操做;等待同一事件循環中的全部數據變化完成以後,會將隊列中的事件拿出來處理。閉包

  這樣作主要是爲了提高性能,由於若是在主線程中更新DOM,循環100次就要更新100次DOM;可是若是等事件循環完成以後更新DOM,只須要更新1次。還不瞭解事件循環的童鞋,能夠看個人另外一篇文章從一道面試題來理解JS事件循環app

  爲了在數據更新操做以後操做DOM,咱們能夠在數據變化以後當即使用Vue.nextTick(callback);這樣回調函數會在DOM更新完成後被調用,就能夠拿到最新的DOM元素了。異步

//第一個demo
this.msg = '我是測試文字'
this.$nextTick(()=>{
    //20
    console.log(document.querySelector('.msg').offsetHeight)
})
//第二個demo
this.childName = '我是子組件名字'
this.$nextTick(()=>{
    //子組件name:我是子組件名字
    this.$refs.child.showName()
})
複製代碼

nextTick源碼分析

  瞭解了nextTick的用法和原理以後,咱們就來看一下Vue是怎麼來實現這波「操做」的。

opt.jpg

  Vue把nextTick的源碼單獨抽到一個文件中,/src/core/util/next-tick.js,刪掉註釋也就大概六七十行的樣子,讓咱們逐段來分析。

const callbacks = []
let pending = false
let timerFunc

export function nextTick (cb?: Function, ctx?: Object) {
  let _resolve
  callbacks.push(() => {
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      _resolve(ctx)
    }
  })
  if (!pending) {
    pending = true
    timerFunc()
  }
  // $flow-disable-line
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}
複製代碼

  咱們首先找到nextTick這個函數定義的地方,看看它具體作了什麼操做;看到它在外層定義了三個變量,有一個變量看名字就很熟悉:callbacks,就是咱們上面說的隊列;在nextTick的外層定義變量就造成了一個閉包,因此咱們每次調用$nextTick的過程其實就是在向callbacks新增回調函數的過程。

  callbacks新增回調函數後又執行了timerFunc函數,pending用來標識同一個時間只能執行一次。那麼這個timerFunc函數是作什麼用的呢,咱們繼續來看代碼:

export let isUsingMicroTask = false
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  //判斷1:是否原生支持Promise
  const p = Promise.resolve()
  timerFunc = () => {
    p.then(flushCallbacks)
    if (isIOS) setTimeout(noop)
  }
  isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
  isNative(MutationObserver) ||
  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
  //判斷2:是否原生支持MutationObserver
  let counter = 1
  const observer = new MutationObserver(flushCallbacks)
  const textNode = document.createTextNode(String(counter))
  observer.observe(textNode, {
    characterData: true
  })
  timerFunc = () => {
    counter = (counter + 1) % 2
    textNode.data = String(counter)
  }
  isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  //判斷3:是否原生支持setImmediate
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else {
  //判斷4:上面都不行,直接用setTimeout
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}
複製代碼

  這裏出現了好幾個isNative函數,這是用來判斷所傳參數是否在當前環境原生就支持;例如某些瀏覽器不支持Promise,雖然咱們使用了墊片(polify),可是isNative(Promise)仍是會返回false。

  能夠看出這邊代碼實際上是作了四個判斷,對當前環境進行不斷的降級處理,嘗試使用原生的Promise.thenMutationObserversetImmediate,上述三個都不支持最後使用setTimeout;降級處理的目的都是將flushCallbacks函數放入微任務(判斷1和判斷2)或者宏任務(判斷3和判斷4),等待下一次事件循環時來執行。MutationObserver是Html5的一個新特性,用來監聽目標DOM結構是否改變,也就是代碼中新建的textNode;若是改變了就執行MutationObserver構造函數中的回調函數,不過是它是在微任務中執行的。

  那麼最終咱們順藤摸瓜找到了最終的大boss:flushCallbacks;nextTick不顧一切的要把它放入微任務或者宏任務中去執行,它到底是何方神聖呢?讓咱們來一睹它的真容:

function flushCallbacks () {
  pending = false
  const copies = callbacks.slice(0)
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}
複製代碼

  原本覺得有多複雜的flushCallbacks,竟然不太短短的8行。它所作的事情也很是的簡單,把callbacks數組複製一份,而後把callbacks置爲空,最後把複製出來的數組中的每一個函數依次執行一遍;因此它的做用僅僅是用來執行callbacks中的回調函數。

總結

  到這裏,總體nextTick的代碼都分析完畢了,總結一下它的流程就是:

  1. 把回調函數放入callbacks等待執行
  2. 將執行函數放到微任務或者宏任務中
  3. 事件循環到了微任務或者宏任務,執行函數依次執行callbacks中的回調

  再回到咱們開頭說的setTimeout,能夠看出來nextTick是對setTimeout進行了多種兼容性的處理,寬泛的也能夠理解爲將回調函數放入setTimeout中執行;不過nextTick優先放入微任務執行,而setTimeout是宏任務,所以nextTick通常狀況下老是先於setTimeout執行,咱們能夠在瀏覽器中嘗試一下:

setTimeout(()=>{
    console.log(1)
}, 0)
this.$nextTick(()=>{
    console.log(2)
})
this.$nextTick(()=>{
    console.log(3)
})
//運行結果 2 3 1
複製代碼

  最後驗證猜測,當前宏任務執行完成後,優先執行兩個微任務,最後再執行宏任務。

更多前端資料請關注公衆號【前端壹讀】

若是以爲寫得還不錯,請關注個人掘金主頁。更多文章請訪問謝小飛的博客

相關文章
相關標籤/搜索