Appearance
计算属性实现
本节目标:理解“计算属性实现”的核心思路,并能把它用于实际开发或面试表达。 计算属性的特点:缓存,如果依赖项没有变化,不会重新运行;所以计算属性中有一个缓存的标识,如果这个依赖有变化,要重新执行get,没有变化就不重新执行get; 这个属性是_dirty:是否是脏的;
计算属性是一个effect,依赖的属性变化了,会更新dirty的值; 计算属性可以放到一个effect中使用;
当计算属性依赖的值变化了,get函数会执行,同时依赖于计算属性的effect也会执行;
依赖的值变化了会触发计算属性effect重新执行, 计算属性重新执行的时候会触发外层effect来执行;
计算属性套计算属性也是一样的;新建文件reactivity/src/computed.ts
import { isFunction } from "@vue/shared"
import { ReactiveEffect, trackEffects } from "./effect";
export const computed = (getterOrOptions) => {
// 整理参数;
// 先定义两个变量存储我们最终需要的值;
// 根据用户传入的参数的不同,给予相应赋值;
let getter;
let setter;
if (isFunction(getterOrOptions)) {
getter = getterOrOptions;
setter = () => { console.warn('no set') }
} else {
getter = getterOrOptions.get;
setter = getterOrOptions.set;
}
// 调用computed函数实际上是创建一个ReactiveEffect实例,它不同于effect函数,默认不会执行;
// effect函数默认会执行一次,即创建一个ReactiveEffect实例,然后调用实例run方法;
// ReactiveEffect构造函数接受一个函数和一个scheduler作为参数;
return new ComputedRefImpl(getter, setter);
}到此,computed()执行完毕;也就是说如果我们不对computed()进行取值或赋值操作,computed的get和set是不会再执行的;实际工作中,计算属性如果没有被使用,是不会执行的;比如在模板中使用,且模板默认的v-if条件是true,如果是false,也相当于没有使用;
在packages/shared/src/index.ts中添加几个工具方法
export const isObject = (value) => {
return typeof value === 'object' && value !== null;
}
export const isString = (value) => {
return typeof value === 'string'
}
export const isNumber = (value) => {
return typeof value === 'number'
}
export const isFunction = (value) => {
return typeof value === 'function'
}
export const isArray = Array.isArray;
export const assign = Object.assigncomputed返回的computedRefImpl实例只有一个属性,这个属性是访问器属性,只有get和set方法来定义对值的访问和修改; 当在计算属性上取Value属性时,会触发ComputedRefImpl实例的get value方法,在这个方法中,我们调用我们创建的ReactiveEffect的run方法,就是执行一下getter函数,这里会进行依赖收集,当前effect就是使用计算属性value的effect;我们把这个effect放到了计算属性实例的deps上,下次,我们在set方法中并这些effect依次执行;
class ComputedRefImpl {
// 放在实例上便于调用
public effect;
public _dirty = true;
public __v_readonly = true;
public __v_isRef = true;
public _value;
// 默认使用空Set
public dep = new Set();
constructor(getter, public setter) {
this.effect = new ReactiveEffect(getter, () => {
// 使用了调度器,当依赖项变化时,执行这个调度器函数
})
}
// 类中的属性访问器,底层就是Object.defineProperty
// 计算属性使用的时候是使用.value来使用的,所以使用计算属性的地方
// 会调用这个实例的get方法,在get方法中,我们调用实例上的effect的run方法
// 这个run方法会进行依赖收集,
// 也就是说取计算属性的value属性,会调用我们当初传入的getter函数;
get value() {
// 做依赖收集
trackEffects(this.dep);
// 使用_dirty来进行缓存逻辑;
if (this._dirty) {
this._dirty = false;
this._value = this.effect.run();
}
return this._value;
}
set value(newValue) {
// 直接调用setter
this.setter(newValue)
}
}我们执行计算属性的getter函数时,已经进行了依赖收集;现在,我们修改计算属性的依赖项,自然会让ComputedRefImpl的effect这个effect重新执行getter函数,但由于有调度器的存在,我们执行的是我们创建effect时传入的调度器函数;
让依赖计算属性的effect,在计算属性变化时,能再执行,其它非effect实现,自然不用更新; 所以,我们在get value()方法中,进行依赖收集,如同我们在new Proxy的get方法中所做的一样;目的是一样的,让value记住effect,然后设置value的值时,重新执行effect。
我们将这个effect存在了实例的dep属性上,它的值是一个Set,里面存放了它影响到的effect;
get value() {
// 做依赖收集
==trackEffects====(====this====.====dep====);==
// 使用_dirty来进行缓存逻辑;
if (this._dirty) {
this._dirty = false;
this._value = this.effect.run();
}
return this._value;
}class ComputedRefImpl {
// 放在实例上便于调用
public effect;
public _dirty = true;
public __v_readonly = true;
public __v_isRef = true;
public _value;
==//== ==默认使用空====Set==
==public== ==dep== ===== ==new== ==Set====();==trackEffects方法
export function trackEffects(dep) {
if (activeEffect) {
let shouldTrack = !dep.has(activeEffect); // 去重了
if (shouldTrack) {
dep.add(activeEffect);
// 存放的是属性对应的set
activeEffect.deps.push(dep);
}
}
}接下来,当计算属性值变化时,我们要让dep里的effect执行;
constructor(getter, public setter) {
this.effect = new ReactiveEffect(getter, () => {
// 使用了调度器,当依赖项变化时,执行这个调度器函数
// 重置
if (!this._dirty) {
this._dirty = true;
==//== ==实现触发更新==
==triggerEffects====(====this====.====dep====)==
}
})
}triggerEffects代码
export function triggerEffects(effects) {
effects = new Set(effects);
effects.forEach(effect => {
// 我们在执行effect的时候,又要执行自己,那我们需要屏蔽掉,不要无限调用
if (effect !== activeEffect) {
if (effect.scheduler) {
// 如果用户传入了调度函数,则用用户的
effect.scheduler();
} else {
effect.run() // 否则默认刷新视图
}
}
});
}<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script src="./reactivity.global.js"></script>
<div id="app"></div>
<script>
let waiting = false;
const { effect, reactive, computed } = VueReactivity;
const state = reactive({ firstname: '姜', lastname: '文' });
const fullName = computed(() => {
console.log('runner')
return state.firstname + state.lastname
})
effect(() => {
app.innerHTML = fullName.value
})
setTimeout(() => {
state.firname = '珠'
}, 1000)
</script>
</body>
</html>