vue源碼分析之nextTick

Vue中有個API是nextTick,官方文檔是這樣介紹做用的:html

將回調延遲到下次 DOM 更新循環以後執行。在修改數據以後當即使用它,而後等待 DOM 更新。

理解這部份內容,有助於理解Vue對頁面的渲染過程,同時也能夠了解到beforeUpdateupdated的使用。另外就是經過了解nextTick的調用瞭解vue內部是如何使用Promise的。這部份內容和以前介紹計算屬性的內容也有關聯,能夠比照着看。vue

首先看一下我建立的例子:express

<!-- HTML 部分 -->
    <div id="test">
      <p>{{ name }}的年齡是{{ age }}</p>
      <!-- <p>
        {{ info }}
      </p> -->
      <div>體重<input type="text" v-model="age" /></div>
      <button @click="setAge">設置年齡爲100</button>
    </div>
// js 部分
      new Vue({
        el: '#test',
        data() {
          return {
            name: 'tuanzi',
            age: 2
          }
        },
        beforeUpdate() {
          console.log('before update')
          debugger
        },
        updated() {
          console.log('updated')
          debugger
        },
        methods: {
          setAge() {
            this.age = 190
            debugger
            this.$nextTick(() => {
              console.log('next tick', this.age)
              debugger
            })
          }
        }
      })

當頁面渲染完成,點擊按鈕觸發事件以後,都會發生什麼呢~~promise

直接介紹計算屬性的時候說過,當頁面初次加載渲染,會調用模板中的值,這時會觸發該值的getter設置。因此對於咱們這裏,data中的nameage都會訂閱updateComponent這個方法,這裏咱們看下這個函數的定義:dom

updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }

簡而言之,這時用來渲染頁面的,因此當代碼執行到this.age = 190,這裏就會觸發agesetter屬性,該屬性會調用dep.notify方法:異步

// 通知
  notify() {
    // stabilize the subscriber list first
    // 淺拷貝訂閱列表
    const subs = this.subs.slice()
    if (process.env.NODE_ENV !== 'production' && !config.async) {
      // subs aren't sorted in scheduler if not running async
      // we need to sort them now to make sure they fire in correct
      // order

      // 關閉異步,則subs不在調度中排序
      // 爲了保證他們能正確的執行,如今就帶他們進行排序
      subs.sort((a, b) => a.id - b.id)
    }
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }

這裏的this.subs就是頁面初始化過程當中,age這個屬性收集到的依賴關係,也就是renderWatcher實例。接着調用renderWatcherupdate方法。async

/**
   * Subscriber interface.
   * Will be called when a dependency changes.
   */
  update() {
    // debugger
    /* istanbul ignore else */
    if (this.lazy) {
      // 執行 computedWacher 會運行到這裏
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      // 運行 renderWatcher
      queueWatcher(this)
    }
  }

那爲了更好的理解這裏,我把renderWatcher的實例化的代碼也貼出來:ide

// we set this to vm._watcher inside the watcher's constructor
  // since the watcher's initial patch may call $forceUpdate (e.g. inside child
  // component's mounted hook), which relies on vm._watcher being already defined
  new Watcher(
    vm,
    updateComponent,
    noop,
    {
      before() {
        if (vm._isMounted && !vm._isDestroyed) {
          callHook(vm, 'beforeUpdate')
        }
      }
    },
    true /* isRenderWatcher */
  )

所以,renderWatcher是沒有設置lazy這個屬性的,同時我也沒有手動設置sync屬性,所以代碼會執行到queueWatcher(this)。注意這裏的this,當前屬於renderWatcher實例對象,所以這裏傳遞的this就是該對象。函數

// 將一個watcher實例推入隊列準備執行
// 若是隊列中存在相同的watcher則跳過這個watcher
// 除非隊列正在刷新
export function queueWatcher(watcher: Watcher) {
  const id = watcher.id
  debugger
  if (has[id] == null) {
    has[id] = true
    if (!flushing) {
      // 沒有在刷新隊列,則推入新的watcher實例
      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.

      // 隊列已經刷新,則用傳入的watcher實例的id和隊列中的id比較,按大小順序插入隊列
      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()
        return
      }
      debugger
      nextTick(flushSchedulerQueue)
    }
  }
}

這段代碼比較簡單,就說一點。代碼裏有個判斷是config.async,這是Vue私有對象上的值,默認的是true,所以代碼會執行到nextTick這裏,此時會傳入一個回調函數flushSchedulerQueue,咱們這裏先不說,以後用的的時候再介紹。如今看看nextTick的實現。oop

const callbacks = []
let pending = false

export function nextTick(cb?: Function, ctx?: Object) {
  debugger
  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
    })
  }
}

pendding用來判斷是否存在等待的隊列,callbacks是執行回調的隊列。那對於此時此刻,就是向callbacks推入一個回調函數,其中要執行的部分就是flushSchedulerQueue。由於是初次調用這個函數,這裏的就會調用到timerFunc

let timerFunc
  
  const p = Promise.resolve()
  
  timerFunc = () => {
    p.then(flushCallbacks)
    // In problematic UIWebViews, Promise.then doesn't completely break, but
    // it can get stuck in a weird state where callbacks are pushed into the
    // microtask queue but the queue isn't being flushed, until the browser
    // needs to do some other work, e.g. handle a timer. Therefore we can
    // "force" the microtask queue to be flushed by adding an empty timer.
    if (isIOS) setTimeout(noop)
  }

如今毫無由於的是timerFunc這個函數會被調用。可是有個問題,p.then(flushCallbacks)這句話會執行麼?來看個例子:

