目录
- computed 用法
- computed 实现
- computed 初始化
- computed 获取值的实现
- 值的展示
- 缓存功能
- computed 设置值实现
computed 用法
本文给大家带来的是vue3 中 computed API的实现。
大家看过vue3的官网,应该都知道,在vue3 的组合式API中,computed这个功能与以往的有所不同了。
以往vue2 的 computed 用法:
export default {
components: {
},
computed: {
people() {
return '这个人' + this.age + '岁'
}
},
watch() {
},
data() {
return {
age: 1
}
}
}
现在我们大多数情况使用的是组合式 API,可以直接使用computed函数来进行属性的监听。
比如官网的这种案例写法:
const count = ref(1) const plusOne = computed(() => count.value + 1) // 只读属性
console.log(plusOne.value) // 2
vue3 中的 computed 接受一个 getter
函数,返回一个响应式的ref
对象,并且该对象是只读属性。读取该属性通过 .value
的形式来对外暴露该属性的值。
此外,如果想要修改该对象,可以传入一个带有 get
和 set
函数的对象,通过get
和 set
分别读取和设置值。
我们来看看这样的一个简单的computed用法:
<div id="app"></div>
<!-- <script src="./reactivity.global.js"></script> -->
<script src="../../../../node_modules/@vue/reactivity/dist//reactivity.global.js"></script>
<script>
const { reactive, effect, computed } = VueReactivity
const author = reactive({
name: 'clying',
sex: '女'
})
// 用法一:
// const introduction = computed({ // 调用的是defineProperty 中的
// // getter
// get() {
// console.log('run get');
// return author.name + '是个' + author.sex + '程序员!'
// },
// // setter
// set(newValue) {
// // console.log(newValue);
// // 注意:我们这里使用的是解构赋值语法
// [author.name, author.sex] = newValue.split(' ')
// }
// })
// 用法二:
// 一个计算属性 ref
const introduction = computed(() => {
console.log('run get');
return author.name + '是个' + author.sex + '程序员!'
})
effect(() => {
app.innerHTML = introduction.value // computed通过 .value 读取
})
introduction.value
introduction.value
introduction.value
</script>
在上述案例中,我们可以很容易就知道,不管读取introduction
几次,只要值未发生改变,始终只会输出一次run get
。
computed 实现
那么既然使用vue可以实现,是不是我们也可以自己去搞一个叻?
其实,computed 也是基于effect这个功能函数来实现的。我们可以在我们完成的effect功能上继续扩展。
computed 初始化
一步步来,我们先来实现computed的读取值功能。
const introduction = computed({ // 调用的是defineProperty 中的
// getter
get() {
console.log('run get');
return author.name + '是个' + author.sex + '程序员!'
},
// setter
set(newValue) {
console.log(newValue);
// 注意:我们这里使用的是解构赋值语法
[author.name, author.sex] = newValue.split(' ')
}
})
使用原有的 computed API可以发现,计算属性introduction
其实是一个ComputedRefImpl
类。
那么我们要做的就是先初始化一个类,然后对外暴露其实例。computed可以接收两种写法,那么我们在接收参数的时候,需要判断下用户传入的是对象还是一个回调函数。
- 如果接收的是一个回调函数,那么就只存在取值的
get
,无法设置值; - 如果接收的是一个对象,那么就应该存在取值的
get
和设置值的set
。
export const computed = (getterOrOptions) => {
let onlyGetter = isFunction(getterOrOptions) // 用户传入的回调
let getter
let setter
if (onlyGetter) {
getter = getterOrOptions
setter = () => {
console.warn('no set');
}
} else { // 用户传入的包含get set函数的对象
getter = getterOrOptions.get
setter = getterOrOptions.set
}
// ref 引用类型
return new ComputedRefImpl(getter, setter)
}
对外暴露的是ComputedRefImpl
实例,那么我们初始化还需要在创建一个ComputedRefImpl
的类。
ComputedRefImpl
类中应该是一个effect,并且应该可以设置个读取相关的属性。在属性访问器get和set中,它们操作的需要是同一个值,所以我们还需要两个属性访问器操作的同一个值_value
,且是私有属性。
ps:类中的属性访问器get和set,底层调用的是Object.defineProperty
。
class ComputedRefImpl {
public readonly effect
private _value// get和set需要使用的同一个值
constructor(getter, private readonly _setter) {
}
get value() {
return this._value
}
set value(newValue) {
this._setter(newValue)
}
}
computed 获取值的实现
值的展示
在我们完成初始化之后,页面上其实是不会存在我们期望的内容的。它其实是这样的一个页面:
必然。我们还没有将用户传入的逻辑进行处理,所以根本看不到呀!
那我们现在要做的就是先将数据展示到页面上。那我们就先将其effect出来,在读取值的时候执行effect。
class ComputedRefImpl {
public readonly effect
private _value// get和set需要使用的同一个值
constructor(getter, private readonly _setter) {
this.effect = new ReactiveEffect(getter, () => { })
}
get value() {
this._value = this.effect.run() // 执行
return this._value
}
set value(newValue) {
this._setter(newValue)
}
}
这样页面的数据就可以正常展示了。
but,我们却忽略了一个问题,在多次读取时,其实并没有computed的缓存功能,只是effect正常获取某个值,获取一次执行一次。
缓存功能
那么,我们要做的就是继续完善computed
的缓存功能。
既然computed
也是响应式的,那么它是一个effect,同时肯定也需要有一个缓存标识,控制它的缓存特性。如果依赖的属性发生变化,那么这个缓存标识会更新,重新执行get,没有就不重新执行。
定义一个_dirty
,默认应该取值的时候进行计算。一开始为true,默认为新值,更新。当执行完更新的时候应该将其置称false,为false时就默认不执行更新。
那么除了需要在get读取值的时候进行判断,我们还需要依赖属性发生变化的时候,再去进行判断重新渲染,即在构造函数传入我们的一个调度回调scheduler
(我们上篇文章实现的调度函数,在依赖属性发生变化的时候,会执行我们传入的回调函数)。
class ComputedRefImpl {
public readonly effect
private _value// get和set需要使用的同一个值
public _dirty = true // 默认应该取值的时候进行计算
constructor(getter, private readonly _setter) {
this.effect = new ReactiveEffect(getter, () => {
//稍后依赖的属性变化 就会执行此调度函数
if (!this._dirty) {
this._dirty = true
}
})
}
get value() {
if (this._dirty) {
// 脏数据
this._dirty = false
this._value = this.effect.run() // 执行
}
return this._value
}
set value(newValue) {
this._setter(newValue)
}
}
我们可以看到,此时introduction
值未发生变化,就只会执行一次effet。
computed 设置值实现
修改值,我们可以通过上述用法一来传入一个带有set和get函数的对象,在设置值的时候通过解构的方式进行赋值。
在 computed 中的set时,我们可以拿到computed的新值,但并没有重新渲染更新页面。此时我们需要做的就是将computed相关的属性进行依赖的收集,并在发生变化的时候进行相应的依赖触发。与effect类似。
既然类似effect,那我们就需要一个存放依赖的dep(在类中定义public dep = undefined
),get时收集,属性变化时触发。
// ComputedRefImpl 类
get value() {
trackEffects(this.dep || (this.dep = new Set())) // 收集
if (this._dirty) {
// 脏数据
this._dirty = false
this._value = this.effect.run() // 执行
}
return this._value
}
// ComputedRefImpl 类
constructor(getter, private readonly _setter) {
this.effect = new ReactiveEffect(getter, () => {
//稍后依赖的属性变化 就会执行此调度函数
if (!this._dirty) {
this._dirty = true
// 触发更新
triggerEffects(this.dep)
}
})
}
其中收集trackEffects
和触发triggerEffects
就是将effect中的track
和trigger
中部分共用功能提取出来了。
比如收集的这个功能trackEffects
。与effect类似,我们都需要将记录相应的依赖属性和依赖属性的effect,那我们就可以将其独立成一个新的功能函数,降低耦合度。
export function track(target, type, key) {
// 收集effect中 属性对应的effect
if (!activeEffect) return
let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
let dep = depsMap.get(key)
if (!dep) { depsMap.set(key, (dep = new Set())) }
// 判断去重 set中是否存在activeEffect
trackEffects(dep)
}
// 将set存起来
export function trackEffects(dep) {
if (!activeEffect) return
let shouldTrack = !dep.has(activeEffect) // 不存在
if (shouldTrack) {
dep.add(activeEffect) // 属性记录effect
// 反向记录 effect记录哪些属性收集过
activeEffect.deps.push(dep) // 让activeEffect 记录住对应的dep 稍后清理会用到
}
}
triggerEffects
也是如此:
export function trigger(target, type, key, value, oldValue) {
// 判断targetMap是否存在target
// 不存在 直接返回 不需要收集
// 存在 取depsMap中对应key的effect 执行run
const depsMap = targetMap.get(target)
if (!depsMap) return
let effects = depsMap.get(key)
if (effects) {
triggerEffects(effects)
}
}
export function triggerEffects(effects) {
effects = [...effects] // effects 中 set结构删除再添加会导致死循环
effects.forEach(effect => {
// 在执行effect时,又要执行自己,需要屏蔽自己的effect
if (effect !== activeEffect) {
if (effect.scheduler)
effect.scheduler() // 如果存在自己的调度函数就执行自己的scheduler
else effect.run() // 否则就执行run
}
});
}
上述只是vue3中computed的简版实现方式,源码中比我们实现的考虑的要多很多,有兴趣的可以自己去看看(源码路径:packages/reactivity/src/computed.ts):
最后,我们也可以看到,页面上过了1秒之后,相应的属性变化,页面也同样发生了变化🧐🧐🧐。
其实,computed
的核心就是effect + 缓存 + 依赖收集、触发