概述
vue3中,computed
函数用于表示计算属性,有惰性求值、响应式追踪依赖的特点。本文将介绍computed
的实现原理以及其机制细节。
源码解析
computed
计算属性和computed
方法、ComputedRefImpl
类以及refreshComputed
方法有关。
computed
方法
computed
暴露给外部的就是computed
方法,其源码实现如下:
js
function computed(getterOrOptions, debugOptions, isSSR = false) {
let getter;
let setter;
if (shared.isFunction(getterOrOptions)) {
getter = getterOrOptions;
} else {
getter = getterOrOptions.get;
setter = getterOrOptions.set;
}
const cRef = new ComputedRefImpl(getter, setter, isSSR);
return cRef;
}
computed
方法实现比较简单,需要关注参数getterOrOptions
和isSSR
,isSSR
默认为false
,它在服务端渲染会传值为true
。debugOptions
用以在开发环境调试。
computed
会先判断getterOrOptions
是否是函数,若是函数,则将其赋值给getter
;当然getterOptions
也可以是一个包含get
和set
方法的对象。computed
方法返回的是ComputedRefImpl
实例,一般我们读取计算属性的值也是读取它的返回值的.value
。
ComputedRefImpl
类
ComputedRefImpl
用于构造一个计算属性。
ComputedRefImpl
的源码实现如下:
js
class ComputedRefImpl {
constructor(fn, setter, isSSR) {
this.fn = getter; //计算函数
this.setter = setter; // 设置函数(可选)
this["_value"] = undefined; // 缓存的结果,计算属性的值
this.dep = new Dep(this); // 依赖收集器(收集依赖此计算属性的副作用effect)
this["__v_isRef"] = true; // 表示为ref类型
this["__v_isReadonly"] = undefined; // 只读标记
this.deps = undefined; //当前计算属性依赖的响应式集合对象链表头
this.depsTail = undefined; //链表尾
this.flags = 16; //状态标记
this.globalVersion = globalVersion - 1;// 全局版本号,用于脏检查
this.isSSR = undefined; //服务端渲染标记
this.next = undefined; //用于在effect链表中指向下一个节点
this.effect = this; // 指向自身
this["__V_isReadonly"] = !setter; //若无setter,则表示计算属性是只读的
this.isSSR = isSSR;//ssr标记赋值
}
// 依赖变更调用
notify() {
this.flags |= 16;
if (!(this.flags & 8) && activeSub !== this) {
batch(this, true);
return true;
}
}
// 计算属性值访问器
get value() {
const link = this.dep.track()
refreshComputed(this);
if (link) {
link.version = this.dep.version;
}
return this._value;
}
// 计算属性值设置器
set value(newValue) {
if (this.setter) {
this.setter(newValue)
} else {
warn("Write operation failed: computed value is readonly");
}
}
}
ComputedRefImpl
中除了在构造器中定义了相关属性外,就是包含两个属性访问器函数和一个notify
方法
notify
当计算属性依赖的响应式值发生变化时,会调用notify
方法.notify
方法会先设置this.flags
标志位的值,将其第4位置为1 ,表示有更新请求;然后判断标志位的第3位是否为1 并且当前激活订阅(副作用)是不是自身,若条件满足,则调用batch
方法,将该计算属性添加到更新队列中,进行批量更新,最后返回true
,表示更新已调度;若不满足条件,则返回false
,表示更新被跳过。
getter
getter
属性访问器,会在读取计算属性的值时触发。先进行依赖收集,追踪当前正在运行的effect
,然后调用refreshComputed
方法进行有条件性的重新计算,若当前计算属性被其他effect
依赖,则更新依赖的版本,最后返回this._value
。
setter
setter
属性设置,一般情况下计算属性只是只读的,若this.setter
方法存在,则可以调用该方法设置计算属性的值。
refreshComputed
方法
refreshComputed
方法就是用于进行刷新计算属性的值,满足条件就重新进行计算,得到最新的计算属性的值。
refreshComputed
的源码实现如下:
js
function refreshComputed(computed) {
// 检查更新条件
if (computed.flags & 4 && !(computed.flags & 16)) {
return;
}
// 清除pending状态
computed.flags & =-17;
// 全局版本校验 避免重复计算
if (computed.globalVersion === globalVersion) {
return;
}
computed.globalVersion = globalVersion;
// 缓存有效性检查
if (!computed.isSSR && computed.flags & 128 && (!computed.deps && !computed._dirty || !isDirty(computed))) {
return;
}
// 标记计算状态
computed.flags |= 2;
const dep = computed.dep;
const prevSub = activeSub;
const prevShouldTrack = shouldTrack;
activeSub = computed;
shouldTrack = true;
try {
// 依赖收集准备
prepareDeps(computed);
// 执行计算函数
const value = computed.fn(computed._value);
// 值变化检测
if (dep.version === 0 || hasChanged(value, computed._value)) {
computed.flags |= 128; // 标记valid
computed._value = value;
dep.version++; // 触发依赖更新
}
} catch (err) {
dep.version++;
throw err;
} finally {
activeSub = prevSub;
shouldTrack = prevShouldTrack;
cleanupDep(computed); // 清理过期依赖
computed.flags &= -3; // 清除computing状态
}
}
refreshComputed
方法会先检查flags
的值,若是被移除或者没有更新请求,则直接返回;然后修改flags
状态,清除pending
状态;比较全局版本号和计算属性的版本号,若二者一样,则返回,避免是在计算属性中修改了响应式属性引起的重新计算;修改响应式的版本号;然后做缓存有效性的检查;若是脏数据,则返回;再次标记flags
状态,表示是计算中;将当前effect
计算属性切换为activeSub
,修改shouldTrack
为true
,调用prepareDeps
进行依赖收集,然后执行计算属性的fn
,即传入computed
的参数函数,得到新的value
,比较计算属性的值是否发生改变,赋值this._value
,并将其依赖dep
的版本递增,如此会触发依赖该计算属性的副作用effect
更新;最后恢复activeSub
和shouldTrack
,清理过期依赖以及清除计算状态。
refreshComputed
是计算属性computed
的核心方法,依据一些规则判断需要执行fn
,获取最新的value
以及触发相关依赖。