vue3复习-响应式

87 阅读6分钟

1.响应式定义

把JS对象封装成响应式对象。当发生数据获取或修改等变化,触发对应的更新方法实现自动更新。

安装

npm init test
npm i vue

测试

const {effect,reactive}  = requrie('@vue/reactivity')

const obj = reactive({val:1})
const showCnt = 0

effect(() => {
   showCnt = 100 +  obj.val
})

setInterval(() => {
    obj.val++
}, 1000);

2. reactive

实现思路

整理思路:

  1. 通过浏览器Proxy代理对象的get和set方法
  2. 当调用effect方法时候,遇到对象访问属性,触发get方法,
  3. 由于在触发get之前,已经把effect临时存储了全局的activeEffect
  4. 这时会在track方法里面收集到全局的activeEffect 到effectSet
  5. 当对对象做设置属性时候,触发set方法,出发trigger,调用对应的effectSet里面的activeEffect方法

数据结构

  1. 使用大Map:targetMap记录所有响应式对象
  2. targetMap里面所有key对应具体的响应式reactive对象
  3. targetMap的key对应值是depsMap,记录每个对象属性
  4. 每个属性对应effectSet 关联多个effectFn方法

image.png

测试驱动开发

基于vitest 测试用例

使用 npm create vue@latest搭建vitest,参考Vue测试驱动开发(TDD)的应用

在 src/reactivity/tests/

新增reactive.spec.js

import { describe, it, expect } from 'vitest'
import { effect } from '../effect'
import { reactive } from '../reactive'
describe('响应式测试', () => {
  it('reactive测试', () => { 
    const ret = reactive({ num: 0 })
    let val
    effect(() => {
      val = ret.num
    })
    expect(val).toBe(0)
    ret.num++
    expect(val).toBe(1)
    ret.num = 12
    expect(val).toBe(12)

  //嵌套测试
    const ret2 = reactive({ obj2: {num2:0} })
    let val2
    effect(() => {
      val2 = ret2.obj2.num2
    }) 
    expect(val2).toBe(0)
    ret2.obj2.num2++
    expect(val2).toBe(1)
  })
}) 

/src/components/reactive.js

 import { track,trigger } from './effect'
 
 export function reactive(obj) {
    if (typeof obj!=='object') {
        return obj
    }
    return new Proxy(obj,handles)
}


const createGetFn = () => {
    return function get(target,key,receiver){
        const res = Reflect.get(target,key,receiver)
        track(target,key)
        console.log("get",target[key])
        if (typeof res ==='object') {
            return reactive(res)
        }
        return res
    }
}
const createSetFn = () => {
    return function set(target,key,value,receiver){
        const res = Reflect.set(target,key,value,receiver)
        trigger(target,key)
        console.log("set",value)
        return res
    }
}

const get = createGetFn()
const set = createSetFn()
const handles = {get,set} //注意这里必需是 get set 不能自己重命名

/src/components/effect.js

由于需要动态获取effect方法,所以需要把track和trigger 定义放在里面。导出给reactive使用



let activeEffect
 export function effect(fn) { 
    const effectFn = () => {
        activeEffect = effectFn
        fn()
        activeEffect = null
    } 
    effectFn()  
    return effectFn
}


const targetMap = new WeakMap() //设置外层大map关联所有对象

//收集依赖

export function track(target,key) {
    let depsMap = targetMap.get(target) // 获取具体对象map
    if(!depsMap) { 
        depsMap = new Map()
        targetMap.set(target,depsMap)
    }
    //设置具体对象对应的属性关联effect方法map
    let effectSet = depsMap.get(key) 
    if(!effectSet) {
        effectSet = new Set()
    }
    if(!effectSet.has(activeEffect) && activeEffect) {
        effectSet.add(activeEffect)
    }
    
    depsMap.set(key,effectSet) 
}
//触发依赖
export function trigger(target,key) {
    const depsMap = targetMap.get(target) 
    if(!depsMap) {
        return 
    }
    const effectSet = depsMap.get(key)
    if(!effectSet) {
        return 
    } 
    effectSet && effectSet.forEach(fn => {
        if(typeof fn === 'function'){   
            fn()
        }
    })
}

测试

npm run test:unit

image.png

vscode+ node 调试

使用ndb

