vue3 - mini-vue(八)实现ref,实现基本数据类型的响应式

120 阅读5分钟

ref

第一步

测试用例
describe('ref', () => {
  it('happy path', () => {
    // 创建一个ref:a
    const a = ref(1)
    // a.value === 1
    expect(a.value).toBe(1)
  })
})
功能描述

使用ref创建一个响应式数据(传入原始值),可以通过ref.value来访问它

实现
// 创建ref对象的方法
function ref (value) {
  return new RefImpl(value)
}
// 创建一个类,用它的实例来包装一个ref,与reactiveEffect一个道理
class RefImpl {
  private _value
  constructor(value) {
    this._value = value
  }
  // 定义get和set方法
  get value () {
    return this._value
  }
  // todo
  set value (newValue) {
  }
}

第一步很简单,已经实现了通过value来访问ref的值

第二步

测试用例
describe('ref', () => {
  // 第一步的功能点,不用关心
  it('happy path', () => {
    const a = ref(1)
    expect(a.value).toBe(1)
  })
  // 第二步需要实现的功能点
  it('should be reactive', () => {
    // 创建一个ref:a
    let a = ref(1)
    let dummy
    // 声明一个变量用来计数
    let calls = 0
    // 通过effect做依赖收集
    effect(() => {
      calls++
      dummy = a.value
    })
    // 上一步effect做依赖收集回先执行一次传入的fn,calls === 1,dummy === 1
    expect(calls).toBe(1)
    expect(dummy).toBe(1)
    // 设置value的值,触发set
    a.value = 2
    // 传入effect的fn又执行了一次,calls计数加1:calls === 2,触发依赖后dummy === 2
    expect(calls).toBe(2)
    expect(dummy).toBe(2)
  })
})
功能描述

第二不需要实现的是收集和触发响应式对象的依赖,如测试用例描述那样

实现
// 更新RefImpl
class RefImpl {
  private _value
  public dep
  private _rawValue
  constructor(value) {
    this._value = value
    // 初始化dep,需要一个dep来保存依赖
    this.dep = new Set()
  }
  get value () {
    /*
     * reactive代理的是对象,对象有多个属性值,而且属性值也有可能是对象,
     *   所以它使用了Map来保存多个属性的依赖
     * 因为ref代理的是原始值,它只有一个value属性,所以只需要一个set来存储这个属性的依赖项就可以
     */
    /*
     * 如果activeEffect = undefined不需要收集依赖
     *   这里其实不需要关心 shouldTrack,因为shouldTrack的相关逻辑在实现reactive的时候已经实现了
     *   在这里shouldTrack与reactive的用处和实现reactive是的用处是一样的
     */
    if (!isTracking()) return
    // 如果已经收集了,直接return掉
    if (dep.has(activeEffect)) return
    dep.add(activeEffect)
    activeEffect.deps.push(dep)

    return this._value
  }
  set value (newValue) {
    // 一定需要先去修改value,再去触发依赖
    this._value = newValue
    // 遍历ref的value属性的所有所有依赖项并执行
    for (const effect of dep) {
      if (effect.scheduler) {
        effect.scheduler()
      } else {
        effect.run()
      }
    }
  }
}
优化

对之前实现的reactive还有印象的话,不难发现,实现ref收集依赖和触发依赖的代码有重复,所以可以将它抽出来

// 执行触发依赖的操作
function triggerEffects (dep) {
  for (const effect of dep) {
    if (effect.scheduler) {
      effect.scheduler()
    } else {
      effect.run()
    }
  }
}
// 执行收集依赖的操作
function trackEffects (dep) {
  if (dep.has(activeEffect)) return
  dep.add(activeEffect)
  activeEffect.deps.push(dep)
}
// 更新RefImpl
class RefImpl {
  ... // 省略
  get value () {
    // todo 这一步也可以抽离出来
    // if (!isTracking()) return
    // trackEffects(this.dep)
    
    // 抽离后
    trackRefValue(this)
    return this._value
  }
  set value (newValue) {
    this._value = newValue
    triggerEffects(this.dep)
}

// get收集依赖时的操作也可以进行抽离
function trackRefValue (ref) {
  if (isTracking()) {
    trackEffects(ref.dep)
  }
}

第三步

测试用例
describe('ref', () => {
  it('happy path', () => {
    ... // 省略
  })
  it('should be reactive', () => {
    ... // 省略第一二步的测试
    /*
    * 第三步
    * 再次修改a.value的值,和上一次的值相等,那么它就不应该触发依赖
    * calls不变:calls === 2
    * 没有触发依赖:dummy === 2
    * */
    a.value = 2
    expect(calls).toBe(2)
    expect(dummy).toBe(2)
  })
})
功能描述

第三步也很简单,修改ref的值,如果修改的值和当前的值一样,那么不触发依赖

实现
class RefImpl {
  private _value
  public dep
  constructor(value) {
    // 如果这个value是一个对象,那么需要通过reactive给它进行包装
    // 1.检查value是不是对象
    this._value = value
    this.dep = new Set()
  }
  get value () {
    trackRefValue(this)
    return this._value
  }
  set value (newValue) {
    /*
    * newValue -> this._value,如果相等不需要执行操作
    * */
    if (hasChange(newValue, this._value)) {
      // 一定需要先去修改value,再去触发依赖
      this._value = newValue
      triggerEffects(this.dep)
    }
  }
}
// 对比新旧值有变化(两个值是否相等)
function hasChange = (newValue, oldValue) => !Object.is(newValue, oldValue)

第四步

测试用例
it('should make nested properties reactive', () => {
  // 如果ref接受的值是一个对象
  const a = ref({ count: 1 })
  let dummy
  // 它的值也是通过.value去取的
  effect(() => {
    dummy = a.value.count
  })
  // 调用effect会调用传入的fn,dummy === 1
  expect(dummy).toBe(1)
  // 修改ref的值
  a.value.count = 2
  // dummy的值也会被修改
  expect(dummy).toBe(2)
})
功能描述

ref可以将原始值处理为响应式对象,那么如果传入一个对象呢,与原始值相似,也是通过ref.value可以访问到传入的这个值,但是呢因为它的value是一个对象,所以需要将它用reactive包装起来再返回,它依旧是一个响应式数据。

实现
// 更新RefImpl
class RefImpl {
  private _value
  public dep
  private _rawValue // 声明一个值来保存传入的值(无论是基本数据类型还是引用类型都保存起来)
  constructor(value) {
    // 如果这个value是一个对象,那么需要通过reactive给它进行包装
    // 1.检查value是不是对象
    this._value = convert(value)
    this._rawValue = value

    // this._value = value
    this.dep = new Set()
  }
  get value () {
    trackRefValue(this)
    return this._value
  }
  set value (newValue) {
    // if (Object.is(newValue, this._value)) return
    /*
    * 对比的时候呢,是需要对两个原始值进行对比,而如果value是一个对象的话,this._value就成了reactive
    *   所以传入的value如果是对象,在对比前需要将reactive转为原始对象
    * 解决办法:初始化RefImpl的时候,声明一个值(_rawValue)保存传入的value,对比的时候就使用_rawValue对比
    * */
    if (hasChange(newValue, this._rawValue)) {
      // 一定需要先去修改value,再去触发依赖
      this._rawValue = newValue
      this._value = convert(newValue)
      triggerEffects(this.dep)
    }
  }
}
/*
 * 给_value赋值时,需要先判断它时引用类型还是基本数据类型,如果是引用类型,需要用reactive包装
 * isObject前面已经实现过了
 */
function convert (value) {
  return isObject(value) ? reactive(value) : value
}