根據調試工具看Vue源碼之computed(一)

官方定義

  • 類型:{ [key: string]: Function | { get: Function, set: Function } }
  • 詳細:計算屬性將被混入到 Vue 實例中。全部 getter 和 setter 的 this 上下文自動地綁定爲 Vue 實例...

計算屬性的結果會被緩存,除非依賴的響應式屬性變化纔會從新計算。注意,若是某個依賴 (好比非響應式屬性) 在該實例範疇以外,則計算屬性是不會被更新的。javascript

上面這幾段話其實能夠概括爲如下幾點:前端

  • computed是計算屬性,會被混入到Vue實例中
  • computed的結果會被緩存,除非依賴的響應式屬性變化纔會從新計算

如何初始化computed

同以往同樣,先新建一個Vue項目,同時加入如下代碼:java

export default {
  name: 'test',
  data () {
    return {
      app: 666
    }
  },
  created () {
    console.log('app proxy -->', this.appProxy)
  },
  computed () {
    appProxy () {
      debugger
      return this.app
    }
  }
}

F12打開調試界面,刷新後斷點停在了debugger的位置,同時能夠看到右邊的調用棧:express

  • appProxy
  • get
  • evaluate
  • computedGetter
  • created
  • ...

瞥到computedGetter以後,點進去,能夠看到:緩存

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,看看它是在哪裏調用的。此處忽略搜索的過程,直接給出個人結論:微信

Vue中存在兩種初始化computed的方法:app

  1. option中初始化
  2. Vue.prototype.extend函數中初始化

這兩種初始化其實大同小異,咱們選擇在組件中寫computed,天然斷點就會跑到Vue.prototype.extend函數裏:函數

...
if (Sub.options.computed) {
  initComputed$1(Sub);
}
...

initComputed$1函數:oop

function initComputed$1 (Comp) {
  // 拿到組件的computed
  var computed = Comp.options.computed;
  for (var key in computed) {
    // 循環遍歷
    defineComputed(Comp.prototype, key, computed[key]);
  }
}

顯然,這句代碼:defineComputed(Comp.prototype, key, computed[key])computed掛載在了組件的原型上,下面來看下它的實現方式:this

defineComputed

function defineComputed (
  target,
  key,
  userDef
) {
  // 判斷是否要將結果緩存下來
  var shouldCache = !isServerRendering();
  // 下面進行分類判斷
  // 對應的computed是函數的狀況
  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 (process.env.NODE_ENV !== 'production' &&
      sharedPropertyDefinition.set === noop) {
    sharedPropertyDefinition.set = function () {
      warn(
        ("Computed property \"" + key + "\" was assigned to but it has no setter."),
        this
      );
    };
  }
  // 將sharedPropertyDefinition綁定到組件對象上
  Object.defineProperty(target, key, sharedPropertyDefinition);
}

😅感受有點亂,最後再梳理下上邊的邏輯:

initComputed

  • 執行initComputed,從Vue中拿到computed對象裏全部的key
  • 循環拿到的key值,調用defineComputed函數,把computed綁定到組件對象上

defineComputed

  • 判斷是否在服務端渲染,是則computed的結果會被緩存,不是則不會緩存計算結果
  • 因爲computed存在兩種寫法,這裏也對函數對象的寫法作了區分

computed的結果緩存是如何實現的?

上面咱們大體梳理了下 computed的初始化邏輯,如今咱們回過頭來再看一下官方定義,發現其中提到了 計算屬性會將計算結果緩存下來,那麼這個計算結果究竟是怎麼被緩存下來的呢?

回到defineComputed

defineComputed裏最後將sharedPropertyDefinition綁定到組件對象上,在代碼裏面能夠看到對sharedPropertyDefinition.get作了特殊處理,兩種狀況分別封裝了:

  • createComputedGetter
  • createGetterInvoker

createComputedGetter的實現:

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

createGetterInvoker的實現:

function createGetterInvoker(fn) {
  return function computedGetter () {
    return fn.call(this, this)
  }
}

能夠看到,服務端渲染確實是對計算屬性的結果不作緩存的,可是咱們對結果是如何緩存,依舊是一臉懵逼😐

回到最初的斷點

刷新頁面回到一開始咱們在appProxy中打下的斷點,在調用棧中有兩個顯眼的函數:

  • evaluate
  • get

分別點進去,咱們能夠看到:

evaluate實現源碼:

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

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

結合上面給出的createComputedGetter源碼咱們能夠知道,computed的計算結果是經過Watcher.prototype.get來獲得的,拿到value之後,在Wathcer.prototype.evaluate中執行了這樣一行代碼:

...
this.dirty = false;

聰明的讀者確定猜到了,計算屬性是否從新計算結果,確定跟這個屬性有關。接下來咱們只要跟蹤這個屬性的變化,就能夠輕鬆的知道計算屬性的緩存原理了。

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

相關文章
相關標籤/搜索