npm install -g ndb
ndb npm run test

3.ref

实现思路

  1. 直接通过对象的get 和 set方法 访问隐藏的value值。
  2. 如果发现是对象则嵌套调用reactive返回。
  3. 每次在get set的对象里加入__isRef = true,再下次传入检验是否已经存在该属性,减少重复

代码实现

新增测试用例 ref.spec.js

import { describe, it, expect } from 'vitest'
import { effect } from '../effect'
import { ref } from '../ref'
describe('响应式测试', () => {
  it('ref测试', () => {   
    const ret3 = ref(0)
    let val3
    effect(() => {
      val3 = ret3.value
    })
    expect(val3).toBe(0)
    ret3.value++
    expect(val3).toBe(1)

    let obj = {num4:1}
    const ret4 = ref(obj)
    let val4
    effect(() => {
      val4 = ret4.value.num4
    })
    expect(val4).toBe(1)
    ret4.value.num4++
    expect(val4).toBe(2)
 
    const ret5 = ref(ret4)
    expect(ret5).toBe(ret4)  
  })
 
}) 

  

ref.js

import {  track, trigger } from './effect'
import { reactive } from './reactive'
export function ref(obj) {
    console.log("obj.__isRef",obj.__isRef)
    if(obj.__isRef) { //缓存优化 下次重复传入会忽略
        console.log("命中缓存,直接返回")
        return obj
    }
    return new RefImpl(obj)
}
class RefImpl {
    constructor(obj) {
        this.__isRef = true //缓存优化 
        this._value = chkToRective(obj)
    }
    get value() { 
        track(this, 'value')  
        return this._value
    }
    set value(newVal) {
        if (this._value !== newVal) { 
            this._value = chkToRective(newVal) 
            trigger(this, 'value')
        }
    } 
}
const chkToRective = (val) => {
    if (typeof val === 'object') {
      return reactive(val)
    }
    return val
}

测试用例

import { describe, it, expect } from 'vitest'
import { effect } from '../effect'
import { reactive } from '../reactive'
import { ref } from '../ref'
import { computed } from '../computed'
describe('响应式测试', () => {
  it('ref测试', () => {   
     const ret3 = ref(0)
     let val3
     effect(() => {
       val3 = ret3.value
     })
     expect(val3).toBe(0)
     ret3.value++
     expect(val3).toBe(1)

    let obj = {num4:1}
    const ret4 = ref(obj)
    let val4
    effect(() => {
      val4 = ret4.value.num4
    })
    expect(val4).toBe(1)
    ret4.value.num4++
    expect(val4).toBe(2)
 
    const ret5 = ref(ret4)
    expect(ret5).toBe(ret4)  
  })
 
}) 

4.computed

实现思路

  1. 改造effect.js 的effect方法,不是立即执行,而且通过fn.scheduler, 用于自定义触发逻辑。
  2. 改造effect.js 的trigger方法,遍历触发方法的时候判断是否含有scheduler,有则执行scheduler,没有才执行原来的fn。
effect.js 修改点

let activeEffect 
export function effect(fn, options = {}) {
    // effect嵌套,通过队列管理
    const effectFn = () => {
      try {
        activeEffect = effectFn
        //fn执行的时候,内部读取响应式数据的时候,就能在get配置里读取到activeEffect
        return fn()
      } finally {
        activeEffect = null
      }
    }
    if (!options.lazy) { 
      effectFn()
    }
    effectFn.scheduler = options.scheduler // 延迟执行
    return effectFn
}


//触发依赖
export function trigger(target,key) {
    const depsMap = targetMap.get(target)
    if(!depsMap) {
        return 
    }
    const effectSet = depsMap.get(key)
    if(!effectSet) {
        return 
    }
    effectSet && effectSet.forEach(fn => {
        if(typeof fn === 'function'){ 
          if (fn.scheduler) {
            fn.scheduler()
          } else {
            fn()
          }
        } 
    })
}

  1. computed方法支持get和set方法
  2. 新建一个ComputedRefImpl类,跟ref类似,有get set 方法操作内置的_value
  3. ComputedRefImpl 新增上面传入的get和set方法
  4. ComputedRefImpl 新增dirty属性,用于get方法重复获取值时候,缓存处理
  5. ComputedRefImpl 新增effect的内置方法,通过fn.scheduler的方法,执行如下
  constructor(getter,setter) {
        this._getter = getter
        this._setter = setter
        this._value = undefined
        this._dirty = true
        this.effect = effect(getter, { // 这里二次包裹computed内编写的代码。让里面触发的变量变成响应式, lazy为false 延迟执行
            lazy: true,
            scheduler: () => {  
               this._value = getter() 
            },
        })
    }

