深刻了解 Vue.js 是如何進行「依賴收集]

前言

在上一章節咱們已經粗略的分析了整個的Vue 的源碼(還在草稿箱,須要梳理清楚才放出來),可是還有不少東西沒有深刻的去進行分析,我會經過以下幾個重要點,進行進一步深刻分析。react

  1. 深刻了解 Vue 響應式原理(數據攔截)
  2. 深刻了解 Vue.js 是如何進行「依賴收集」,準確地追蹤全部修改
  3. 深刻了解 Virtual DOM
  4. 深刻了解 Vue.js 的批量異步更新策略
  5. 深刻了解 Vue.js 內部運行機制,理解調用各個 API 背後的原理

這一章節咱們針對2. 深刻了解 Vue.js 是如何進行「依賴收集」,準確地追蹤全部修改 來進行分析。express

初始化Vue

咱們簡單實例化一個Vue的實例, 下面的咱們針對這個簡單的實例進行深刻的去思考:數組

// app Vue instance
var app = new Vue({
  data: {
    newTodo: '', 
  },

  // watch todos change for localStorage persistence
  watch: {
    newTodo: {
      handler: function (newTodo) {
        console.log(newTodo);
      },
      sync: false, 
      before: function () {

      }
    }
  }  
})
// mount
app.$mount('.todoapp')
複製代碼

initState

在上面咱們有添加一個watch的屬性配置:bash

從上面的代碼咱們可知,咱們配置了一個key爲newTodo的配置項, 咱們從上面的代碼能夠理解爲:服務器

newTodo的值發生變化了,咱們須要執行hander方法,因此咱們來分析下具體是怎麼實現的。app

咱們仍是先從initState方法查看入手:異步

function initState (vm) {
    vm._watchers = [];
    var opts = vm.$options;
    if (opts.props) { initProps(vm, opts.props); }
    if (opts.methods) { initMethods(vm, opts.methods); }
    if (opts.data) {
      initData(vm);
    } else {
      observe(vm._data = {}, true /* asRootData */);
    }
    if (opts.computed) { initComputed(vm, opts.computed); }
    if (opts.watch && opts.watch !== nativeWatch) {
      initWatch(vm, opts.watch);
    }
  }
複製代碼

咱們來具體分析下initWatch方法:async

function initWatch (vm, watch) {
    for (var key in watch) {
      var handler = watch[key];
      if (Array.isArray(handler)) {
        for (var i = 0; i < handler.length; i++) {
          createWatcher(vm, key, handler[i]);
        }
      } else {
        createWatcher(vm, key, handler);
      }
    }
  }
複製代碼

從上面的代碼分析,咱們能夠發現watch 能夠有多個hander,寫法以下:函數

watch: {
    todos:
      [
        {
          handler: function (todos) {
            todoStorage.save(todos)
          },
          deep: true
        },
        {
          handler: function (todos) {
            console.log(todos)
          },
          deep: true
        }
      ]
  },
複製代碼

咱們接下來分析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)
  }
複製代碼

總結:

  1. 從這個方法可知,其實咱們的hanlder還能夠是一個string
  2. 而且這個handervm對象上的一個方法,咱們以前已經分析methods裏面的方法都最終掛載在vm 實例對象上,能夠直接經過vm["method"]訪問,因此咱們又發現watch的另一種寫法, 直接給watchkey 直接賦值一個字符串名稱, 這個名稱能夠是methods裏面定一個的一個方法:
watch: {
    todos: 'newTodo'
  },
複製代碼
methods: {
    handlerTodos: function (todos) {
      todoStorage.save(todos)
    }
  }
複製代碼

接下來調用$watch方法

Vue.prototype.$watch = function (
      expOrFn,
      cb,
      options
    ) {
      var vm = this;
      if (isPlainObject(cb)) {
        return createWatcher(vm, expOrFn, cb, options)
      }
      options = options || {};
      options.user = true;
      var watcher = new Watcher(vm, expOrFn, cb, options);
      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();
      }
    };
複製代碼

在這個方法,咱們看到有一個immediate的屬性,中文意思就是當即, 若是咱們配置了這個屬性爲true, 就會當即執行watchhander,也就是同步 執行, 若是沒有設置, 則會這個watcher異步執行,下面會具體分析怎麼去異步執行的。 因此這個屬性可能在某些業務場景應該用的着。

在這個方法中new 了一個Watcher對象, 這個對象是一個重頭戲,咱們下面須要好好的分析下這個對象。 其代碼以下(刪除只保留了核心的代碼):

