Vue中computed分析

WindrunnerMax發表於2020-09-26

Vue中computed分析

Vuecomputed是計算屬性,其會根據所依賴的資料動態顯示新的計算結果,雖然使用{{}}模板內的表示式非常便利,但是設計它們的初衷是用於簡單運算的,在模板中放入太多的邏輯會讓模板過重且難以維護,所以對於任何複雜邏輯,都應當使用計算屬性。計算屬性是基於資料的響應式依賴進行快取的,只在相關響應式依賴發生改變時它們才會重新求值,也就是說只要計算屬性依賴的資料還沒有發生改變,多次訪問計算屬性會立即返回之前的計算結果,而不必再次執行函式,當然如果不希望使用快取可以使用方法屬性並返回值即可,computed計算屬性非常適用於一個資料受多個資料影響以及需要對資料進行預處理的條件下使用。

描述

computed計算屬性可以定義兩種方式的引數,{ [key: string]: Function | { get: Function, set: Function } },計算屬性直接定義在Vue例項中,所有gettersetterthis上下文自動地繫結為Vue例項,此外如果為一個計算屬性使用了箭頭函式,則this不會指向這個元件的例項,不過仍然可以將其例項作為函式的第一個引數來訪問,計算屬性的結果會被快取,除非依賴的響應式property變化才會重新計算,注意如果某個依賴例如非響應式property在該例項範疇之外,則計算屬性是不會被更新的。

<!DOCTYPE html>
<html>
<head>
    <title>Vue</title>
</head>
<body>
    <div id="app"></div>
</body>
<script src="https://cdn.bootcss.com/vue/2.4.2/vue.js"></script>
<script type="text/javascript">
    var vm = new Vue({
        el: "#app",
        data: {
            a: 1,
            b: 2
        },
        template:`
            <div>
                <div>{{multiplication}}</div>
                <div>{{multiplication}}</div>
                <div>{{multiplication}}</div>
                <div>{{multiplicationArrow}}</div>
                <button @click="updateSetting">updateSetting</button>
            </div>
        `,
        computed:{
            multiplication: function(){
                console.log("a * b"); // 初始只列印一次 返回值被快取
                return this.a * this.b;
            },
            multiplicationArrow: vm => vm.a * vm.b * 3, // 箭頭函式可以通過傳入的引數獲取當前例項
            setting: {
                get: function(){
                    console.log("a * b * 6");
                    return this.a * this.b * 6;
                },
                set: function(v){
                    console.log(`${v} -> a`);
                    this.a = v;
                }
            }
        },
        methods:{
            updateSetting: function(){ // 點選按鈕後
                console.log(this.setting); // 12
                this.setting = 3; // 3 -> a
                console.log(this.setting); // 36
            }
        },

    })
</script>
</html>

分析

首先在Vue中完成雙向繫結是通過Object.defineProperty()實現的,Vue的雙向資料繫結,簡單點來說分為以下三個部分:

  • Observer: 這裡的主要工作是遞迴地監聽物件上的所有屬性,在屬性值改變的時候,觸發相應的Watcher
  • Watcher: 觀察者,當監聽的資料值修改時,執行響應的回撥函式,在Vue裡面的更新模板內容。
  • Dep: 連結ObserverWatcher的橋樑,每一個Observer對應一個Dep,它內部維護一個陣列,儲存與該Observer相關的Watcher

Vue原始碼的實現比較複雜,會處理各種相容問題與異常以及各種條件分支,文章分析比較核心的程式碼部分,精簡過後的版本,重要部分做出註釋,commit id0664cb0
首先在dev/src/core/instance/state.js中定義了初始化computed以及initComputed函式的實現,現在暫不考慮SSR服務端渲染的computed實現。

// dev/src/core/instance/state.js line 47
export function initState (vm: Component) {
  vm._watchers = []
  const 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) // 定義computed屬性則進行初始化
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

// dev/src/core/instance/state.js line 169
function initComputed (vm: Component, computed: Object) {
  // $flow-disable-line
  const watchers = vm._computedWatchers = Object.create(null) // 建立一個沒有原型鏈指向的物件
  // computed properties are just getters during SSR
  const isSSR = isServerRendering()

  for (const key in computed) {
    const userDef = computed[key] // 獲取計算屬性的key值定義
    const getter = typeof userDef === 'function' ? userDef : userDef.get // 由於計算屬性接受兩種型別的引數 此處判斷用以獲取getter
    if (process.env.NODE_ENV !== 'production' && getter == null) {
      warn(
        `Getter is missing for computed property "${key}".`,
        vm
      )
    }

    if (!isSSR) {
      // create internal watcher for the computed property.
      // 生成computed watcher(vm, getter, noop, { lazy: true })
      watchers[key] = new Watcher( // 計算屬性建立觀察者watcher和訊息訂閱器dep
        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 (process.env.NODE_ENV !== 'production') {
      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)
      }
    }
  }
}

defineComputed傳入了三個引數,vm例項、計算屬性的key以及userDef計算屬性的定義,屬性描述符sharedPropertyDefinition在初始化定義之後經過userDefshouldCache等多重判斷後被重寫,進而通過Object.defineProperty(target, key, sharedPropertyDefinition)進行屬性的定義。

// dev/src/core/instance/state.js line 31
const sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop
}

// dev/src/core/instance/state.js line 210
export function defineComputed (
  target: any,
  key: string,
  userDef: Object | Function
) {
  const 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 (process.env.NODE_ENV !== 'production' &&
      sharedPropertyDefinition.set === noop) {
    sharedPropertyDefinition.set = function () {
      warn(
        `Computed property "${key}" was assigned to but it has no setter.`,
        this
      )
    }
  }
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

/**
 經過重寫之後的屬性描述符在某條件分支大致呈現如下
 sharedPropertyDefinition = {
    enumerable: true,
    configurable: true,
    get: function computedGetter () {
      const watcher = this._computedWatchers && this._computedWatchers[key]
      if (watcher) {
        if (watcher.dirty) {
          watcher.evaluate()
        }
        if (Dep.target) {
          watcher.depend()
        }
        return watcher.value
      }
    },
    set: userDef.set || noop
 } 
 當計算屬性被呼叫時便會執行 get 訪問函式,從而關聯上觀察者物件 watcher 然後執行 wather.depend() 收集依賴和 watcher.evaluate() 計算求值。
*/

每日一題

https://github.com/WindrunnerMax/EveryDay

參考

https://cn.vuejs.org/v2/api/#computed
https://juejin.im/post/6844903678533451783
https://juejin.im/post/6844903873925087239
https://cn.vuejs.org/v2/guide/computed.html
https://zheyaoa.github.io/2019/09/07/computed/
https://www.cnblogs.com/tugenhua0707/p/11760466.html

相關文章