Vue2 自定义计算属性

  • 简述:类似 Vue 2.7 / Vue 3.x 的 computed 组合式API,可自由创建计算属性对象,灵活设置计算属性到Vue实例中。兼容 uniapp 环境和 Vue 2.7 以前版本下使用。

源码

/**
*  创建Vue计算属性对象
*  @param  {Vue}             vm                    Vue实例
*  @param  {Function|Object} getterOrOptions       取值方法或配置项
*  @param  {Function}        getterOrOptions.get   取值方法
*  @param  {Function}        getterOrOptions.set   设值方法
*  @param  {Boolean}         getterOrOptions.cache 是否使用缓存 (默认是)
*  @return {Object}
*/
function createVueComputed(vm, getterOrOptions) {
let Dep = createVueComputed.Dep ||= (vm._data || vm.$parent?._data)?.__ob__.dep.constructor
let Watcher = createVueComputed.Watcher ||= (vm._watcher || vm.$parent?._watcher)?.constructor
if (!Dep || !Watcher) throw new Error('Cannot find necessary dependency methods.')
let noop = createVueComputed.noop ||= function () {}
let isFn = typeof getterOrOptions == 'function'
let getter = isFn ? getterOrOptions : getterOrOptions.get
let setter = isFn ? void 0 : getterOrOptions.set
let cache = getter ? isFn || (getterOrOptions.cache ?? true) : false
let watcher = new Watcher(vm, getter || noop, noop, { lazy: true })
return Object.defineProperties(Object.create(null), {
value: {
configurable: true,
enumerable: true,
set: setter ? setter.bind(null) : noop,
get: cache ? function () {
if (watcher.dirty) watcher.evaluate()
if (Dep.target) {
Dep.target.onTrack && Dep.target.onTrack({
effect: Dep.target,
target: this,
type: 'get',
key: 'value'
})
watcher.depend()
}
return watcher.value
} : getter || noop
},
effect: { configurable: true, enumerable: true, value: watcher },
__v_isRef: { configurable: true, value: true },
__v_isReadonly: { configurable: true, value: !setter }
})
}
/**
*  设置Vue实例的计算属性
*  @param  {Vue}     vm          Vue实例
*  @param  {Object}  properties  计算属性表 { [key]: { get(rawGet), set(newVal, rawSet), cache } }
*/
function setVueInstanceComputed(vm, properties) {
let noop = setVueInstanceComputed.noop ||= function () {}
let getDesc = Object.getOwnPropertyDescriptor
let proto = Object.getPrototypeOf(vm)
let watchers = vm._computedWatchers ||= Object.create(null)
for (let key in properties) {
let desc = getDesc(vm, key) || getDesc(proto, key) || {}
let isFn = typeof properties[key] == 'function'
let getter = isFn ? properties[key] : properties[key].get
let setter = isFn ? void 0 : properties[key].set
let cache = getter ? isFn || (properties[key].cache ?? true) : false
let rawGet = desc.get?.bind(vm), rawSet = desc.set?.bind(vm)
let useGet = getter ? function () { return getter.call(this, rawGet) } : noop
let useSet = setter ? function (val) { setter.call(this, val, rawSet) } : noop
let ref = cache ? createVueComputed(vm, useGet) : null
if (watchers[key]) watchers[key].teardown(), delete watchers[key]
if (cache) watchers[key] = ref.effect
Object.defineProperty(vm, key, {
configurable: desc.configurable ?? true,
enumerable: desc.enumerable ?? true,
set: useSet,
get: cache ? getDesc(ref, 'value').get : useGet
})
}
}
// 可绑定到Vue原型链中使用
Vue.prototype.$computed = function () {
return createVueComputed(this, ...arguments)
}
Vue.prototype.$setComputed = function () {
return setVueInstanceComputed(this, ...arguments)
}

使用示例

<template>
  <div>
    <div>
    Demo1: {{ demo1 }} <button @click="val1++">++</button>
    </div>
    <div>
    Demo2: {{ demo2 }} <button @click="demo2++">++</button>
    </div>
    <div>
      Demo3: <input type="text" v-model="val3" />
    </div>
    <div>
      Bind new computed ref: {{ val4 ? val4.value : '' }}
      <div>
      <button v-if="val4" @click="bindVal4">bind</button>
      <button v-else @click="unbindVal4">unbind</button>
      </div>
    </div>
  </div>
</template>
<script>
  // 绑定方法到Vue原型链中 (可选)
  Vue.prototype.$computed = function () {
  return createVueComputed(this, ...arguments)
  }
  Vue.prototype.$setComputed = function () {
  return setVueInstanceComputed(this, ...arguments)
  }
  export default {
  data() {
  return {
  val1: 100,
  val2: 200,
  val3: 'test'
  }
  },
  created() {
  // 在 created 周期内且当前为根实例时 (this === this.$root),需嵌套在 this.$nextTick 下使用。
  // 在 created 周期内且当前不为根实例或 mounted 周期内,可同步执行。
  const initComputed = () => this.$setComputed({
  // 基本设置方式
  demo1() {
  let res = this.val1 + 1
  console.log(`val1 get: ${res}`)
  return res
  },
  // 带配置设置方式,与 computed 配置一致
  demo2: {
  cache: false,
  get() {
  let res = this.val2 + 2
  console.log(`val2 get: ${res}`)
  return res
  },
  set(newVal) {
  this.val2 = newVal
  console.log(`val2 set: ${newVal}`)
  }
  },
  // 可以覆盖实例下原有的 data, props, computed, methods 内的属性
  val3: {
  // 第一参数为原来的取值方法
  get(rawGet) {
  let res = `x_${rawGet()}`
  console.log(`val3 get: ${res}`)
  return res
  },
  // 第一参数为设置的新值,第二参数为原来的设值方法
  set(newVal, rawSet) {
  rawSet((newVal = newVal.replace(/^x_/, '')))
  console.log(`val3 set: ${newVal}`)
  }
  }
  })
  this === this.$root ? this.$nextTick(initComputed) : initComputed()
  },
  methods: {
  bindVal4() {
  // 自由设置计算属性
  this.val4 = this.$computed(() => {
  let res = this.val1 + this.val2
  console.log(`val4 get: ${res}`)
  return res
  })
  },
  unbindVal4() {
  // 销毁设置的计算属性
  this.val4.effect.teardown()
  }
  }
  }
</script>