var Watcher = function Watcher (
    vm,
    expOrFn,
    cb,
    options,
    isRenderWatcher
  ) {
    this.vm = vm;
    vm._watchers.push(this);
    // parse expression for getter
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn;
    } else {
      this.getter = parsePath(expOrFn);
      if (!this.getter) {
        this.getter = noop;
        }
    }
    this.value = this.lazy
      ? undefined
      : this.get();
  };
複製代碼

主要作了以下幾件事:

  1. watcher 對象保存在vm._watchers
  2. 獲取getter,this.getter = parsePath(expOrFn);
  3. 執行this.get()去獲取value

其中parsePath方法代碼以下,返回的是一個函數:

var bailRE = /[^\w.$]/;
  function parsePath (path) {
    if (bailRE.test(path)) {
      return
    }
    var segments = path.split('.');
    return function (obj) {
      for (var i = 0; i < segments.length; i++) {
        if (!obj) { return }
        obj = obj[segments[i]];
      }
      return obj
    }
  }
複製代碼

在調用this.get()方法中去調用value = this.getter.call(vm, vm);

而後會調用上面經過obj = obj[segments[i]];去取值,如vm.newTodo, 咱們從 深刻了解 Vue 響應式原理(數據攔截),已經知道,Vue 會將data裏面的全部的數據進行攔截,以下:

Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      get: function reactiveGetter () {
        var value = getter ? getter.call(obj) : val;
        if (Dep.target) {
          dep.depend();
          if (childOb) {
            childOb.dep.depend();
            if (Array.isArray(value)) {
              dependArray(value);
            }
          }
        }
        return value
      },
      set: function reactiveSetter (newVal) {
        var value = getter ? getter.call(obj) : val;
        /* eslint-disable no-self-compare */
        if (newVal === value || (newVal !== newVal && value !== value)) {
          return
        }
        /* eslint-enable no-self-compare */
        if (customSetter) {
          customSetter();
        }
        // #7981: for accessor properties without setter
        if (getter && !setter) { return }
        if (setter) {
          setter.call(obj, newVal);
        } else {
          val = newVal;
        }
        childOb = !shallow && observe(newVal);
        dep.notify();
      }
    });
複製代碼

因此咱們在調用vm.newTodo時,會觸發getter,因此咱們來深刻的分析下getter的方法

getter

getter 的代碼以下:

get: function reactiveGetter () {
        var value = getter ? getter.call(obj) : val;
        if (Dep.target) {
          dep.depend();
          if (childOb) {
            childOb.dep.depend();
            if (Array.isArray(value)) {
              dependArray(value);
            }
          }
        }
        return value
      }
複製代碼
  1. 首先取到值var value = getter ? getter.call(obj) : val;
  2. 調用Dep對象的depend方法, 將dep對象保存在target屬性中Dep.target.addDep(this);target是一個Watcher對象 其代碼以下:
Watcher.prototype.addDep = function addDep (dep) {
    var id = dep.id;
    if (!this.newDepIds.has(id)) {
      this.newDepIds.add(id);
      this.newDeps.push(dep);
      if (!this.depIds.has(id)) {
        dep.addSub(this);
      }
    }
  };
複製代碼

生成的Dep對象以下圖:

3. 判斷是否有自屬性,若是有自屬性,遞歸調用。

如今咱們已經完成了依賴收集, 下面咱們來分析當數據改變是,怎麼去準確地追蹤全部修改

準確地追蹤全部修改

咱們能夠嘗試去修改data裏面的一個屬性值,如newTodo, 首先會進入set方法,其代碼以下:

set: function reactiveSetter (newVal) {
        var value = getter ? getter.call(obj) : val;
        /* eslint-disable no-self-compare */
        if (newVal === value || (newVal !== newVal && value !== value)) {
          return
        }
        /* eslint-enable no-self-compare */
        if (customSetter) {
          customSetter();
        }
        // #7981: for accessor properties without setter
        if (getter && !setter) { return }
        if (setter) {
          setter.call(obj, newVal);
        } else {
          val = newVal;
        }
        childOb = !shallow && observe(newVal);
        dep.notify();
      }
複製代碼

下面我來分析這個方法。

  1. 首先判斷新的value 和舊的value ,若是相等,則就直接return
  2. 調用dep.notify();去通知全部的subs, subs是一個類型是Watcher對象的數組 而subs裏面的數據,是咱們上面分析的getter邏輯維護的watcher對象.

notify方法,就是去遍歷整個subs數組裏面的對象,而後去執行update()

