演示Vue.js 是如何進行「依賴收集]

初始化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的屬性配置:

從上面的代碼我們可知,我們配置了一個key爲newTodo的配置項, 我們從上面的代碼可以理解爲:

newTodo的值發生變化了,我們需要執行hander方法,所以我們來分析下具體是怎麼實現的。

我們還是先從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);
    }
  }
    //在此我向大家推薦一個前端全棧開發交流圈:582735936  突破技術瓶頸,提升思維能力

我們來具體分析下initWatch方法:

  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)
  }
  //在此我向大家推薦一個前端全棧開發交流圈:582735936  突破技術瓶頸,提升思維能力  

總結:

  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
    }
  }
    //在此我向大家推薦一個前端全棧開發交流圈:582735936  突破技術瓶頸,提升思維能力

在調用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);
      }
    }
  };
    //在此我向大家推薦一個前端全棧開發交流圈:582735936  突破技術瓶頸,提升思維能力

生成的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();
    }
  };
    //在此我向大家推薦一個前端全棧開發交流圈:582735936  突破技術瓶頸,提升思維能力

上面有一個判斷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);
      }
    }
  }
//在此我向大家推薦一個前端全棧開發交流圈:582735936  突破技術瓶頸,提升思維能力    
  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;
      })
    }
  }
//在此我向大家推薦一個前端全棧開發交流圈:582735936  突破技術瓶頸,提升思維能力    

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();
//在此我向大家推薦一個前端全棧開發交流圈:582735936  突破技術瓶頸,提升思維能力
    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
        );
      }
//在此我向大家推薦一個前端全棧開發交流圈:582735936  突破技術瓶頸,提升思維能力
      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);
      }
    }
  };

結語

感謝您的觀看,如有不足之處,歡迎批評指正。
獲取資料👈👈👈
本次給大家推薦一個免費的學習羣,裏面概括移動應用網站開發,css,html,webpack,vue node angular以及面試資源等。
對web開發技術感興趣的同學,歡迎加入Q羣:👉👉👉582735936 👈👈👈,不管你是小白還是大牛我都歡迎,還有大牛整理的一套高效率學習路線和教程與您免費分享,同時每天更新視頻資料。
最後,祝大家早日學有所成,拿到滿意offer,快速升職加薪,走上人生巔峯。

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