function callback() {
    console.log('callback')
}

let p = Promise.resolve()
function func() {
    p.then(callback)
}

console.log('this is start')

func()

console.log('this is pre promise 1')

let a = 1
console.log('this is pre promise 2')
console.log(a)

思考一下結果是什麼吧。看看和答案是否一致:

說回上面,p.then(flushCallbacks)這句話在這裏會執行,可是是將flushCallbacks這個方法推入了微任務隊列,要等其餘的同步代碼執行完成,執行棧空了以後纔會調用。因此對於renderWatcher來講,目前就算執行完了。

接下來代碼執行到這裏:

this.$nextTick(() => {
  console.log('next tick', this.age)
  debugger
})

看下$nextTick的定義:

Vue.prototype.$nextTick = function (fn: Function) {
    return nextTick(fn, this)
  }

這裏定義$nextTick是定義在Vue的原型對象上,因此在頁面中能夠經過this.$nextTick調用,同時傳入的this就是當前頁的實例。因此看會nextTick定義的部分,惟一的區別是,這是的penddingfalse,所以不會再調用一次timerFunc

setAge裏的同步代碼都執行完了,所以就輪到flushCallbacks出場。來看下定義:

function flushCallbacks() {
  debugger
  pending = false
  const copies = callbacks.slice(0)
  callbacks.length = 0
  console.log(copies)
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}

這裏定義的位置和定義nextTick是在同一個文件裏,所以penddingcallbacks是共享的。主要就看copies[i]()這一段。通過前面的執行,此時callbacks.length的值應該是2。copies[1]指的就是先前推動隊列的flushSchedulerQueue

/**
 * Flush both queues and run the watchers.
 *
 * 刷新隊列而且運行watcher
 */
function flushSchedulerQueue() {
  currentFlushTimestamp = getNow()
  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.

  // 給刷新隊列排序,緣由以下:
  // 1. 組件的更新是從父組件開始,子組件結束
  // 2. 組件的 userWatcher 的運行老是先於 renderWatcher
  // 3. 若是父組件的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]
    if (watcher.before) {
      watcher.before()
    }
    id = watcher.id
    has[id] = null
    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
      }
    }
  }

watcher.before這個方法是存在的,先前的代碼中有,在初始化renderWatcher時傳入了這個參數。這裏就調用了callHook(vm, 'beforeUpdate'),因此能看出來,此時beforeUpdate執行了。接着執行watcher.run()runWatcher類上定義的一個方法。

/**
   * Scheduler job interface.
   * Will be called by the scheduler.
   */
  run() {
    debugger
    if (this.active) {
      const value = this.get()
      if (
        value !== this.value ||
        // Deep watchers and watchers on Object/Arrays should fire even
        // when the value is the same, because the value may
        // have mutated.
        isObject(value) ||
        this.deep
      ) {
        // set new value
        const oldValue = this.value
        this.value = value
        if (this.user) {
          try {
            this.cb.call(this.vm, value, oldValue)
          } catch (e) {
            handleError(e, this.vm, `callback for watcher "${this.expression}"`)
          }
        } else {
          this.cb.call(this.vm, value, oldValue)
        }
      }
    }
  }

this.active初始化的值就是true,get方法以前的文章也提到過,這裏再貼一遍代碼:

/**
   * Evaluate the getter, and re-collect dependencies.
   */
  get() {
    // debugger
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      value = this.getter.call(vm, vm)
    } catch (e) {
      if (this.user) {
        handleError(e, vm, `getter for watcher "${this.expression}"`)
      } else {
        throw e
      }
    } finally {
      // "touch" every property so they are all tracked as
      // dependencies for deep watching
      if (this.deep) {
        traverse(value)
      }
      popTarget()
      this.cleanupDeps()
    }
    return value
  }

這部分代碼以前說過,這裏就再也不說了,只提一點,此時的this.getter執行的是updateComponent,其實也就是裏面定義的vm._update(vm._render(), hydrating)。關於renderupdate我會在分析虛擬dom時介紹。

如今須要知道的是,頁面此時會從新渲染,我在setAge方法中修改了age的值,當vm._update執行完,就會發現頁面上的值變化了。那接着就執行callbacks中的下一個值,也就是我寫在$nextTick中的回調函數,這個就很簡單,不必再說。點擊按鈕到如今新的頁面渲染完成,執行的結果就是:

before update
updated
next tick 100

這裏就把整個流程講完了,可是我想到vue文檔中說的:

在修改數據以後當即使用它,而後等待 DOM 更新

假設我如今要是把$nextTick放到修改值以前呢。把setAge修改一下。

setAge() {
    this.$nextTick(() => {
      console.log('next tick', this.age)
      debugger
    })
    debugger
    this.age = 100
  }

思考一下,此時點擊按鈕,頁面會打印出什麼東西。按照邏輯,由於$nextTick寫在了前面,所以會被先推動callbacks中,也就會被第一個執行。因此此時我覺得打印出來的age仍是2。但我既然都這樣說了,那結果確定是和我覺得的不同,但我有一部分想的沒錯,就是優先推入,優先調用。當我忘了一點,你們也能夠會想一下,renderWatcher是如何被觸發的?

$nextTick回調如今是進入了微任務隊列,因此會繼續執行接下來的賦值。此時會觸發age設置的setter裏的dep.notify。但在調用以前,新的值就已經傳給age了。因此當$nextTick裏的回調執行時,會觸發agegetter,拿到的值就是新的值。

整個nextTick事件就介紹完了。

相關文章
相關標籤/搜索