Dep.prototype.notify = function notify () {
    // stabilize the subscriber list first
    var subs = this.subs.slice();
    if (!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.sort(function (a, b) { return a.id - b.id; }); } for (var i = 0, l = subs.length; i < l; i++) { subs[i].update(); } }; 複製代碼

上面有一個判斷config.async,是不是異步,若是是異步,須要排序,先進先出, 而後去遍歷執行update()方法,下面咱們來看下update()方法。

Watcher.prototype.update = function update () {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true;
    } else if (this.sync) {
      this.run();
    } else {
      queueWatcher(this);
    }
  };
複製代碼

上面的方法,分紅三種狀況:

  1. 若是watch配置了lazy(懶惰的),不會當即執行(後面會分析會何時執行)
  2. 若是配置了sync(同步)爲true則會當即執行hander方法
  3. 第三種狀況就是會將其添加到watcher隊列(queue)中

咱們會重點分析下第三種狀況, 下面是queueWatcher源碼

function queueWatcher (watcher) {
    var id = watcher.id;
    if (has[id] == null) {
      has[id] = true;
      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.
        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;

        if (!config.async) {
          flushSchedulerQueue();
          return
        }
        nextTick(flushSchedulerQueue);
      }
    }
  }
複製代碼
  1. 首先flushing默認是false, 因此將watcher保存在queue的數組中。
  2. 而後waiting默認是false, 因此會走if(waiting)分支
  3. configVue的全局配置, 其async(異步)值默認是true, 因此會執行nextTick函數。

下面咱們來分析下nextTick函數

nextTick

nextTick 代碼以下:

function nextTick (cb, ctx) {
    var _resolve;
    callbacks.push(function () {
      if (cb) {
        try {
          cb.call(ctx);
        } catch (e) {
          handleError(e, ctx, 'nextTick');
        }
      } else if (_resolve) {
        _resolve(ctx);
      }
    });
    if (!pending) {
      pending = true;
      if (useMacroTask) {
        macroTimerFunc();
      } else {
        microTimerFunc();
      }
    }
    // $flow-disable-line
    if (!cb && typeof Promise !== 'undefined') {
      return new Promise(function (resolve) {
        _resolve = resolve;
      })
    }
  }
複製代碼

nextTick 主要作以下事情:

  1. 將傳遞的參數cb 的執行放在一個匿名函數中,而後保存在一個callbacks 的數組中
  2. pendinguseMacroTask的默認值都是false, 因此會執行microTimerFunc()(微Task) microTimerFunc()的定義以下:
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  microTimerFunc = () => {
    p.then(flushCallbacks)   
    if (isIOS) setTimeout(noop)
  }
} else {
  // fallback to macro
  microTimerFunc = macroTimerFunc
}
複製代碼

其實就是用Promise函數(只分析Promise兼容的狀況), 而Promise 是一個i額微Task 必須等全部的宏Task 執行完成後纔會執行, 也就是主線程空閒的時候纔會去執行微Task;

如今咱們查看下flushCallbacks函數:

function flushCallbacks () {
    pending = false;
    var copies = callbacks.slice(0);
    callbacks.length = 0;
    for (var i = 0; i < copies.length; i++) {
      copies[i]();
    }
  }
複製代碼

這個方法很簡單,

  1. 第一個是變動pending的狀態爲false
  2. 遍歷執行callbacks數組裏面的函數,咱們還記得在nextTick 函數中,將cb 保存在callbacks 中。

咱們下面來看下cb 的定義,咱們調用nextTick(flushSchedulerQueue);, 因此cb 指的就是flushSchedulerQueue 函數, 其代碼以下:

function flushSchedulerQueue () {
    flushing = true;
    var watcher, id; 
    queue.sort(function (a, b) { return a.id - b.id; });
  
    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 (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
    var activatedQueue = activatedChildren.slice();
    var 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');
    }
  }
複製代碼
  1. 首先將flushing 狀態開關變成true
  2. queue 進行按照ID 升序排序,queue是在queueWatcher 方法中,將對應的Watcher 保存在其中的。
  3. 遍歷queue去執行對應的watcherrun 方法。
  4. 執行resetSchedulerState()是去重置狀態值,如waiting = flushing = false
  5. 執行callActivatedHooks(activatedQueue);更新組件 ToDO:
  6. 執行callUpdatedHooks(updatedQueue);調用生命週期函數updated
  7. 執行devtools.emit('flush');刷新調試工具。

咱們在3. 遍歷queue去執行對應的watcher的run 方法。, 發現queue中有兩個watcher, 可是咱們在咱們的app.js中初始化Vue的 時候watch的代碼以下:

