Vue的computed計算屬性是如何實現的

一個開始

有以下代碼,full是一個計算屬性,開始,他的值是'hello world',1s後,msg變成了‘I like’, full的值同步變成了'I like world';其原理解析來看一下。vue

<div id="app">
    <span :msg="msg"></span>
    <div> {{full}}</div>
</div>
<script src="./vue.js"></script>
<script>
new Vue({
    el: '#app',
    data: {
        msg: 'hello ',
    },
    computed: {
        full() {
            return this.msg + 'world';
        },
    },
    mounted() {
        setTimeout(() => {
            this.msg = 'I like ';
        }, 1000);
    }
})

</script>

從入口開始

new Vue時,首先vue執行了_init方法,在這裏作了vue的初始化工做,其中執行了一個initState函數,該函數進行的數據處理。node

函數內容以下react

function initState (vm) {
  vm._watchers = [];
  var opts = vm.$options;
  if (opts.props) { initProps(vm, opts.props); }
  if (opts.methods) { initMethods(vm, opts.methods); }
  // data數據綁定,數據驅動核心
  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);
  }
}

其中有兩個核心流程,data綁定和computed初始化。首先來看一下計算屬性初始化幹了什麼事。express

計算屬性初始化

執行initComputed時,會執行如下操做,會爲每個computed屬性建立watcher而且執行defineComputed,在開始的示例中,會給full屬性new一個watcher。緩存

function initComputed (vm, computed) {
    // 建立watchers對象緩存watcher
    var watchers = vm._computedWatchers = Object.create(null);
    for (var key in computed) {
      // 計算屬性的執行函數/get、set描述對象
      var userDef = computed[key];
      var getter = typeof userDef === 'function' ? userDef : userDef.get;
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions
      );
      if (!(key in vm)) {
        defineComputed(vm, key, userDef);
      }
    }
  }

defineComputed作了什麼呢?app

function defineComputed (
    target,
    key,
    userDef
  ) {
    // 只論述傳入的值爲函數的狀況
    if (typeof userDef === 'function') {
      sharedPropertyDefinition.get = function computedGetter () {
        var watcher = this._computedWatchers && this._computedWatchers[key];
        if (watcher) {
          watcher.depend();
          return watcher.evaluate()
        }
      }
      sharedPropertyDefinition.set = noop;
    }
    // 進行了屬性的描述定義。
    Object.defineProperty(target, key, sharedPropertyDefinition);
}

defineComputed函數會給當前的vue實例掛載計算屬性,defineProperty定義其描述,其中get執行的函數如上。dom

那麼如今回到開始。開始的示例中,定義了full計算屬性,而且template中使用了full屬性,當模板中渲染full時,作了什麼(這是vnode解析並渲染部分)?咱們假設會獲取full的值而且填充到模板中函數

,所以咱們會觸發了full的get函數,就是以上get代碼。oop

首先獲取當前vue實例中的計算屬性對應的監聽器Watcher,而後進行depend方法執行,而後執行evaluate()方法,接下來咱們走進監聽器Watcher。ui

Vue中的Watcher

簡單介紹watcher的做用,watcher顧名思義,監聽器,1.監聽什麼2.要幹什麼事,這是咱們關心的。

var Watcher = function Watcher (
  vm,
  expOrFn,
  cb,
  options,
  isRenderWatcher
) {
  this.vm = vm;
  if (isRenderWatcher) {
    vm._watcher = this;
  }
  vm._watchers.push(this);
  // options
  if (options) {
    this.deep = !!options.deep;
    this.computed = !!options.computed;
  } else {
    this.deep = this.user = this.computed = this.sync = false;
  }
  this.cb = cb;
  this.id = ++uid$1; // uid for batching
  this.active = true;
  this.dirty = this.computed; // for computed watchers
  this.deps = [];
  this.newDeps = [];
  this.depIds = new _Set();
  this.newDepIds = new _Set();
  // parse expression for getter
  if (typeof expOrFn === 'function') {
    this.getter = expOrFn;
  }
  // 若是是計算屬性,執行if,若是不是執行else
  if (this.computed) {
    this.value = undefined;
    this.dep = new Dep();
  } else {
    this.value = this.get();
  }
};

以上代碼是Watcher的入口,看咱們關心的入參數:

vm:當前vue對象

expOrFn:監聽器觸發的函數(2.要幹什麼)

options:其餘參數

計算屬性在new Watcher時,會傳入getter函數給expOrFn,從上面代碼看,若是expOrFn是函數,就會給getter屬性賦值expOrFn,這是沒問題的。

同時計算實行new Watcher時,傳遞{computed: true}給options, 從以上代碼看出,若是是計算屬性的watcher,會與其餘watcher不一樣的邏輯。

計算屬性的Watcher會new Dep賦值給this.dep。

那麼Watcher究竟是幹嗎的,Watcher是監聽器,Vue會提供觀察者去訂閱他,若是觀察者發現須要更新某個操做,會通知Watcher,watcher會執行update進行更新。

那麼Dep是什麼。

Vue中的Dep

Dep是個訂閱器,觀察者想要訂閱監聽器,須要訂閱器Dep來實現。

同時計算屬性的Watcher也會有訂閱器,那麼他訂閱什麼呢?一樣是其餘的Watcher,好比render Watcher, 當計算屬性發生變化時,他能夠通知render Watcher進行view渲染。

回到主鏈路

如今咱們知道了,計算屬性初始化會new Watcher,並計算屬性在渲染到視圖層時會觸發getter,getter中計算屬性會觸發本身的watcher的兩個函數,depend和evaluate,

depend函數會將當前的訂閱對象添加到本身的訂閱器中,此時的訂閱對像則是render watcher,此步驟能夠本身作詳細瞭解。

