回顾
effect 函数
主要作用是用来注册副作用函数,同时也允许指定一些选项参数
options
,例如指定scheduler
调度器来控制副作用函数的执行时机和方式;还有像收集依赖的track
函数和触发依赖(副作用)的trigger
函数;
但是常规的effect
函数会立即执行
传递给他的副作用函数,有时需要进行进行懒执行的操作,可以通过options
进行相关的配置来实现,如options.lazy配置实现
懒执行``
参考文献
Vue3 响应式 effect 拓展
Vue3 响应式 reactive、ref、effect 实现浅析
computed 计算属性
功能概述
接受一个 getter 函数,返回一个只读的响应式 ref对象。该 ref 通过 .value
暴露 getter 函数的返回值。它也可以接受一个带有 get
和 set
函数的对象来创建一个可写的 ref 对象。
- 创建一个只读的计算属性 ref
const count = ref(1)
const plusOne = computed(() => count.value + 1)
console.log(plusOne.value) // 2
plusOne.value++ // 错误
- 创建一个可写的计算属性 ref
const count = ref(1)
const plusOne = computed({
get: () => count.value + 1,
set: (val) => {
count.value = val - 1
}
})
plusOne.value = 1
console.log(count.value) // 0
-
内部逻辑
computed
会对fn进行缓存,只有内容发生变化了,且调用了computed
的返回值.value
才会去执行fn
- 需要通过
.value
来进行访问,可以用class
类对value
的get
和set
进行代理拦截 - 依赖收集还是在get中进行收集,触发可以通过
effect class
中的scheduler
配置中触发; - 缓存问题可以通过
flag
来实现
- 需要通过
-
需求分析
- computed接收一个fn函数,且初始化时不执行,最后函数返回一个带有
.value
的对象 - 第一次调用
.value
,fn会执行一次,等后续调用不会再执行 - 改变fn依赖的响应式对象的值时,fn还是不执行
- 当再次调用
.value
的时候,fn会再次执行
懒加载和缓存的特性
- computed接收一个fn函数,且初始化时不执行,最后函数返回一个带有
computed
会对fn
进行缓存,只有内容变化了,且调用了computed
的返回值的.value
的是才会去执行fn
- 使用方式
- 传入一个fn或是自定义的
get
和set
,返回一个对象,并且需要使用.value
来调用里面的内容
- 传入一个fn或是自定义的
测试用例
import { computed } from "./computed";
import { reactive } from "./reactive";
describe('computed', () => {
it('happy path', () => {
// 类似ref 采用.value获得值
// 具有缓存功能
const user = reactive({
name: 'Lbxin'
})
const name = computed(()=>{
return user.name
})
expect(name.value).toBe('Lbxin')
});
it('should computed lazily', () => {
const user = reactive({
name: "Lbxin"
})
// jest.fn 由于用到了 computed的get属性 需要对getter进行验证 所以用jest.fn进行操作
const getter = jest.fn(()=>{
return user.name
})
const name = computed(getter)
expect(getter).not.toBeCalled()
// name.value触发get操作 进而getter被触发 只不过是有缓存
expect(name.value).toBe('Lbxin')
expect(getter).toBeCalledTimes(1)
name.value // 再次触发 get操作 computed具有缓存功能 getter 还是被调用了一次
expect(getter).toBeCalledTimes(1)
user.name = "Lbxin12" //响应式对象在set的时候回触发set中的trigger -> effect -> get 重新执行
expect(getter).toBeCalledTimes(1)
expect(name.value).toBe('Lbxin12')
expect(getter).toBeCalledTimes(2)
user.name = "Lbxin12" //响应式对象在set的时候回触发set中的trigger -> effect -> get 重新执行
expect(getter).toBeCalledTimes(2)
});
});
内部实现
import { createDep } from "./dep";
import { ReactiveEffect } from "./effect";
import { trackRefValue, triggerRefValue } from "./ref";
export class ComputedRefImpl {
public dep: any; // 收集getter的依赖
public effect: ReactiveEffect;
private _dirty: boolean;
private _value
constructor(getter) {
this._dirty = true;
this.dep = createDep();
// 实例化ReactiveEffect对象 当触发响应式对象的getter的时候 会将getter作为依赖收集起来 - 同时解决了设置依赖值时找不到依赖的问题
this.effect = new ReactiveEffect(getter, () => {
// scheduler
// 只要触发了这个函数说明响应式对象的值发生改变了
// 那么就解锁,后续在调用 get 的时候就会重新执行,所以会得到最新的值
if (this._dirty) return;
this._dirty = true;
// 触发依赖 triggerEffect(this.deps)
triggerRefValue(this);
});
}
get value() {
// 收集依赖
trackRefValue(this);
// 锁上,只可以调用一次
// 当数据改变的时候才会解锁
// 这里就是缓存实现的核心
// 解锁是在 scheduler 里面做的
// 当value被更改的时候 期望this._dirty需要改为true -
// 在依赖的响应式对象的值发生改变时 需要依赖于 effect
if (this._dirty) {
// 触发过一次就可以
this._dirty = false;
// this._value = this._getter() 不能采用这种方式进行重新计算 无法收集依赖 而是通过收集的依赖中的run方法间接执行
// 这里执行 run 的话,就是执行用户传入的 fn
this._value = this.effect.run(); //在调用run的时候会重新执行缓存的依赖fn 即传入的getter
}
//实现缓存
return this._value;
}
}
export function computed(getter) {
return new ComputedRefImpl(getter);
}