【玩轉Vue.js】數據偵聽和計算屬性實現原理


引言:

在 Vuejs 中用 watch 來偵聽數據變化,computed 用來監聽多個屬性的變化並返回計算值,那么這兩個特性是如何實現的呢?本文講一下兩者實現的具體方法以及一些使用經驗,介紹過程中會使用到前面【核心原理】篇中的知識,建議先看透原理再看本文,可以達到互相印證加深理解的效果。

結論:

由前面的【Vue核心原理】篇中介紹的數據綁定可以了解到,如果想監聽某個屬性的數據變化,那么只需要 new 一個 watcher 並在 watcher 執行的時候用到那個屬性就夠了,使用的過程中該 watcher 會被加入到對應數據的依賴中,數據變化的時候就會得到通知。所以,如果想要實現 Vue 的 watch 和 computed,實際上只需要為對應的屬性建立 watcher,並構造出執行時使用數據的函數即可,接下來展開講一下。

一、watch實現原理:

借官網的例子用一下

<div id="demo">{{ fullName }}</div>
var vm = new Vue({ el: '#demo', data: { firstName: 'Foo', lastName: 'Bar', fullName: 'Foo Bar' }, watch: { firstName: function (val) { this.fullName = val + ' ' + this.lastName }, lastName: function (val) { this.fullName = this.firstName + ' ' + val } } })

watch 這段的意思就是要監聽 firstName 和 lastName 的變化,當數據變化時執行對應的函數,給fullName賦值。

所以,根據開篇的思路,如果要監聽 firstName 的變化,那么只需要在初始化的時候創建一個 watcher,watcher 在初始化過程(立即執行watcher,還有一種懶執行 watcher 不會在初始化中執行,后面會講,此處可以忽略)中使用到 firstName 就可以了,這里的關鍵是如何構造這個函數,按照這個思路我們看一下 Vue 的實現。

  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(vm, opts.watch) ,注意這里的順序,initwatch 是在 initData 之后執行的,因為 watch 也是在已有的響應式基礎上進行監聽,所以要先初始化數據。

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);  //以firstName為例,此處為:createWatcher(vm, 'firstName', watch.firstName)
      }
    }
  }

  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) //以firstName為例,此處為:vm.$watch('firstName',watch.firstName, options) 
 }

之后調用 $watch 為 watch 中監聽的每個屬性建立 warcher ,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); //以firstName為例,此處為:new watcher('firstName',watch.firstName, undefined) 
      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();
      }
    };
  }

watcher函數邏輯

var Watcher = function Watcher (
    vm, 
    expOrFn, // 'firstName'
    cb, //watch.firstName 函數
    options,
    isRenderWatcher
  ) {
    this.vm = vm;if (isRenderWatcher) {
      vm._watcher = this;
    }
    vm._watchers.push(this);
    // options
    if (options) {
      this.deep = !!options.deep;
      this.user = !!options.user;
      this.lazy = !!options.lazy;
      this.sync = !!options.sync;
      this.before = options.before;
    } else {
      this.deep = this.user = this.lazy = this.sync = false;
    }
    this.cb = cb;
    this.id = ++uid$2; // uid for batching
    this.active = true;
    this.dirty = this.lazy; // for lazy watchers
    this.deps = [];
    this.newDeps = [];
    this.depIds = new _Set();
    this.newDepIds = new _Set();
    this.expression = expOrFn.toString();
    // parse expression for getter
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn;
    } else {
      this.getter = parsePath(expOrFn); 
      if (!this.getter) {
        this.getter = noop;
        warn(
          "Failed watching path: \"" + expOrFn + "\" " +
          'Watcher only accepts simple dot-delimited paths. ' +
          'For full control, use a function instead.',
          vm
        );
      }
    }
    this.value = this.lazy
      ? undefined
      : this.get();
  };

  /**
   * Evaluate the getter, and re-collect dependencies.
   */
  Watcher.prototype.get = function get () {
    pushTarget(this);
    var value;
    var 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
  };