代码实现

测试用例 computed.spec.js

import { describe, it, expect } from 'vitest'
import { effect } from '../effect'
import { reactive } from '../reactive'
import { ref } from '../ref'
import { computed } from '../computed' 
 
describe('computed测试',()=>{
  it('computed base',()=>{
    const ret = reactive({ count: 1 })
    const num = ref(2)
    const sum = computed(() => num.value + ret.count)
    expect(sum.value).toBe(3)
    ret.count++
    expect(sum.value).toBe(4)
    num.value = 10
    expect(sum.value).toBe(12)
  })
  it('computed修改',()=>{
    const author = ref('xxxx')
    const course = ref('yyyy')
    const title = computed({
      get(){
        return author.value+":"+course.value
      },
      set(val){
        [author.value,course.value] = val.split(':')
      }
    })
    expect(title.value).toBe('xxxx:yyyy')
    author.value="aaa"
    course.value="bbb"
    expect(title.value).toBe('aaa:bbb')
    // //计算属性赋值
    title.value = 'cc:dd'
    expect(author.value).toBe('cc')
    expect(course.value).toBe('dd')
  })
})

先优化effect代码,让其支持延迟执行effect的方法

effect.js



let activeEffect 
export function effect(fn, options = {}) {
    // effect嵌套,通过队列管理
    const effectFn = () => {
      try {
        activeEffect = effectFn
        //fn执行的时候,内部读取响应式数据的时候,就能在get配置里读取到activeEffect
        return fn()
      } finally {
        activeEffect = null
      }
    }
    if (!options.lazy) { 
      effectFn()
    }
    effectFn.scheduler = options.scheduler // 延迟执行
    return effectFn
}
  

const targetMap = new WeakMap() //设置外层大map关联所有对象
 

//收集依赖
export function track(target,key) {
    let depsMap = targetMap.get(target) // 获取具体对象map
    if(!depsMap) { 
        depsMap = new Map()
        targetMap.set(target,depsMap)
    }
    //设置具体对象对应的属性关联effect方法map
    let effectSet = depsMap.get(key) 
    if(!effectSet) {
        effectSet = new Set()
    }
    if(!effectSet.has(activeEffect) && activeEffect) {
        effectSet.add(activeEffect)
    }
    
    depsMap.set(key,effectSet) 
}
 
//触发依赖
export function trigger(target,key) {
    const depsMap = targetMap.get(target)
    if(!depsMap) {
        return 
    }
    const effectSet = depsMap.get(key)
    if(!effectSet) {
        return 
    }
    effectSet && effectSet.forEach(fn => {
        if(typeof fn === 'function'){ 
          if (fn.scheduler) {
            fn.scheduler()
          } else {
            fn()
          }
        } 
    })
}

computed.js

import { effect} from './effect.js'
export function computed(fnOrOptions) {
    let getter,setter;
    if( typeof fnOrOptions === 'function') {
        getter = fnOrOptions
        setter = () => {
            console.log('computed value 只读')
        }
    }else{
        getter = fnOrOptions.get
        setter = fnOrOptions.set
    }
    return new ComputedRefImpl(getter,setter)
}

class ComputedRefImpl {
    constructor(getter,setter) {
        this._getter = getter
        this._setter = setter
        this._value = undefined
        this._dirty = true
        this.effect = effect(getter, { // 这里二次包裹computed内编写的代码。让里面触发的变量变成响应式, lazy为false 延迟执行
            lazy: true,
            scheduler: () => {  
               this._value = getter() 
            },
        })
    }
    get value() { 
        if(this._dirty) { // 这里重复访问 computed的xx.value 时候缓存处理
            this._dirty = false
            this._value = this.effect()
        }
        return this._value
    }
    set value(newValue) { 
        this._setter(newValue)
        this._dirty = true
    }
}

参考地址

github.com/mjsong07/vu…