vue源碼分析nextTick()

1、vue頁面更新簡介

在這裏粗略的描述下當數據變化後頁面更新流程:
1、通過Observe數據劫持監聽數據變化,當數據變化後通知觸發閉包內的dep執行dep.notify,
2 、接着執行Watcher的update()
3、update()中並沒有立即執行dom的更新,而是將更新事件推送到一個任務隊列中。
4、執行任務隊列中的方法。
下圖是我對數據雙向綁定的一個理解。
在這裏插入圖片描述
這篇文章重點不是理解雙向綁定,只要明白每次數據變化,或執行一個函數將更新事件推送到一個任務隊列中。

2、macrotasks和microtasks基本瞭解

參考文章:
javascript中的異步 macrotask 和 microtask 簡介
Promise的隊列與setTimeout的隊列有何關聯?–知乎
JavaScript 運行機制詳解:再談Event Loop
Macrotasks和Microtasks 其實都是是屬於異步任務。常見的有:
macrotasks: setTimeout, setInterval, setImmediate, I/O, UI rendering
microtasks: process.nextTick, Promise, MutationObserver

我的理解:
  • macrotasks(宏任務)microtacks(微任務)。
  • 所有的同步任務都是在主線程上執行。
  • 1先會從宏任務中取出一個任務執行
  • 2宏任務執行完了,會將microtacks(微任務)隊列全部取出,依次執行
  • 3然後再去取下一個宏任務

3、源碼分析

1. 當數據變化時,觸發watcher.update
 /**
   * Subscriber interface.
   * Will be called when a dependency changes.
   */
  update () {    //watcher作爲訂閱者的update方法
    /* istanbul ignore else */
    // debugger
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      // 同步則執行run直接渲染視圖
      this.run()
    } else {
      // 異步推送到觀察者隊列中,下一個tick時調用。
      queueWatcher(this)
    }
  }
  
2. queueWatcher 將觀察者對象推入任務隊列中
/**
 * Push a watcher into the watcher queue.
 * Jobs with duplicate IDs will be skipped unless it's
 * pushed when the queue is being flushed.
 */
// 將一個觀察者對象push進觀察者隊列,在隊列中已經存在相同的id則該觀察者對象將被跳過,除非它是在隊列被刷新時推送 
export function queueWatcher (watcher: Watcher) {
  // 獲取watcher的id
  const id = watcher.id
  // 檢驗id是否存在,已經存在則直接跳過,不存在則標記哈希表has,用於下次檢驗
  // console.log('has',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 > 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
      }
      nextTick(flushSchedulerQueue)
    }
  }
}

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.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
      }
    }
  }

  // keep copies of post queues before resetting state
  const activatedQueue = activatedChildren.slice()
  const updatedQueue = queue.slice()

  resetSchedulerState()

  // call component updated and activated hooks
  callActivatedHooks(activatedQueue)
  callUpdatedHooks(updatedQueue)

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

從代碼中可以看到,flushSchedulerQueue被放入nextTick中,此時處於waiting狀態,期間還會不斷的有Watcher對象被push進隊列queue中(若原先已有相同的Watcher對象存在此次隊列中,就不進行重複推送,這裏用了has[id]來進行篩選),等待下一個tick。flushSchedulerQueue其實就是watcher視圖更新。
關於waiting變量,這是很重要的一個標誌位,它保證flushSchedulerQueue回調只允許被置入callbacks一次。只有在下一個tick中執行完flushSchedulerQueue,纔會resetSchedulerState()重置調度器狀態將waitting置爲false,允許重新被推入callbacks。
正常情況callbacks長度一直是1,只有主動去調用Vue.nextTick( [callback, context] )。

3、nextTick內部方法
/**
 * 
 *  推送到隊列中下一個tick時執行
    cb 回調函數
    ctx 上下文
 */