主要的邏輯在evaluate中,evaluate中若是是計算屬性而且沒有被污染則調用get方法,來看一下get方法

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

get方法中,首先,計算屬性的watcher會將本身設置爲訂閱對象,共觀察者訂閱。而後執行getter,那麼this.getter咱們前面提到了,是咱們寫的計算屬性函數 () {return this.msg + 'world'};

當此getter執行時,咱們來想一下。this.msg觸發了msg屬性的get,那麼咱們前面提到vue啓動了2個核心流程 ,那麼這裏computed的流程中進入到了data流程中。

initData簡介

function initData (vm) {
    var data = vm.$options.data;
    data = vm._data = typeof data === 'function'
      ? getData(data, vm)
      : data || {};
    observe(data, true /* asRootData */);
}

簡化後,initData就作了這個事情,將data包裝爲觀察者,observe方法中最終會針對data中每個屬性作defineReactive操做,而且遞歸調用。

defineReactive即是咱們雙向數據綁定的主要部分。vue將msg屬性進行defineReactive重寫get/set,而且將它做爲一個觀察者。

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 ("development" !== 'production' && customSetter) {
          customSetter();
        }
        if (setter) {
          setter.call(obj, newVal);
        } else {
          val = newVal;
        }
        childOb = !shallow && observe(newVal);
        dep.notify();
      }
    });

當咱們執行this.msg, 進行msg的get時,以上get方法執行,而且此時咱們說過計算屬性full將本身的watcher設置爲訂閱對象Dep.target,所以msg的get中會執行dep.depend,depend方法中會將當前的Dep.target添加到訂閱器中,所以msg的訂閱列表會有full的watcher。

再次回到主鏈路

前面說到,計算屬性在初始化時會建立一個watcher,而且計算屬性會被定義爲vue實例的一個屬性Object.defineProperty,而且其getter會觸發自身watcher的兩個方法。

sharedPropertyDefinition.get = function computedGetter () {
        var watcher = this._computedWatchers && this._computedWatchers[key];
        if (watcher) {
          watcher.depend();
          return watcher.evaluate()
        }
}

getter的返回值是watcher.evaluate();

Watcher.prototype.evaluate = function evaluate () {
  if (this.dirty) {
    this.value = this.get();
    this.dirty = false;
  }
  return this.value
};

evaluate方法返回了this.value,其實此時value就是計算好的值 hello world。計算的邏輯上面講述了,連貫的敘述一遍:

在full計算屬性getter執行時,會使用this.msg的值,觸發this.msg的get,在這裏,發現目前擁有被觀察對象Dep.target(也就是計算屬性full的監聽器),msg的訂閱器會添加此觀察對象進行觀察,msg getter返回msg的值,所以full的getter執行完畢,返回了'hello world',這就是初始化的整個過程。


計算屬性的動態性實現

計算屬性的初始化已經講述完成了。那麼在msg改變時,full怎麼動態改變的呢。

大概你應該明白麼,前面講到了,msg做爲雙向數據綁定的屬性,會包裝爲觀察者,其有訂閱器會訂閱監聽器。當full計算屬性初始化時,msg的訂閱器訂閱了full的watcher,

那麼在msg值改變時,也就是setter時,我只須要通知full的watcher同步更新就行了。

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 ("development" !== 'production' && customSetter) {
        customSetter();
      }
      if (setter) {
        setter.call(obj, newVal);
      } else {
        val = newVal;
      }
      childOb = !shallow && observe(newVal);
      console.log(dep)
      dep.notify();
    }
  });

dep.notify()執行後,會通知全部觀察的watcher進行更新,所以full的watcher天然也會觸發更新,

Watcher.prototype.update = function update () {
  var this$1 = this;
  // 若是是計算屬性,執行這裏
  if (this.computed) {
    this.getAndInvoke(function () {
        this$1.dep.notify();
    });
  } else if (this.sync) {
    this.run();
  } else {
    queueWatcher(this);
  }
};

按照咱們的代碼走,執行到getAndInvoke。

Watcher.prototype.getAndInvoke = function getAndInvoke (cb) {
    var value = this.get();
    if (
      value !== this.value ||
      isObject(value) ||
      this.deep
    ) {
      // set new value
      var oldValue = this.value;
      this.value = value;
      this.dirty = false;
      if (this.user) {
        try {
          cb.call(this.vm, value, oldValue);
        } catch (e) {
          handleError(e, this.vm, ("callback for watcher \"" + (this.expression) + "\""));
        }
      } else {
        cb.call(this.vm, value, oldValue);
      }
    }
  };

經過getAndInvoke方法,咱們又一次執行了this.get,此時,msg值已經變爲了'I like ',所以這裏獲取到了新的full值。並執行了cb,cb是什麼呢,就是上一步的代碼

this.getAndInvoke(function () {
    this$1.dep.notify();
  });

this$1指向計算屬性full的watcher對象本身,this$1.dep是full watcher的訂閱器,這段代碼就是通知full watcher訂閱的watcher進行update。前面說到,計算屬性在初始getter時候,進行了

watcher.depend並添加了訂閱對象render watcher,因此在這裏,計算屬性通知更新的watcher也就是render watcher。 render watcher是什麼,是整個vue實例的渲染watcher,承載着vnode

渲染真實dom的角色。

結尾

到這裏這次分析已經完成了,這次分析從computed初始化爲入口,以雙向數據綁定爲輔助,完成的整個解析思路,以上代碼片斷均已刪減。

能力有限,源碼比較龐大,有些錯誤的地方請指正。

vue v2.5.17-beta.0版本。

相關文章
相關標籤/搜索