根據調試工具看Vue源碼之watch

官方定義

  • 類型{ [key: string]: string | Function | Object | Array }javascript

  • 詳細前端

一個對象,鍵是須要觀察的表達式,值是對應回調函數。值也能夠是方法名,或者包含選項的對象。Vue 實例將會在實例化時調用 $watch(),遍歷 watch 對象的每個屬性。java

初次探索

咱們的意圖是 —— 監測app這個變量,並在函數中打下一個斷點。
咱們期待的是 —— 斷點停下後,調用棧中出現相關的函數,提供咱們分析watch原理的依據。react

抱着上面的意圖以及期待,咱們新建一個Vue項目,同時寫入如下代碼:express

created () {
    this.app = 233
},
watch: {
    app (val) {
      debugger
      console.log('val:', val)
    }
}
複製代碼

刷新頁面後右邊的調用棧顯示以下👇:數組

  • app
  • run
  • flushSchedulerQueue
  • anonymous
  • flushCallbacks
  • timeFunc
  • nextTick
  • queueWatcher
  • update
  • notify
  • reactiveSetter
  • proxySetter
  • created
  • ...

看到須要通過這麼多的調用過程,不由內心一慌... 然而,若是你理解了上一篇關於computed的文章,你很容易就能知道:瀏覽器

Vue經過對變量進行依賴收集,進而在變量的值變化時進行消息提醒。最後,依賴該變量的computed最後決定須要從新計算仍是使用緩存緩存

computedwatch仍是有些類似的,因此在看到reactiveSetter的時候,咱們心中大概想到,watch必定也利用了依賴收集微信

爲何執行了queueWatcher

單看調用棧的話,這個watch過程當中執行了queueWatcher,這個函數是放在update中的app

update的實現👇:

/** * Subscriber interface. * Will be called when a dependency changes. */
Watcher.prototype.update = function update () {
  /* istanbul ignore else */
  if (this.lazy) {
    this.dirty = true;
  } else if (this.sync) {
    this.run();
  } else {
    queueWatcher(this);
  }
};
複製代碼

顯然,queueWatcher函數是否調用,取決於這兩個變量:

  • this.lazy
  • this.sync

這兩個變量其實是在Watcher類裏初始化的,因此在這裏打下斷點,下面直接給出調用順序👇:

  • initWatch
  • createWatcher
  • Vue.$watch
  • Watcher
initWatch👇
function initWatch (vm, watch) {
  // 遍歷watch屬性
  for (var key in watch) {
    var handler = watch[key];
    // 若是是數組,那麼再遍歷一次
    if (Array.isArray(handler)) {
      for (var i = 0; i < handler.length; i++) {
        // 調用createWatcher
        createWatcher(vm, key, handler[i]);
      }
    } else {
      // 同上
      createWatcher(vm, key, handler);
    }
  }
}
複製代碼
createWatcher👇
function createWatcher ( vm, expOrFn, handler, options ) {
   // 傳值是對象時從新拿一次屬性
  if (isPlainObject(handler)) {
    options = handler;
    handler = handler.handler;
  }
  // 兼容字符類型
  if (typeof handler === 'string') {
    handler = vm[handler];
  }
  return vm.$watch(expOrFn, handler, options)
}
複製代碼
Vue.prototype.$watch👇
Vue.prototype.$watch = function ( expOrFn, cb, options ) {
	var vm = this;
	// 若是傳的cb是對象,那麼再調用一次createWatcher
    if (isPlainObject(cb)) {
      return createWatcher(vm, expOrFn, cb, options)
    }
    options = options || {};
	options.user = true;
	// 新建一個Watcher的實例
	var watcher = new Watcher(vm, expOrFn, cb, options);
	// 若是在watch的對象裏設置了immediate爲true,那麼當即執行這個它
    if (options.immediate) {
      try {
        cb.call(vm, watcher.value);
      } catch (error) {
        handleError(error, vm, ("callback for immediate watcher \"" + (watcher.expression) + "\""));
      }
    }
    return function unwatchFn () {
      watcher.teardown();
    }
  };
複製代碼
小結

watch的初始化過程比較簡單,光看上面給的註釋也是足夠清晰的了。固然,前面提到的this.lazythis.sync變量,因爲在初始化過程當中沒有傳入true值,那麼在update觸發時直接走入了queueWatcher函數

深刻研究

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. */
function queueWatcher (watcher) {
  var id = watcher.id;
  // 判斷是否已經在隊列中,防止重複觸發
  if (has[id] == null) {
    has[id] = true;
	// 沒有刷新隊列的話,直接將wacher塞入隊列中排隊
    if (!flushing) {
      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的排序插入進去
      // 若是已經刷新了這個watcher,那麼它將會在下次刷新再次被執行
      var 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;

      // 若是是開發環境,同時配置了async爲false,那麼直接調用flushSchedulerQueue
      if (process.env.NODE_ENV !== 'production' && !config.async) {
        flushSchedulerQueue();
        return
      }
      // 不然在nextTick裏調用flushSchedulerQueue
      nextTick(flushSchedulerQueue);
    }
  }
}
複製代碼

queueWatcher是一個很重要的函數,從上面的代碼咱們能夠提煉出一些關鍵點👇

  • watcher.id作去重處理,對於同時觸發queueWatcher的同一個watcher,只push一個進入隊列中
  • 一個異步刷新隊列(flashSchedulerQueue)在下一個tick中執行,同時使用waiting變量,避免重複調用
  • 若是在刷新階段觸發了queueWatcher,那麼將它按id順序從小到大的方式插入到隊列中;若是它已經刷新過了,那麼它將在隊列的下一次調用中當即執行
如何理解在刷新階段觸發queueWatcher的操做?

其實理解這個並不難,咱們將斷點打入flushSchedulerQueue中,這裏只列出簡化後的代碼👇

function flushSchedulerQueue () {
  currentFlushTimestamp = getNow();
  flushing = true;
  var watcher, id;

  ...

  for (index = 0; index < queue.length; index++) {
    watcher = queue[index];
    if (watcher.before) {
      watcher.before();
    }
    id = watcher.id;
    has[id] = null;
    watcher.run();
    
    ...
  }

  ...
}
複製代碼

其中兩個關鍵的變量:

  • fluashing
  • has[id]

都是在watcher.run()以前變化的。這意味着,在對應的watch函數執行前/執行時(此時處於刷新隊列階段),其餘變量都能在這個刷新階段從新加入到這個刷新隊列中

最後放上完整的代碼:

/** * Flush both queues and run the watchers. */
function flushSchedulerQueue () {
  currentFlushTimestamp = getNow();
  flushing = true;
  var watcher, id;

  // 刷新以前對隊列作一次排序
  // 這個操做能夠保證:
  // 1. 組件都是從父組件更新到子組件(由於父組件老是在子組件以前建立)
  // 2. 一個組件自定義的watchers都是在它的渲染watcher以前執行(由於自定義watchers都是在渲染watchers以前執行(render watcher))
  // 3. 若是一個組件在父組件的watcher執行期間恰好被銷燬,那麼這些watchers都將會被跳過
  queue.sort(function (a, b) { return a.id - b.id; });

  // 不對隊列的長度作緩存,由於在刷新階段還可能會有新的watcher加入到隊列中來
  for (index = 0; index < queue.length; index++) {
    watcher = queue[index];
    if (watcher.before) {
      watcher.before();
    }
    id = watcher.id;
    has[id] = null;
    // 執行watch裏面定義的方法
    watcher.run();
    // 在測試環境下,對可能出現的死循環作特殊處理並給出提示
    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
      }
    }
  }

  // 重置狀態前對activatedChildren、queue作一次淺拷貝(備份)
  var activatedQueue = activatedChildren.slice();
  var updatedQueue = queue.slice();

  // 重置定時器的狀態,也就是這個異步刷新中的has、waiting、flushing三個變量的狀態
  resetSchedulerState();

  // 調用組件的 updated 和 activated 鉤子
  callActivatedHooks(activatedQueue);
  callUpdatedHooks(updatedQueue);

  // deltools 的鉤子
  if (devtools && config.devtools) {
    devtools.emit('flush');
  }
}
複製代碼

nextTick

異步刷新隊列(flushSchedulerQueue)實際上是在nextTick中執行的,這裏咱們簡單分析下nextTick的實現,具體代碼以下👇

// 兩個參數,一個cb(回調),一個ctx(上下文對象)
function nextTick (cb, ctx) {
  var _resolve;
  // 把毀掉函數放入到callbacks數組裏
  callbacks.push(function () {
    if (cb) {
      try {
        // 調用回調
        cb.call(ctx);
      } catch (e) {
        // 捕獲錯誤
        handleError(e, ctx, 'nextTick');
      }
    } else if (_resolve) { // 若是cb不存在,那麼調用_resolve
      _resolve(ctx);
    }
  });
  if (!pending) {
    pending = true;
    timerFunc();
  }
  // $flow-disable-line
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(function (resolve) {
      _resolve = resolve;
    })
  }
}
複製代碼

咱們看到這裏其實還調用了一個timeFunc函數(偷個懶,這段代碼的註釋就不翻譯了🤣)👇

var timerFunc;

// The nextTick behavior leverages the microtask queue, which can be accessed
// via either native Promise.then or MutationObserver.
// MutationObserver has wider support, however it is seriously bugged in
// UIWebView in iOS >= 9.3.3 when triggered in touch event handlers. It
// completely stops working after triggering a few times... so, if native
// Promise is available, we will use it:
/* istanbul ignore next, $flow-disable-line */
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  var p = Promise.resolve();
  timerFunc = function () {
    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); }
  };
  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)
  var counter = 1;
  var observer = new MutationObserver(flushCallbacks);
  var textNode = document.createTextNode(String(counter));
  observer.observe(textNode, {
    characterData: true
  });
  timerFunc = function () {
    counter = (counter + 1) % 2;
    textNode.data = String(counter);
  };
  isUsingMicroTask = true;
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  // Fallback to setImmediate.
  // Techinically it leverages the (macro) task queue,
  // but it is still a better choice than setTimeout.
  timerFunc = function () {
    setImmediate(flushCallbacks);
  };
} else {
  // Fallback to setTimeout.
  timerFunc = function () {
    setTimeout(flushCallbacks, 0);
  };
}
複製代碼

timerFunc的代碼其實很簡單,無非是作了這些事情:

  • 檢查瀏覽器對於PromiseMutationObserversetImmediate的兼容性,並按優先級從大到小的順序分別選擇
    1. Promise
    2. MutationObserver
    3. setImmediate
    4. setTimeout
  • 在支持Promise / MutationObserver的狀況下即可以觸發微任務(microTask),在兼容性較差的時候只能使用setImmediate / setTimeout觸發宏任務(macroTask)

固然,關於宏任務(macroTask)和微任務(microTask)的概念這裏就不詳細闡述了,咱們只要知道,在異步任務執行過程當中,在同一塊兒跑線下,微任務(microTask)的優先級永遠高於宏任務(macroTask)。

tips
  1. 全局檢索其實能夠發現nextTick這個方法被綁定在了Vue的原型上👇
Vue.prototype.$nextTick = function (fn) {
  return nextTick(fn, this)
};
複製代碼
  1. nextTick並不能被隨意調起👇
if (!pending) {
  pending = true;
  timerFunc();
}
複製代碼

總結

  • watchcomputed同樣,依託於Vue的響應式系統
  • 對於一個異步刷新隊列(flushSchedulerQueue),刷新前 / 刷新後均可以有新的watcher進入隊列,固然前提是nextTick執行以前
  • computed不一樣的是,watch並非當即執行的,而是在下一個tick裏執行,也就是微任務(microTask) / 宏任務(macroTask)

掃描下方的二維碼或搜索「tony老師的前端補習班」關注個人微信公衆號,那麼就能夠第一時間收到個人最新文章。

相關文章
相關標籤/搜索