Vue3 源码学习(3)--响应式系统(3)
前言
本文主进行ref系列以及computed的源码学习和实现
1、ref
Vue3文档详情
ref接收一个内部值并返回一个响应式是且可变的ref对象。ref对象具有单个valueporperty ,指向内部值。
Type
function ref<T>(value: T): Ref<UnwrapRef<T>>
interface Ref<T> {
value: T
}
Example
const count = ref(0)
console.log(count.value) // 0
count.value++
console.log(count.value) // 1
如果将对象分配哥ref值,则通过reactive函数使用该对象具有响应式
1.1、实现最基础的ref
测试代码
//ref.spec.ts
describe("ref",()=>{
it("should hold a value",()=>{
const a = ref(1)
expect(a.value).toBe(1)
a.value = 2
expect(a.value).toBe(1)
})
it("should be reactive",()=>{
const a = ref(1)
let dummy
let calls = 0
effect(()=>{
calls++
dummy = a.value
})
expect(calls).toBe(1)
expect(dummy).toBe(1)
//ref对象是响应式的
a.value = 2
expect(calls).toBe(2)
expect(dummy).toBe(2)
//ref 对象的 value property 的 set 具有缓存,不会重复触发依赖
a.value = 2
expect(calls).toBe(2) expect(dummy).toBe(2)
})
})
源码实现
// ref.ts
calss RefImpl {
private _value
constructor(value){
this._value = value
}
//创建一个getter方法
get value(){
//TODO 收集依赖
return this._value
}
//创建一个setter方法
set value(newVal){
this._value = newVal
//TODO触发依赖
}
//ref函数
export function ref(value){
const ref = new RefImpl(value)
return ref
}
上面就是先了一个不完全的ref,即能够将传入的值转变为ref对象,但是由于没有进行依赖收集,所以并不支持数据的响应式
接下来要做的就是getter时收集依赖和setter时触发依赖
ref函数本意是让我们传递值类型,所以我们的存储依赖仅仅只是个简单的桶,并不是树形结构
//
calss RefImpl {
/*其他代码*/
public dep // 用来存储依赖的“桶”
constructor(value){
/*其他代码*/
this.dep = new Set()
}
//创建一个getter方法
get value(){
if(shouldTrack || activeEffect == undefined) {
//TODO 收集依赖
trackEffects(this.dep)
}
return this._value
}
//创建一个setter方法
set value(newVal){
this._value = newVal
triggerEffect(this.dep)
}
function trackEffect(dep){
if(dep.has(activeEffect)) return
dep.add(activeEffect)
activeEffect.deps.push(dep)
}
function triggerEffect(dep){
dep.forEach(reactvieEffect=>{
if (reactvieEffect.scheduler) {
reactvieEffect.scheduler();
} else {
reactvieEffect.run();
}
})
}
接下来实现赋值同样的值不会再次触发依赖响应
//ref.ts
class RefImpl {
/*其他代码*/
private _rawValue:any
constructor(value){
// 将传入的值赋值给实例的私有属性property_value
this._rawValue = value;
/*其他代码*/
}
set value(value){
//提前声明一个this._rawValue 来存储并进行比较
if (Object.is(val, this._rawValue)) return;
/*其他代码*/
}
执行yarn test ref命令运行ref的测试,可以看到测试通过,这样就完成了ref最基础的实现。
此时,ref最基本的响应式已经部分完成,我们可以看到tracEffect和triggerEffect函数和之前track、trigger有很多重复代码,所以我们进行重构
重构代码
//effect.ts
export function isTracking(){
return (shouldTrack || activeEffect == undefined
}
function track(target,key){
if(!isTracking()) return
/* 其他代码*/
trackEffect(deps)
}
function trigger(target,key){
/* 其他代码*/
triggerEffect(deps)
}
export function trackEffect(dep){
//看看dep之前有没有添加过,添加过的话 就不添加了
if (dep.has(activeEffect)) return;
dep.add(activeEffect);
activeEffect.deps.push(dep);
}
export function triggerEffect(dep){
dep.forEach((reactiveEffect) => {
if (reactiveEffect.scheduler) {
reactiveEffect.scheduler();
} else {
reactiveEffect.run();
}
});
}
//ref.ts
class RefImpl {
private _value
private _rawValue:any
public dep
constructor(value){
// 将传入的值赋值给实例的私有属性property_value
this._rawValue = value;
this._value = value
this.dep = new Set()
}
get value(){
trackEffect(this.dep)
return this._value
}
set value(newVal){
//提前声明一个this._rawValue 来存储并进行比较
if (Object.is(newVal, this._rawValue)) return;
this._rawValue = val;
this._value = newVal
triggerEffect(this.dep)
}
}
1.2、完善ref
若传入的值为一个对象,需要利用reactive对该对象进行响应式转换
测试代码
//ref.spec.ts
describe("ref",()=>{
/*其他代码*/
it("should make nested properties reactive" ,()=>{
cosnt a = ref ({
count :1
})
let dummy
effect(()=>{
dummy = a.value.count
})
expect(dummy).toBe(1)
// ref 对象的 value property 的是一个响应式对象
a.value.count = 2
expect(dummy).toBe(2)
})
})
源码实现
//ref.ts
//首先 声明一个工具函数,当传入值为对象是转为reactive对象
// isObject 之前定义过此工具函数
function toReactive (val){
return isObject(val) ? reactive(obj) :val
}
class RefImpl {
/*其他代码*/
constructor(value){
this.value = toReactive(value)
}
set value(newVal){
/*其他代码*/
this._value = toReactive(newVal)
triggerEffect(this.dep)
}
}
执行yarn test ref命令运行ref的测试,可以看到测试通过,这样就进一步完善了ref的实现。
2、isRef和unRef
文档介绍
isRef :检查是否为一个ref对象
unref :如果参数是一个ref对象,则返回内部值,否则返回参数本事。 是val = isRef(val) ? val.value : val的语法糖
测试代码
describe('ref', () => {
it('isRef', () => {
expect(isRef(ref(1))).toBe(true)
expect(isRef(reactive({ foo: 1 }))).toBe(false)
expect(isRef(0)).toBe(false)
expect(isRef({ bar: 0 })).toBe(false)
})
it('unref', () => {
expect(unref(1)).toBe(1)
expect(unref(ref(1))).toBe(1)
})
})
代码实现
实现思路与isReactive、isReadonly一致,在RefImpl类中增加共有 property __v_isRef用于标志实例是一个 ref 对象。
//ref.ts
calss RefImpl {
/*其他代码*/
public __v_isRef = true
/*其他代码*/
}
//用于判断一个值是否为ref对象
export function isRef(value){
return !!(value.__v_isRef === true)
}
//unref
export function unref (value){
return isRef(value)? value.value :value
}
4、computed计算属性
之前我们实现了effect函数用来调用注册副作用函数,同时允许传递第二个参数option对象,option对象可以指定scheduler调度器来控制副作用函数的执行时机和方式;也实现了用来收集依赖、触发依赖的track、trigger函数。结合上面所实现的内容,我们可以实现computed计算属性
4.1 什么是计算属性
computed:接收一个getter函数,并且返回一个readonly的reactive/ref对象。它还可以使用具有 getter和setter函数的对象来创建可写 ref 对象。
Type
// read-only
function computed<T>(
getter: () => T,
// see "Computed Debugging" link below
debuggerOptions?: DebuggerOptions
): Readonly<Ref<Readonly<T>>>
// writable
function computed<T>(
options: {
get: () => T
set: (value: T) => void
},
debuggerOptions?: DebuggerOptions
): Ref<T>
Example
Creating a readonly computed ref:
const count = ref(1)
const plusOne = computed(() => count.value + 1)
console.log(plusOne.value) // 2
plusOne.value++ // error
Creating a writable computed 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
4.2、实现最基础的computed
测试代码
//computed.spce.ts
describe("computed",()=>{
it("should return updated value",()=>{
const value = reactive({foo :1})
//接受一个getter函数创建只读的ref对象
const cValue = computed(()=>value.foo)
expect(cValue.value).toBe(1)
value.foo = 2
expect(cValue.value).toBe(2)
})
})
代码实现
在时间过程中,利用ref接口的类的实现思路,对操作进行封装,同时利用了effect的实现中抽离出来的ReactvieEffect类
// computed.ts
class ComputedImpl {
private _effect: ReactiveEffect
constructor(getter){
//利用getter函数创建ReactiveEffect类的实例
this._effect = new ReactiveEffect(getter)
}
get value(){
return this._effect.run()
}
}
export function computed (getter){
reutrn new ComputedImpl(getter)
}
执行yarn test computed命令运行computed的测试,可以看到测试通过,这样就完成了computed最基础的实现。
4.3、完善computed
computed会来懒执行getter函数,同时对getter函数返回的ref对象的value进行了缓存
添加测试
//computed.spec.ts
describe("computed", () => {
/*其他代码*/
it("should compute lazily",()=>{
const value = reactive({foo :1})
const getter = jest.fn(()=>value.foo)
const cValue = computed(getter)
//在获取ref对象的value 的值是才执行getter
expect(getter).not.toHaveBeenCalled()
expect(cValue.value).toBe(1)
expect(getter).toHaveBeenCalled(1)
// 若依赖的响应式对象的 property 的值没有更新,则再次获取 ref 对象的 value property 的值不会重复执行 getter
cValue.value
expect(getter).toHaveBeenCalledTimes(1)
// 修改依赖的响应式对象的 property 的值时不会执行 getter
value.foo = 2
expect(getter).toHaveBeenCalledTimes(1)
// 在依赖的响应式对象的 property 的值没有更新后,获取 ref 对象的 value property 的值再次执行getter
expect(cValue.value).toBe(2)
expect(getter).toHaveBeenCalledTimes(2)
cValue.value
expect(getter).toHaveBeenCalledTimes(2)
})
})
代码实现
//computed.ts
class ComputedImpl {
/*其他代码*/
// 用于保存 getter 函数的执行结果
private _Value
// 用于记录是否不使用缓存
private _dirty = true
constructor(getter){
const _effect = new ReactiveEffect(getter,{
scheduler :()=>{
if(!this._dirty){
this._dirty = true
}
}
})
get value(){
if(this._dirty){
this._dirty = false
this._Value = this._effect.run()
}
return this._value
}
}
执行yarn test computed命令运行computed的测试,可以看到测试通过,这样就进一步完善了computed的实现。