Vue 计算属性(3)

240 阅读3分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

1. computedgettersetter

在大多数情况下,计算属性(computed)只需要一个 getter 方法即可,所以我们会将计算属性直接写成一个函数(表示的就是计算属性的 getter 方法,本质上就是对象的 get 属性对应的函数):

computed: {
  // 计算属性 fullName 的 getter 方法
  fullName: function() {
    return this.firstName + ' ' + this.lastName;
  }
}

上面的写法相当于下面这种写法的语法糖:

computed: {
  // 计算属性 fullName 对应一个对象,对象中需要有 getter(get) 方法
  fullName: {
    get: function() {
      return this.firstName + ' ' + this.lastName;
    }
  }
}

但是,如果我们还想设置计算属性的值呢?这时我们就可以给计算属性设置一个 setter 方法:

computed: {
  // 计算属性 fullName 的完整写法
  fullName: {
    // 计算属性 fullName 的 getter 方法
    get: function() {
      return this.firstName + ' ' + this.lastName;
    },
    // 计算属性 fullName 的 setter 方法
    set: function(newValue) {
      console.log(newValue);
      const newArr = newValue.split(' ');
      if (newArr.length === 2) {
        this.firstName = newArr[0];
        this.lastName = newArr[1];
      } else {
        this.firstName = newValue;
        this.lastName = '';
      }
    }
  }
}

这样一来,一旦给计算属性 fullName 赋值,就会执行其中的 setterset) 方法。而在 setter 方法中,因为对 data 选项中的 firstNamelastName 做了修改,所以计算属性 fullNamegetter 方法又会被重新调用,从而返回最新的 fullName 值。

<body> 元素中的代码:

<div id="app"></div>

<template id="my-app">
  <button @click="changeFullName">修改 fullName</button>
  <h2>{{ fullName }}</h2>
</template>

<script src="./js/vue.js"></script>
<script>
  const App = {
    data() {
      return {
        firstName: 'Shane',
        lastName: 'Filan',
      }
    },
    computed: {
      // 计算属性 fullName 的完整写法
      fullName: {
        // 计算属性 fullName 的 getter 方法
        get: function() {
          return this.firstName + ' ' + this.lastName;
        },
        // 计算属性 fullName 的 setter 方法
        set: function(newValue) {
          console.log(newValue);
          const newArr = newValue.split(' ');
          if (newArr.length === 2) {
            this.firstName = newArr[0];
            this.lastName = newArr[1];
          } else {
            this.firstName = newValue;
            this.lastName = '';
          }
        }
      }
    },
    methods: {
      changeFullName() {
        this.fullName = 'Coder Zhj';
      }
    },
    template: '#my-app'
  };

  Vue.createApp(App).mount('#app');
</script>

页面效果:

computed 的 getter 和 setter.gif

2. 源码解析

你可能想知道,Vue 内部是如何对我们传入的是一个 getter,还是一个包含 gettersetter 的对象进行处理的呢?

其实非常简单,Vue 源码内部只是做了一个逻辑判断而已,有关代码如下:

image-20210823123217610.png

image-20210826223101195.png

image-20210826225118262.png

源码解析:

if (computedOptions) { // 如果有 computedOptions 即 computed 选项(属性)
  for (const key in computedOptions) { // 遍历 computedOptions(computed)对象中的属性
    const opt = (computedOptions as ComputedOptions)[key] // 通过计算属性名取出对应的计算属性值
    const get = isFunction(opt) // 该计算属性是函数吗?
      ? opt.bind(publicThis, publicThis) // 是函数,将该函数中的 this 绑定为 publicThis(即实例中的代理对象),同时把 publicThis 传给该函数的第一个参数,然后把该函数赋值给 get(后面作为该计算属性的 getter 方法)
      : isFunction(opt.get) // 不是函数,那应该是个对象,那该对象中的 get 属性值是函数吗?(对象中是否有 getter 方法?)
        ? opt.get.bind(publicThis, publicThis) // 对象中的 get 属性值是函数(对象中有 getter 方法),将该函数中的 this 绑定为 publicThis,同时把 publicThis 传给该函数的第一个参数,然后把该函数赋值给 get(后面作为该计算属性的 getter 方法)
        : NOOP // 对象中的 get 属性值也不是函数(对象中没有 getter 方法),那么把一个空的函数实现赋值给 get
    if (__DEV__ && get === NOOP) {
      warn(`Computed property "${key}" has no getter.`)
    }
    const set =
      !isFunction(opt) && isFunction(opt.set) // 如果该计算属性不是函数同时其中的 set 属性是函数
        ? opt.set.bind(publicThis) // 则将该函数中的 this 绑定为 publicThis,然后把该函数赋值给 set
        : __DEV__ // 否则如果是开发环境
          ? () => {
              warn(
                `Write operation failed: computed property "${key}" is readonly.`
              )
            } // 则把会给出警告信息的函数赋值给 set
          : NOOP // 不是开发环境,就把空函数赋值给 set
    
    // 调用 computed 函数以实现计算属性具体的一些功能(响应式、缓存等),后面讲 Composition API 时,
    // 我们也都是自己调用此函数来实现计算属性的,后面讲完响应式原理之后再来理解
    const c = computed({
      get,
      set
    })
    Object.defineProperty(ctx, key, {
      enumerable: true,
      configurable: true,
      get: () => c.value,
      set: v => (c.value = v)
    })
    if (__DEV__) {
      checkDuplicateProperties!(OptionTypes.COMPUTED, key)
    }
  }
}