export function nextTick (cb?: Function, ctx?: Object) {
  let _resolve
  /*cb存到callbacks中*/
  debugger
  callbacks.push(() => {
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      _resolve(ctx)
    }
  })
  // 是否有任務在執行,pending 會在任務執行完更改狀態false
  if (!pending) {
    pending = true
    if (useMacroTask) {
      //在Vue執行綁定的DOM事件,導致dom更新的任務,會被推入宏任務列表。例:v-on的一些事件綁定
      macroTimerFunc()
    } else {
      // 微任務
      microTimerFunc()
    }
  }
  // $flow-disable-line
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

Vue對任務進行了兼容的寫法按順序 優先監測。對宏任務,和微任務進行聲明。
宏任務: setImmediate => MessageChannel =>setTimeout

/**
 * 而對於macroTask的執行,Vue優先檢測是否支持原生setImmediate(高版本IE和Edge支持),
不支持的話再去檢測是否支持原生MessageChannel,如果還不支持的話爲setTimeout(fn, 0)。
 */
if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  macroTimerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else if (typeof MessageChannel !== 'undefined' && (
  isNative(MessageChannel) ||
  // PhantomJS
  MessageChannel.toString() === '[object MessageChannelConstructor]'
)) {
  // MessageChannel與原先的MutationObserver異曲同工
/**
在Vue 2.4版本以前使用的MutationObserver來模擬異步任務。
而Vue 2.5版本以後,由於兼容性棄用了MutationObserver。
Vue 2.5+版本使用了MessageChannel來模擬macroTask。
除了IE以外,messageChannel的兼容性還是比較可觀的。
**/
 /**
  可見,新建一個MessageChannel對象,該對象通過port1來檢測信息,port2發送信息。
  通過port2的主動postMessage來觸發port1的onmessage事件,
  進而把回調函數flushCallbacks作爲macroTask參與事件循環。
  **/
  const channel = new MessageChannel()
  const port = channel.port2
  channel.port1.onmessage = flushCallbacks
  macroTimerFunc = () => {
    port.postMessage(1)
  }
} else {
  /* istanbul ignore next */
  //上面兩種都不支持,用setTimeout
  macroTimerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}

微任務 :Promise=> macroTimerFunc (setImmediate => MessageChannel =>setTimeout)

// Determine microtask defer implementation.
/* istanbul ignore next, $flow-disable-line */
// 微觀任務 microTask 延遲執行
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  microTimerFunc = () => {
    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)
  }
} else {
  // fallback to macro
  microTimerFunc = macroTimerFunc
}

源碼中有一段註釋比較重要:
其大概的意思就是:在Vue2.4之前的版本中,nextTick幾乎都是基於microTask實現的,但是由於microTask的執行優先級非常高,在某些場景之下它甚至要比事件冒泡還要快,就會導致一些詭異的問題;但是如果全部都改成macroTask,對一些有重繪和動畫的場景也會有性能的影響。所以最終nextTick採取的策略是默認走icroTask,對於一些DOM的交互事件,如v-on綁定的事件回調處理函數的處理,會強制走macroTask。

從上述兩個任務聲明可以看出,都會傳入一個flushCallbacks的函數。下面看下這個函數裏面具體執行了什麼,主要就是遍歷任務隊列,執行nextTick 中推入的函數,即flushSchedulerQueue或者開發者手動調用的 Vue.nextTick傳入的回調方法。

/*下一個tick時的回調*/
function flushCallbacks () {
  // 一個標記位,標記等待狀態(即函數已經被推入任務隊列或者主線程,已經在等待當前棧執行完畢去執行),這樣就不需要在push多個回調到callbacks時將timerFunc多次推入任務隊列或者主線程
  pending = false
  //複製callback
  const copies = callbacks.slice(0)
   //清除callback
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}

參考

【Vue源碼】Vue中DOM的異步更新策略以及nextTick機制
Vue.js異步更新DOM策略及nextTick

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