可以看到,new Watcher 時傳入的表達式是 ‘firstName’,非函數類型,Vue 調用  parsePath(expOrFn=‘firstName’) 構造使用 firstName 的一個 getter 函數,從而建立依賴,開啟監聽。

  /**
   * Parse simple path.
   */
  var bailRE = new RegExp(("[^" + unicodeLetters + ".$_\\d]"));
  function parsePath (path) { //path === 'firstName' 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]]; //首次循環 obj === vm ,即 vm.fisrtName
      }
      return obj
    }
  }

可以看到,new Watcher 時傳入的表達式是 ‘firstName’,非函數類型,Vue 調用 parsePath(expOrFn=‘firstName’) 構造出使用 firstName 的一個 getter 函數,從而建立依賴,開啟監聽。所以,在這里需要看一下 parsePath 的邏輯:

this.getter = function (){
  return vm.firstName;
}

執行次函數就可以將 ‘fisrtName ’ 與當前的 watcher 關聯起來,此時的 this.cb 即為 watch 中傳入的 firstName 函數。據變化時會通知此 watcher 執行 this.cb。

this.cb = function (val) {
  this.fullName = val + ' ' + this.lastName
}

以上就是 Vue watch 的實現原理,其核心就是如何為偵聽的屬性構造一個 watcher 以及 watcher 的 getter 函數。

二、computed 實現原理:

同樣借助官網的例子看下

  <div id="example">
    <p>Original message: "{{ message }}"</p>
    <p>Computed reversed message: "{{ reversedMessage }}"</p>
  </div>

 var vm = new Vue({
  el: '#example',
  data: {
    message: 'Hello'
  },
  computed: {
    // 計算屬性的 getter
    reversedMessage: function () {
      // `this` 指向 vm 實例
      return this.message.split('').reverse().join('')
    }
  }
})

同樣,如果要監聽數據( 這里是 'message' )的變化,我們需要建立一個watcher 並構造出使用 ' message' 的函數在 watcher 執行的過程中進行使用。這里很顯然新建 watcher 需要用到的就是 computed.reverseMessage 函數,不需要構造了(也就是不需要像 watch 中那樣 調用 parsePath 來生成)。這里需要考慮一個問題,reversedMessage 是一個新增屬性,vm上並未定義過響應式,所以此處還需要借助 Object.defineProperty 將 reverMessage 定義到 vm 上,看一下總體實現:

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);
    }
  }
var computedWatcherOptions = { lazy: true };

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, // computed.reverseMessage 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); } } } }

初始化首先調用 initComputed(vm, opts.computed) ,在 initComputed 中會執行兩個步驟:

第一,為每個 key 建立一個 watcher ,watcher 的 getter 為對應 key 的函數。

var computedWatcherOptions = { lazy: true };
---
watchers[key] = new Watcher(
    vm,
    getter || noop, // computed.reverseMessage
    noop, // 回調 空 function(){}
    computedWatcherOptions // lazy: true
);

這里需要注意的是 initComputed 中創建的 watcher 為 lazy 模式 。

簡單說下,Vue 的 watcher 根據傳參不同可以分為兩種,一種是常規(立即執行)模式 ,一種是 lazy (懶執行) 模式:常規模式的 watcher 在初始化時會直接調用 getter ,getter 會獲取使用到的響應式數據,進而建立依賴關系;lazy 模式 watcher 中的 getter 不會立即執行,這部分可以看下面的 Watcher代碼,執行的時機是在獲取計算屬性(watcher.evaluate())時,稍后會講。

var Watcher = function Watcher (
    vm,
    expOrFn,
    cb,
    options,
    isRenderWatcher
  ) {
    this.vm = vm;
    debugger;

    if (isRenderWatcher) {
      vm._watcher = this;
    }
    vm._watchers.push(this);
    // options
    if (options) {
      this.deep = !!options.deep;
      this.user = !!options.user;
      this.lazy = !!options.lazy;
      this.sync = !!options.sync;
      this.before = options.before;
    } else {
      this.deep = this.user = this.lazy = this.sync = false;
    }
    this.cb = cb;
    this.id = ++uid$1; // uid for batching
    this.active = true;
    this.dirty = this.lazy; // for lazy watchers
    this.deps = [];
    this.newDeps = [];
    this.depIds = new _Set();
    this.newDepIds = new _Set();
    this.expression = expOrFn.toString();
    // parse expression for getter
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn;
    } else {
      this.getter = parsePath(expOrFn);
      if (!this.getter) {
        this.getter = noop;
        warn(
          "Failed watching path: \"" + expOrFn + "\" " +
          'Watcher only accepts simple dot-delimited paths. ' +
          'For full control, use a function instead.',
          vm
        );
      }
    }
    this.value = this.lazy
      ? undefined
      : this.get();
  };