watch: { 
    newTodo: {
      handler: function (newTodo) {
        console.log(newTodo);
      },
      sync: false
    }
  }
複製代碼

從上面的代碼上,咱們只Watch了一個newTodo屬性,按照上面的分析,咱們應該只生成了一個watcher, 可是咱們卻生成了兩個watcher了, 另一個watcher究竟是怎麼來的呢?

總結:

  1. 在咱們配置的watch屬性中,生成的Watcher對象,只負責調用hanlder方法。不會負責UI的渲染
  2. 另一個watch其實算是Vue內置的一個Watch(我的理解),而是在咱們調用Vue$mount方法時生成的, 如咱們在咱們的app.js中直接調用了這個方法:app.$mount('.todoapp'). 另一種方法不直接調用這個方法,而是在初始化Vue的配置中,添加了一個el: '.todoapp'屬性就能夠。這個Watcher 負責了UI的最終渲染,很重要,咱們後面會深刻分析這個Watcher
  3. $mount方法是最後執行的一個方法,因此他生成的Watcher對象的Id 是最大的,因此咱們在遍歷queue以前,咱們會進行一個升序 排序, 限制性全部的Watch配置中生成的Watcher 對象,最後才執行$mount中生成的Watcher對象,去進行UI渲染。

$mount

咱們如今來分析$mount方法中是怎麼生成Watcher對象的,以及他的cb 是什麼。其代碼以下:

new Watcher(vm, updateComponent, noop, {
      before: function before () {
        if (vm._isMounted) {
          callHook(vm, 'beforeUpdate');
        }
      }
    }, true /* isRenderWatcher */);
複製代碼
  1. 從上面的代碼,咱們能夠看到最後一個參數isRenderWatcher設置的值是true , 表示是一個Render Watcher, 在watch 中配置的,生成的Watcher 這個值都是false, 咱們在Watcher 的構造函數中能夠看到:
if (isRenderWatcher) {
      vm._watcher = this;
    }
複製代碼

若是isRenderWatchertrue 直接將這個特殊的Watcher 掛載在Vue 實例的_watcher屬性上, 因此咱們在flushSchedulerQueue 方法中調用callUpdatedHooks 函數中,只有這個watcher纔會執行生命週期函數updated

function callUpdatedHooks (queue) {
    var i = queue.length;
    while (i--) {
      var watcher = queue[i];
      var vm = watcher.vm;
      if (vm._watcher === watcher && vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'updated');
      }
    }
  }
複製代碼
  1. 第二個參數expOrFn , 也就是Watchergetter, 會在實例化Watcher 的時候調用get方法,而後執行value = this.getter.call(vm, vm);, 在這裏就是會執行updateComponent方法,這個方法是UI 渲染的一個關鍵方法,咱們在這裏暫時不深刻分析。
  2. 第三個參數是cb, 傳入的是一個空的方法
  3. 第四個參數傳遞的是一個options對象,在這裏傳入一個before的function, 也就是,在UI從新渲染前會執行的一個生命中期函數beforeUpdate

上面咱們已經分析了watch的一個工做過程,下面咱們來分析下computed的工做過程,看其與watch 有什麼不同的地方。

computed

首先在實例化Vue 對象時,也是在initState 方法中,對computed 進行了處理,執行了initComputed方法, 其代碼以下:

function initComputed (vm, computed) {
    // $flow-disable-line
    var watchers = vm._computedWatchers = Object.create(null);
    // computed properties are just getters during SSR
    var isSSR = isServerRendering();

    for (var key in computed) {
      var userDef = computed[key];
      var getter = typeof userDef === 'function' ? userDef : userDef.get;
      if (getter == null) {
        warn(
          ("Getter is missing for computed property \"" + key + "\"."),
          vm
        );
      }

      if (!isSSR) {
        // create internal watcher for the computed property.
        watchers[key] = new Watcher(
          vm,
          getter || noop,
          noop,
          computedWatcherOptions
        );
      }

      // component-defined computed properties are already defined on the
      // component prototype. We only need to define computed properties defined
      // at instantiation here.
      if (!(key in vm)) {
        defineComputed(vm, key, userDef);
      } else {
        if (key in vm.$data) {
          warn(("The computed property \"" + key + "\" is already defined in data."), vm);
        } else if (vm.$options.props && key in vm.$options.props) {
          warn(("The computed property \"" + key + "\" is already defined as a prop."), vm);
        }
      }
    }
  }
複製代碼

上面代碼比較長,可是咱們能夠總結以下幾點:

  1. var watchers = vm._computedWatchers = Object.create(null);vm實例對象上面掛載了一個_computedWatchers的屬性,保存了由computed 生成的全部的watcher
  2. 而後遍歷全部的key, 每個key 都生成一個watcher
  3. var getter = typeof userDef === 'function' ? userDef : userDef.get; 從這個代碼能夠延伸computed 的兩種寫法,以下:
computed: {
    // 寫法1:直接是一個function
    // strLen: function () {
    //   console.log(this.newTodo.length)
    //   return this.newTodo.length
    // },
    // 寫法2: 能夠是一個對象,可是必需要有get 方法,
    // 不過寫成對象沒有什麼意義, 由於其餘的屬性,都不會使用。
    strLen: {
      get: function () {
        console.log(this.newTodo.length)
        return this.newTodo.length
      }
    }
  }
複製代碼
  1. 若是不是服務端渲染,就生成一個watcher 對象,而且保存在vm._computedWatchers屬性中,可是這個與watch 生成的watcher 有一個重要的區別就是, 傳遞了一個屬性computedWatcherOptions對象,這個對象就配置了一個lazy: ture

咱們在Watcher的構造函數中,有以下邏輯:

this.value = this.lazy
      ? undefined
      : this.get();
複製代碼

由於this.lazytrue 因此不會執行this.get();, 也就不會當即執行computed 裏面配置的對應的方法。

  1. defineComputed(vm, key, userDef);就是將computed 的屬性,直接掛載在vm 上,能夠直接經過vm.strLen去訪問,不過在這個方法中,有針對是否是服務器渲染作了區別,服務器渲染會當即執行computed 的函數,獲取值,可是在Web 則不會當即執行,而是給get 賦值一個函數:
function createComputedGetter (key) {
    return function computedGetter () {
      var watcher = this._computedWatchers && this._computedWatchers[key];
      if (watcher) {
        if (watcher.dirty) {
          watcher.evaluate();
        }
        if (Dep.target) {
          watcher.depend();
        }
        return watcher.value
      }
    }
  }
複製代碼

若是咱們在咱們的template中引用了computed的屬性,如:<div>{{strLen}}</div>, 會執行$mount去渲染模版的時候,會去調用strLen,而後就會執行上面的computedGetter的方法去獲取值, 執行的就是:

Watcher.prototype.evaluate = function evaluate () {
    this.value = this.get();
    this.dirty = false;
  };
複製代碼

執行了this.get() 就是上面分析watch 中的this.get().

思考:

咱們上面基本已經分析了computed邏輯的基本過程,可是咱們好像仍是沒有關聯上, 當咱們的data裏面的值變了,怎麼去通知computed 更新的呢?咱們的computed以下:

computed: {
    strLen: function () {
      return this.newTodo.length
    }, 
  }
複製代碼

當咱們改變this.newTodo 的時候,會執行strLen的方法呢?

答案:

  1. 在上面咱們已經分析了咱們在咱們的template 中有引用strLen,如<div>{{strLen}}</div>,在執行$mount去渲染模版的時候,會去調用strLen,而後就會執行的computedGetter的方法去獲取值,而後調用get 方法,也就是咱們computed 配置的函數:
computed: {
    strLen: function () {
      return this.newTodo.length
    }
  },
複製代碼
  1. 在執行上面方法的時候,會引用this.newTodo , 就會進入reactiveGetter方法(深刻了解 Vue 響應式原理(數據攔截))
get: function reactiveGetter () {
        var value = getter ? getter.call(obj) : val;
        if (Dep.target) {
          dep.depend();
          if (childOb) {
            childOb.dep.depend();
            if (Array.isArray(value)) {
              dependArray(value);
            }
          }
        }
        return value
      }
複製代碼

會將當前的Watcher 對象添加到dep.subs隊列中。

  1. this.newTodo值改變時,就會執行reactiveSetter方法,當執行dep.notify();時,也就會執行computed 裏面的方法,從而達到當data裏面的值改變時,其有引用這個data 屬性的computed 也就會當即執行。
  2. 若是咱們定義了computed 可是沒有任何地方去引用這個computed , 即便對應的data 屬性變動了,也不會執行computed 方法的, 即便手動執行computed 方法, 如:app.strLen也不會生效,由於在WatcheraddDep 方法,已經判斷當前的watcher 不是一個新加入的watcher
Watcher.prototype.addDep = function addDep (dep) {
    var id = dep.id;
    if (!this.newDepIds.has(id)) {
      this.newDepIds.add(id);
      this.newDeps.push(dep);
      if (!this.depIds.has(id)) {
        dep.addSub(this);
      }
    }
  };
複製代碼
相關文章
相關標籤/搜索