Vue源碼解讀之組件爲何採用異步渲染 ( nextTick的實現原理 )

前言

有人說:是爲了提高性能,對,根本上也是這麼個道理 ;那到底是如何做的呢 ?

其實在vue中,響應式數據是組件級的,也就是說,每一次的更新都是渲染整個組件,如果是同步的話,根據我們前邊理解的響應式數據原理,一旦修改了data屬性,便會觸發對應的 watcher,然後調用對應 watcher 下的 update 方法更新視圖,那麼結果顯而易見,太頻繁了 !如下代碼:

// 省略多餘模板語法
data () {
	a:1,
	b:2,
	c:3
}
//如果我們按照同步的邏輯,修改data屬性,this.a = 10; this.b = 20; this.c = 30; 
//就會調用三次update渲染視圖,豈不是很耗性能 ?而且體驗也不好。

所以 vue 採用的是異步渲染 接下來,我們來了解一下 ;前邊也有講過響應式數據原理,不瞭解的童鞋可以回過頭去看看Go,這裏我就接着數據更新方法update開始;

src/croe/observer/watcher.js 166 行,這裏的更新先不考慮計算屬性和同步,我們直接看向queueWatcher

update () {
    /* istanbul ignore else */
    if (this.lazy) { // 計算屬性  依賴的數據發生變化了 會讓計算屬性的watcher的dirty變成true
      this.dirty = true
    } else if (this.sync) { // 同步watcher
      this.run()
    } else {
      queueWatcher(this) // 將要更新的 watcher 放入隊列
    }
}

src/core/observer.scheduler.js 164行,queueWatcher 方法;實現一個watcher 隊列 ,每一次的update 都放入到隊列中,然後進行統一異步處理 。 看代碼:

export function queueWatcher (watcher: Watcher) {
  const id = watcher.id // 過濾 watcher,多個data屬性依賴同一個watcher ,一個組件只有一個watcher
  if (has[id] == null) {
    has[id] = true 
    if (!flushing) {
      queue.push(watcher) // 將watcher放到隊列中
    } else { // 通過對 id 的判斷,這裏的 id 是自加1,可查看 watcher.js 源碼,
    		// 如果已經刷新了,則賦值當前的id , 如果id超過了,將運行如下代碼
      // if already flushing, splice the watcher based on its id
      // if already past its id, it will be run next immediately.
      let i = queue.length - 1
      while (i > index && queue[i].id > watcher.id) {
        i--
      }
      queue.splice(i + 1, 0, watcher)
    }
    // queue the flush
    if (!waiting) { // 如果不等了,則進行刷新 
      waiting = true

      if (process.env.NODE_ENV !== 'production' && !config.async) {
        flushSchedulerQueue() // 該方法做了刷新前的 beforUpdate 方法調用,然後 watcher.run() 
        return
      }
      nextTick(flushSchedulerQueue) // 在下一次tick中刷新 watcher 隊列 (借用nextTick)
      								//(包含同一watcher的多個data屬性),
      								// 這裏的nextTick 就是我們的常用api => this.$nextTick() 
    }
  }
}

好了,通過源碼簡單的分析,明白爲啥 vue 爲啥採用異步更新了吧,原因很簡單,因爲vue是組件級更新視圖,每一次update都要渲染整個組件,爲了提高性能,採用了隊列的形式,存儲同一個watcher的所有data屬性變化,然後統一調用nextTick 方法進行更新渲染(有且只調用一次)。

問題來了,nextTick 方法是異步的 ,那麼它又是如何實現的異步更新呢 ?來看張圖
在這裏插入圖片描述
從圖來看,調用了 nextTick 之後,將watcher隊列回調函數暫時存入了一個數組callbacks 中,然後才依次調用 timeFun()方法執行,而真正讓watcher異步的關鍵就在這兒,我們通過代碼來看一下:

首先進入 nextTick 函數 src/core/util/next-tick.js 87 行

export function nextTick (cb?: Function, ctx?: Object) { 
// flushSchedulerQueue 會使用 nextTick 保證當前視圖渲染完成
  let _resolve
  callbacks.push(() => {  // 暫存 watcher 隊列
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      _resolve(ctx)
    }
  })
  if (!pending) {  // 狀態改變後,調用 timerFun() 方法
    pending = true
    timerFunc() // 重點,重點,重點! 我們進去看一下
  }
  // $flow-disable-line
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

timerFunc 方法 src/core/util/next-tick.js 33 行 ,看如下代碼,會Js的童鞋應該就可以看出來是什麼東西;它對當前的環境進行了判斷,如果支持promise 就用 promise 依次往下: MutationObserver , setImmediate , setTimeout 這四個分別都是異步解決方案,除了 setTimeout 是宏觀任務以外,其它三個都是微觀任務;

let timerFunc
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  timerFunc = () => {
    p.then(flushCallbacks) // then 裏邊執行 flushCallbacks 
    if (isIOS) setTimeout(noop)
  }
  isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
  isNative(MutationObserver) ||
  // PhantomJS and iOS 7.x
  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
  // Use MutationObserver where native Promise is not available, 
  // e.g. PhantomJS, iOS7, Android 4.4 
  // (#6466 MutationObserver is unreliable in IE11) 
  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)) {
  // Fallback to setImmediate.
  // Technically it leverages the (macro) task queue,
  // but it is still a better choice than setTimeout.
  timerFunc = () => {
    setImmediate(flushCallbacks)   // setImmediate 回調裏邊
  }
} else {
  // Fallback to setTimeout.
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)  // setTimeout 回調裏邊
  }
}

總結

nextTick 方法主要是使用了宏任務和微任務,定義了一個異步方法.多次調用 nextTick 會將方法存入
隊列中,通過這個異步方法清空當前隊列。 所以這個 nextTick 方法就是異步方法 。

而我們平常使用的api :vue.nextTick() 也是如此 .

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章