Gadzan

Vue3 如何判断一个变量为 Computed

Ref 与 Computed 都是通过 .value 去获取值的,那么是否可以用 isRef 可以判断是否 Computed 变量?

不行。

Ref 在 Vue3 中是通过 ReactiveEffect 类实现的。其对象内部主要包含以下属性:

  • value - 存储的响应式数据值
  • _shallow - 是否为浅层副作用
  • _isRef - 标识这是一个 ref 对象
  • _rawValue - 存储的原始值,未被代理时的值

通过副作用属性判断

Computed 在内部使用 effect 实现缓存。所以它会有一个 active 属性。
而 Ref 对象内部并不存在一个名为 effect 的属性。

通过判断是否存在 effect.active 属性,就可以判断一个值是否为 Computed。

import { computed } from 'vue';

const count = ref(1);
const plusOne = computed(() => count.value + 1);

function isComputed(value) {
  return !!value.effect?.active;
}

console.log(isComputed(count)); // false
console.log(isComputed(plusOne)); // true

另外,也可以利用这个特性,实现一个计算属性的 watchEffect:

function watchComputed(computed) {
  watchEffect(onInvalidate => {
    // 在计算属性失效时重新执行
    if (!computed.effect.active) {
      onInvalidate();
    }
  })  
}

这样就可以监听一个计算属性何时被破坏,从而重新执行副作用函数。

但是这个方法似乎不太可靠,原因如下:

  1. 计算属性在初始化时,effect 可能还未生成。这时判断 effect.active 会不准确。
  2. 计算属性在组件 setup() 内部定义时,访问不到 effect 对象。setup() 内部的 Ref 和 computed 是用 reactive() 包装的,访问不到原始的 effect
  3. 即使能获取到 effectactive 也不一定为 true。当计算属性被停止跟踪时,active 也会被设置为 false
  4. 在 vue3 中,effect 的实现比较复杂,可能不是一个 effect,而是多个 effect 嵌套。直接判断 active 不够准确。
  5. 计算属性也可能被其他库如 Pinia 实现和包装,不一定遵循 vue 原始的实现。

所以直接通过判断 effect.active 的方式来判断计算属性,实际上是不够可靠和准确的。

ComputedRefIml 实现类

当我们用 console.log 打印一个 computed 的变量时,会看到这个变量的类型是 ComputedRefIml

在 Vue 3 中,计算属性的实现是 ComputedRefImpl 类。

因此使用 instanceof ComputedRefImpl 可以准确地判断一个变量是否为 computed :

import { computed } from 'vue';

const count = ref(1);
const plusOne = computed(() => count.value + 1);

console.log(plusOne instanceof ComputedRefImpl); // true

之所以这种方式可靠,是因为:

  1. ComputedRefImpl 是计算属性的具体实现类,直接判断实例类型可以避免上面提到的 effect 判断的种种问题。
  2. ComputedRefImpl 是 Vue 核心代码的一部分,不太可能被其他库改变实现。
  3. 就算 computed 被其他库代理和包装,只要原始实现不变,实例还是 ComputedRefImpl 类型。
  4. 这个方式简单易用,只要引入 ComputedRefImpl 类即可判断。

如何获取 ComputedRefIml 实现类

但问题来了,ComputedRefImpl 类其实是 Vue 内部的实现, 并没有直接暴露出来。

但我们可以在 Vue 源码中直接引用

import { ComputedRefImpl } from 'vue/dist/vue.runtime.esm-bundler';

这种办法在使用 TypeScript 时会提示类型无法获取的错误,因此不推荐;

我们知道一个类可以通过它的实例的 constructor 去获取;

那么我们可以创建一个计算属性实例后直接获取构造函数

const tmp = computed(() => {});
const ComputedRefImpl = tmp.constructor;

但这种方式去获取 ComputedRefImpl,eslint 会报错;

一个对象的 constructor 位于它原型链中,获取原型我们可以通过 getPrototypeOf 方法:

Object.getPrototypeOf(tmp);

输出:

那么,经过整合,我们可以用下面的办法去获取 Computed 的实现类;

const ComputedRefImpl = Object.getPrototypeOf(computed(() => {})).constructor

或者通过 Reflect 获取

const ComputedRefImpl = Reflect.getPrototypeOf(computed(() => {})).constructor;

在不需要考虑兼容老旧浏览器的情况,请尽量使用 Reflect.getPrototypeOf 的方法,因为对比 Object.getPrototypeOf ,利用 Reflect 的实现在所有环境下的表现都是一致的。

  1. 在某些老版本浏览器中,Object.getPrototypeOf(null) 会返回 null 或者抛出错误,而 Reflect.getPrototypeOf(null) 会一致地返回 null
  2. Object.getPrototypeOf() 在某些情况下无法获取由 Object.create(null) 创建的对象的原型,而 Reflect.getPrototypeOf() 可以正常获取。
  3. 如果参数传入的不是一个对象, Object.getPrototypeOf 可能只是简单地返回传入的参数,而 Reflect.getPrototypeOf 会抛出错误。
  4. 在一些环境下,修改 Object.prototype.getPrototypeOf 方法可能会影响 Object.getPrototypeOf() 的行为,但不会影响 Reflect.getPrototypeOf
  5. 一些早期 JavaScript 引擎在获取不存在的属性的原型时, Object.getPrototypeOf 可能失败或者静默处理,而 Reflect.getPrototypeOf 会抛出错误。
  6. Object.getPrototypeOf() 可能无法处理某些特殊的对象,如Proxy对象。而Reflect.getPrototypeOf() 可以正常处理。

需要注意的是, 这种方式依赖了 Vue 内部的实现,如果 computed 的实现发生改变,就需要对应调整判断逻辑。但通常这种核心实现不太可能频繁修改。


打赏码

知识共享许可协议 本作品采用知识共享署名 4.0 国际许可协议进行许可。

评论