this.value = this.lazy ? undefined : this.get();  處可以看到 lazy 模式的 watcher 不會立即執行。

第二、通過 defineComputed 調用 Object.defineProperty 將 key 定義到 vm 上。

function defineComputed (
    target,
    key,
    userDef
  ) {
    var shouldCache = !isServerRendering();
    if (typeof userDef === 'function') {
      sharedPropertyDefinition.get = shouldCache
        ? createComputedGetter(key) : createGetterInvoker(userDef);
      sharedPropertyDefinition.set = noop;
    } else {
      sharedPropertyDefinition.get = userDef.get
        ? shouldCache && userDef.cache !== false
          ? createComputedGetter(key)
          : createGetterInvoker(userDef.get)
        : noop;
      sharedPropertyDefinition.set = userDef.set || noop;
    }
    if (sharedPropertyDefinition.set === noop) {
      sharedPropertyDefinition.set = function () {
        warn(
          ("Computed property \"" + key + "\" was assigned to but it has no setter."),
          this
        );
      };
    }
    Object.defineProperty(target, key, sharedPropertyDefinition); //target === vm key == 'reverseMessage'
  }

可以看到,計算屬性的get 是由 createComputedGetter 創建而成,那么我們看下 createComputedGetter 的返回值:

ps:通過 sharedPropertyDefinition 的構造過程可以看到,如果傳入的計算屬性值為函數,那么相當於計算屬性的 get ,此時不允許 set,如果需要對計算屬性進行set,那么需要自定義傳入 set、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
      }
    }
  }

 createComputedGetter  返回了一個 computedGetter 函數,這個函數就是獲取計算屬性(reveserMessage)時的 get  函數,當獲取 reveserMessage 的時候會調用 watcher.evaluate() ,看一下watcher.evaluate 的邏輯:

  /**
   * Evaluate the value of the watcher.
   * This only gets called for lazy watchers.
   */
  Watcher.prototype.evaluate = function evaluate () {
    this.value = this.get();
    this.dirty = false;
  };


  /**
   * Evaluate the getter, and re-collect dependencies.
   */
  Watcher.prototype.get = function get () {
    pushTarget(this);
    var value;
    var 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
  };

可以看到 watcher 在 evaluate 中會直接調用 get 方法,get 方法會直接調用 getter 並返回獲取到的值,而這里的 getter 就是前面新建 watcher 時早已傳入的計算屬性值,即 computed.reveseMessage 函數,執行getter過程中就會建立起對數據的依賴。

這里啰嗦兩個小問題:

1、計算屬性使用了懶執行模式,使用時才會運算並建立依賴,可能是考慮到兩方面:一個是計算屬性不一定會被使用,性能會被白白浪費;另一個計算屬性中可能會存在比較復雜的運算邏輯,放在相對靠后的生命周期中比較合適。

2、計算屬性函數的傳入是由開發者自行傳入的,需要注意數據監聽開啟的條件是數據被使用過,在使用過程中需要注意 if 條件語句的使用,最好把需要用到的數據都定義在最上層。

以上就是computed的實現原理。

 

總結:

本文主要講了 Vuejs 中 watch 和 computed 的實現原理,核心就是要創建 watcher 並為 watcher 構造相應的 getter 函數,通過 getter 函數的執行進行綁定依賴。根據 getter 執行的時機不同 watcher 可以分為立即執行以及懶執行兩種模式,立即執行模式 getter 會在構造函數中直接執行,懶執行模式 getter 需要調用 evaluate 來執行。在使用的場景上 watch 適合直接監聽單個屬性,不涉及復雜的監聽邏輯場景,computed 適合涉及多個監聽變化的邏輯,另外 computed 比較適合做數據代理,當某些數據產生的過程比較復雜很難改動的時候,我們可以通過 computed 代理拿到原有的數據直接進行處理,簡直不要太爽~~

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM