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();
}
})
}
这样就可以监听一个计算属性何时被破坏,从而重新执行副作用函数。
但是这个方法似乎不太可靠,原因如下:
- 计算属性在初始化时,
effect
可能还未生成。这时判断effect.active
会不准确。 - 计算属性在组件
setup()
内部定义时,访问不到effect
对象。setup()
内部的 Ref 和computed
是用reactive()
包装的,访问不到原始的effect
。 - 即使能获取到
effect
,active
也不一定为true
。当计算属性被停止跟踪时,active
也会被设置为false
。 - 在 vue3 中,
effect
的实现比较复杂,可能不是一个effect
,而是多个effect
嵌套。直接判断active
不够准确。 - 计算属性也可能被其他库如 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
之所以这种方式可靠,是因为:
ComputedRefImpl
是计算属性的具体实现类,直接判断实例类型可以避免上面提到的effect
判断的种种问题。ComputedRefImpl
是 Vue 核心代码的一部分,不太可能被其他库改变实现。- 就算
computed
被其他库代理和包装,只要原始实现不变,实例还是ComputedRefImpl
类型。 - 这个方式简单易用,只要引入
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
的实现在所有环境下的表现都是一致的。
- 在某些老版本浏览器中,
Object.getPrototypeOf(null)
会返回null
或者抛出错误,而Reflect.getPrototypeOf(null)
会一致地返回null
。Object.getPrototypeOf()
在某些情况下无法获取由Object.create(null)
创建的对象的原型,而Reflect.getPrototypeOf()
可以正常获取。- 如果参数传入的不是一个对象,
Object.getPrototypeOf
可能只是简单地返回传入的参数,而Reflect.getPrototypeOf
会抛出错误。- 在一些环境下,修改
Object.prototype.getPrototypeOf
方法可能会影响Object.getPrototypeOf()
的行为,但不会影响Reflect.getPrototypeOf
。- 一些早期 JavaScript 引擎在获取不存在的属性的原型时,
Object.getPrototypeOf
可能失败或者静默处理,而Reflect.getPrototypeOf
会抛出错误。Object.getPrototypeOf()
可能无法处理某些特殊的对象,如Proxy对象。而Reflect.getPrototypeOf()
可以正常处理。
需要注意的是, 这种方式依赖了 Vue 内部的实现,如果 computed
的实现发生改变,就需要对应调整判断逻辑。但通常这种核心实现不太可能频繁修改。