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 { get, set, cache } = typeof getterOrOptions == 'function' ? { get: getterOrOptions } : getterOrOptions
  cache = get ? cache ?? true : false
  let watcher = new Watcher(vm, get || noop, noop, { lazy: true })
  return Object.defineProperties(Object.create(null), {
    value: {
      configurable: true,
      enumerable: true,
      set: set ? set.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
      } : get || noop
    },
    effect: { configurable: true, enumerable: true, value: watcher },
    __v_isRef: { configurable: true, value: true },
    __v_isReadonly: { configurable: true, value: !set }
  })
}

/**
 *  设置Vue计算属性
 *  @param  {Vue}     vm    Vue实例
 *  @param  {Object}  data  设置的对象
 *  @param  {Object}  props 计算属性表 { [key]: Function | { get(rawGet), set(newVal, rawSet), cache, configurable, enumerable } }
 *  @return {Object}        处理后的对象源
 */
function setVueComputeProps(vm, data, props) {
  data ||= {}
  let noop = createVueComputed.noop ||= function () {}
  let getDesc = Object.getOwnPropertyDescriptor
  let proto = Object.getPrototypeOf(data)
  let watchers = vm === data ? vm._computedWatchers ||= Object.create(null) : null
  for (let key in props) {
    let { get, set, cache, configurable, enumerable } = typeof props[key] == 'function' ? { get: props[key] } : props[key]
    let desc = getDesc(data, key) || proto && getDesc(proto, key) || {}
    cache = get ? cache ?? true : false
    configurable ??= desc.configurable ?? true
    enumerable ??= desc.enumerable ?? true
    let rawGet = desc.get?.bind(data), rawSet = desc.set?.bind(data)
    let useGet = get ? function () { return get.call(this, rawGet) } : noop
    let useSet = set ? function (val) { set.call(this, val, rawSet) } : noop
    let ref = cache ? createVueComputed(vm, useGet) : null
    if (watchers) watchers[key]?.teardown(), cache ? (watchers[key] = ref.effect) : delete watchers[key]
    Object.defineProperty(data, key, {
      configurable, enumerable, set: useSet,
      get: cache ? getDesc(ref, 'value').get : useGet
    })
  }
  return data
}

// 可绑定到Vue原型链中使用
Vue.prototype.$computed = function () {
  return createVueComputed(this, ...arguments)
}
Vue.prototype.$setComputed = function () {
  return setVueComputeProps(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 setVueComputeProps(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(this, {
      // 基本设置方式
      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>
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