vue源碼解讀(四)Vue中的異步更新策略

歡迎star個人github倉庫,共同窗習~目前vue源碼學習系列已經更新了6篇啦~

https://github.com/yisha0307/...

快速跳轉:javascript

異步更新隊列

引用下vue.js官方教程關於vue中異步更新隊列的解釋:html

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

爲何要這麼作呢?來看一個簡單的例子:vue

<template>
  <div>
    <div>{{test}}</div>
  </div>
</template>
export default {
    data () {
        return {
            test: 0
        };
    },
    mounted () {
      for(let i = 0; i < 1000; i++) {
        this.test++;
      }
    }
}

根據mounted裏的代碼,test一共被觸發了1000次,若是不是異步更新,watcher在被觸發1000次時,每次都會去更新視圖,這是很是很是消耗性能的。所以,vue纔會採用異步更新的操做,若是同一個 watcher 被屢次觸發,只會被推入到隊列中一次。而後在下一次事件循環'tick‘的時候,只需更新一次dom便可。java

咱們來看一下watcher類中的update方法,瞭解下具體這個異步更新代碼是如何實現的。git

update () {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      /*同步則執行run直接渲染視圖*/
      // 基本不會用到sync
      this.run()
    } else {
      /*異步推送到觀察者隊列中,由調度者調用。*/
      queueWatcher(this)
    }
  }
export function queueWatcher (watcher: Watcher) {
  /*獲取watcher的id*/
  const id = watcher.id
  /*檢驗id是否存在,已經存在則直接跳過,不存在則標記哈希表has,用於下次檢驗*/
  if (has[id] == null) {
    has[id] = true
    if (!flushing) {
      /*若是沒有flush掉,直接push到隊列中便可*/
      queue.push(watcher)
    } else {
      // 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 >= 0 && queue[i].id > watcher.id) {
        i--
      }
      queue.splice(Math.max(i, index) + 1, 0, watcher)
    }
    // queue the flush
    if (!waiting) {
      waiting = true
      nextTick(flushSchedulerQueue)
    }
  }
}

咱們能夠看到,watcher在update時候,調用了一個queueWatcher的方法,將watcher異步推送到觀察者隊列中;調用queueWatcher的時候,首先查看了watcher的id,保證相同的watcher不被屢次推入;waiting是一個標識,若是沒有在waiting, 就異步執行flushSchedulerQueue方法。github

flushSchedulerQueue

flushSchedulerQueue是下一個tick的時候的回調函數,主要就是去執行watcher中的run方法。看一下源碼:express

/*nextTick的回調函數,在下一個tick時flush掉兩個隊列同時運行watchers*/
function flushSchedulerQueue () {
  flushing = true
  let watcher, id

  // Sort queue before flush.
  // This ensures that:
  // 1. Components are updated from parent to child. (because parent is always
  //    created before the child)
  // 2. A component's user watchers are run before its render watcher (because
  //    user watchers are created before the render watcher)
  // 3. If a component is destroyed during a parent component's watcher run,
  //    its watchers can be skipped.
  /*
    給queue排序,這樣作能夠保證:
    1.組件更新的順序是從父組件到子組件的順序,由於父組件老是比子組件先建立。
    2.一個組件的user watchers比render watcher先運行,由於user watchers每每比render watcher更早建立
    3.若是一個組件在父組件watcher運行期間被銷燬,它的watcher執行將被跳過。
  */
 // queue: Array<Watcher>
  queue.sort((a, b) => a.id - b.id)

  // do not cache length because more watchers might be pushed
  // as we run existing watchers
  for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    id = watcher.id
    /*將has的標記刪除*/
    has[id] = null
    /*執行watcher*/
    watcher.run()
    // in dev build, check and stop circular updates.
    if (process.env.NODE_ENV !== 'production' && has[id] != null) {
      circular[id] = (circular[id] || 0) + 1
      if (circular[id] > MAX_UPDATE_COUNT) {
        warn(
          'You may have an infinite update loop ' + (
            watcher.user
              ? `in watcher with expression "${watcher.expression}"`
              : `in a component render function.`
          ),
          watcher.vm
        )
        break
      }
    }
  }

  // keep copies of post queues before resetting state
  /**/
  /*獲得隊列的拷貝*/
  const activatedQueue = activatedChildren.slice()
  const updatedQueue = queue.slice()

  /*重置調度者的狀態*/
  resetSchedulerState()

  // call component updated and activated hooks
  /*使子組件狀態都改編成active同時調用activated鉤子*/
  callActivatedHooks(activatedQueue)
  /*調用updated鉤子*/
  callUpdateHooks(updatedQueue)

  // devtool hook
  /* istanbul ignore if */
  if (devtools && config.devtools) {
    devtools.emit('flush')
  }
}

操做真實DOM

這時候咱們再來看一下vue中文教程裏的代碼:閉包

<div id="example">{{message}}</div>
var vm = new Vue({
  el: '#example',
  data: {
    message: '123'
  }
})
vm.message = 'new message' // 更改數據
vm.$el.textContent === 'new message' // false
Vue.nextTick(function () {
  vm.$el.textContent === 'new message' // true
})

當設置vm.message = 'new message'時候,該組件並不會當即進行渲染,而是在dep.notify了watcher以後,被異步推到了調度者隊列中,等到nextTick的時候進行更新。nextTick則是會去嘗試原生的Promise.then和MutationObserver,若是都不支持,最差會採用setTimeout(fn, 0)進行異步更新。因此若是想基於更新後的DOM狀態作點什麼,能夠在數據變化以後當即使用Vue.nextTick(callback)對真實DOM進行操做。dom

相關文章
相關標籤/搜索