Vue3 源码学习(4)--响应式系统(3)

229 阅读6分钟

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最基本的响应式已经部分完成,我们可以看到tracEffecttriggerEffect函数和之前tracktrigger有很多重复代码,所以我们进行重构

重构代码

//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、isRefunRef

文档介绍

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)
 })
})

代码实现

实现思路与isReactiveisReadonly一致,在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调度器来控制副作用函数的执行时机和方式;也实现了用来收集依赖、触发依赖的tracktrigger函数。结合上面所实现的内容,我们可以实现computed计算属性

4.1 什么是计算属性

computed:接收一个getter函数,并且返回一个readonlyreactive/ref对象。它还可以使用具有 gettersetter函数的对象来创建可写 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的实现。

TODO watch