手写vue3源码——ref, computed 等

简介: 既然都需要使用 .value,是不是意味着,传入的数据都会被一个对象所包裹,基于这个特点,咋们是否可以使用class 里面有get, set 方法呢?class 本身是一个实例对象,刚好里面的get,set 可以对属性进行拦截存取行为

引言


<<往期回顾>>


1.手写vue3源码——创建项目


2.手写vue3源码——reactive, effect ,scheduler, stop


3.手写vue3源码——readonly, isReactive,isReadonly, shallowReadonly


本期主要实现的api有,ref, isRef, unRef, proxyRefs, computed,本次所有的源码请查看


ref


在代码中,ref这个api也是用的很频繁的,所以今天咋们就一起来实现下


功能分析


ref处理的数据有两种, 原始值类型和引用值类型,不管是get原始值还是引用类型的值都需要使用.value的形式来获取,set的时候也是同用的需要使用.value来进行操作


既然都需要使用 .value,是不是意味着,传入的数据都会被一个对象所包裹,基于这个特点,咋们是否可以使用class 里面有get, set 方法呢?class 本身是一个实例对象,刚好里面的get,set 可以对属性进行拦截存取行为, 详情请查看es6阮一峰class


ref处理普通值


处理普通值的时候,ref在使用get的时候,需要使用 .value 来获取值


测试用例


test('ref 处理普通值 get', () => {
    const aRef = ref(1)
    // ref 会有一个value属性
    expect(aRef.value).toBe(1)
    aRef.value = 2;
     expect(aRef.value).toBe(2)
  })
复制代码


编码


/**
 * 把数据变成一个ref
 * @param val 
 * @returns 
 */
export function ref(val) {
  return new Ref(val)
}
class Ref{
  private _value: any;
  constructor(value) {
    this._value = value
  }
  get value(){
      return this._value
  }
  set value(val){
    this._value = val
  }
}
复制代码


这样写的话,ref处理普通值的场景就ok了,上面的测试用例也是可以通过的


给ref进行一个包装,调用的 .value 其实是调用Ref class 的一个get方法,有没有发现ref的value是这么来的


ref 处理对象


我们知道,ref也是可以处理对象的,处理对象的时候,调用的是 reactive方法来进行包装


这里为啥要调用reactive来包装对象呢?


ref绑定的数据是双向数据绑定的,需要对 对象内部的属性进行劫持,就是说对象里面的内容发生变化,要能够接收到通知,然后进行数据更新操作


测试用例


根据ref处理对象,咋们可以写出测试用例


test('ref 处理对象', () => {
    const aRef = ref({ a: 1, b: 2 })
    // ref 会有一个value属性
    expect(aRef.value.a).toBe(1)
    expect(isReactive(aRef.value)).toBe(true)
    // update
    aRef.value.b = 4;
    expect(aRef.value.b).toBe(4)
  })
复制代码


编码


修改之前的代码,在这里对于构造函数的数据做一个是否是对象的判断和set值的时候,也需要对set的值做判断


  constructor(value) {
    // 判断value是否是对象,对象直接调用reactive
    this._value = isObj(value) ? reactive(value) : value
  }
  // 省略其他
   set value(val){
    this._value = isObj(val) ? reactive(val) : val
  }
复制代码


这样的话,咋们的测试用例是可以通过的,完了么,no, ref也需要和reactive一样,在get数据的时候进行track, 修改数据的时候进行 trigger


处理 track 和 trigger


这里咋们先看vue给出的一个官方的测试用例,通过测试用例来分析功能


测试用例


test('ref 把数据变成响应式', () => {
    const a = ref(1);
    let dummy;
    let calls = 0;
    // 依赖收集
    effect(() => {
      calls++;
      dummy = a.value;
    });
    expect(calls).toBe(1);
    expect(dummy).toBe(1);
    // update
    a.value = 2;
    expect(calls).toBe(2);
    expect(dummy).toBe(2);
    // same value should not trigger
    a.value = 2;
    expect(calls).toBe(2);
    expect(dummy).toBe(2);
  })
复制代码


分析


通过上面的测试用例,咋们可以分析出以下需求:


1.a.value = 2时,后面的数据都进行的改变,说明这个是触发依赖,既然有触发依赖,那么咋们肯定是需要进行依赖收集的


2.当再一次调用 a.value = 2时,会发现 calls 的值没有变化,说明在set中做了新值与旧值的控制


既然要进行依赖收集,那在我们ref中怎么来进行依赖收集呢?


1.毫无疑问的是 —— 收集依赖肯定是在get value函数中进行的,而触发依赖是在 set 函数中进行,但是需要与effect进行联动,就会用到effect里面的activeEffect 和 shouldTrack 等,这里需要注意.


2.控制新旧值的变化也是在set中完成的


编码


// 在 effect模块中咋们可以抽离出以下几个函数来空供我们ref模块使用
/**
 * 收集effect
 * @param deps 
 * @returns 
 */
export function trackEffect(deps) {
  // 存在的话,不需要反复收集
  if (deps.has(activeEffect)) return
  // 收集依赖
  deps.add(activeEffect)
  activeEffect.deps.push(deps)
}
/**
 * 是否可以进行依赖收集
 * @returns 
 */
export function tracking() {
// 可以进行track和activeEffect 有值
  return shouldTrack && activeEffect
}
/**
 * 遍历触发依赖
 * @param deps 
 */
export function triggerEffect(deps) {
  deps.forEach(effect => {
    if (effect.scheduler) {
      effect.scheduler()
    } else {
      effect.run()
    }
  })
}
// 来改造咋们的class
class Ref {
  private _value: any;
  // 收集的ref依赖
  private deps
  // 原始值,由于对象会被转成proxy,所以咋们需要保存一个原始的值,用于控制值是否改变
  private _rawVal: any;
  constructor(value) {
    // 判断value是否是对象,对象直接调用reactive
    this._value = isObj(value) ? reactive(value) : value
    this._rawVal = value
    this.deps = new Set()
  }
  get value() {
    // 进行依赖收集
    if (tracking()) {
      trackEffect(this.deps)
    }
    return this._val
  }
  set value(val) {
    // 数据没有发生改变,不需要重新trigger
    if (!hasChanged(val, this._rawVal)) return
    this._rawVal = val;
    this._value = isObj(val) ? reactive(val) : val
    triggerEffect(this.deps)
  }
}
复制代码


通过这里咋们可以分析出,ref的响应式其实是通过包装了一层实例对象,通过劫持实例对象的 get 和 set 方法来做到的,而为啥需要使用value呢?因为get和set的是方法,value可以被咋们改成任何的名称


isRef


isRef是用于判断一个对象是否被 Ref 实例对象所包裹


对于这个api,咋们怎么实现呢?有了isReactive和isReadonly的基础,我相信你不难想到,也是同样的配方,熟悉的味道。


测试用例


test('isRef', () => {
    const a = ref(1);
    expect(isRef(a)).toBe(true);
    const b = 1;
    expect(isRef(b)).toBe(false);
    const c = reactive({ a: 1 })
    expect(isRef(c)).toBe(false);
  })
复制代码


编码


/**
 * 判断传入的数据是否是ref
 * @param val 
 * @returns 
 */
export function isRef(val) {
  return !!val._v__is_ref
}
// 接下来在我们的class中加一个_v__is_ref属性,并且设置为true即可
 public _v__is_ref = true
复制代码


unRef


unRefapi是用于 当你.value 用的太繁琐的时候,不知道你后面的值到底有没有 .value,说白了就是说你不想写.value 就用这个api来进行包裹一下就行


/**
 * 把ref数据变成origin数据
 * @param val 
 * @returns 
 */
export function unRef(val) {
  return isRef(val) ? val.value : val
}
复制代码


proxyRefs


proxyRefs用的人估计不是很多,但是用了 setup的人都知道, setup返回的数据,不管有没有.value 当你在模板中使用的时候,都可以省略.value,setup返回的结果就调用了这个api


be32227262fdd5cc4f89dcf831b32011.png


测试用例


 test('proxyRefs', () => {
    const user = {
      age: ref(10),
      name: "twinkle",
    };
    const proxyUser = proxyRefs(user);
    // 不改变原数据结构
    expect(user.age.value).toBe(10);
    expect(proxyUser.age).toBe(10);
    expect(proxyUser.name).toBe("twinkle");
    // set 赋值普通值
    proxyUser.age = 20;
    expect(proxyUser.age).toBe(20);
    expect(user.age.value).toBe(20);
    // set 赋值ref
    proxyUser.age = ref(10);
    expect(proxyUser.age).toBe(10);
    expect(user.age.value).toBe(10);
  })
复制代码


分析


通过上面的测试用例,咋们可以分析出以下需求:


1.proxyRefs可以对数据进行get和set,并且还要做对应的处理

2.在 get的时候,会自动的把.value给省略掉

3.在set的时候,需要区分是普通值还是ref


实现功能


1.对于需要劫持对象,肯定使用proxy来进行劫持

2.在get数据的时候,判断获取的结果是ref还是普通的,ref的话,默认调用.value

3.对于set的话,需要判断set的内容是不是ref,并且需要判断,set之前的值是啥类型


  • 。如果set的内容是普通值,并且原来的值是ref的话,需要调用.value来赋值
  • 。否则的话,直接替换即可


编码


/**
 * 
 * @param obj 
 * @returns 
 */
export function proxyRefs(obj) {
  return new Proxy(obj, {
    get(target, key) {
      const val = Reflect.get(target, key)
      // 返回的结果进行判断
      return isRef(val) ? val.value : val
    },
    set(target, key, val) {
      // set -> target[key] is ref && val not is ref 
      if (isRef(target[key]) && !isRef(val)) {
        return target[key].value = val
      } else {
        return Reflect.set(target, key, val)
      }
    }
  })
}
复制代码


computed


computed这个api大家基本上都会使用,传入一个fn或者是自定义get,set, 返回一个对象,并且需要使用 .value 来调用里面的内容,看到.value是不是感觉和ref是一样的结构😀😀😀,来一个class给它包装以下即可。


测试用例


先来简单测试用例,自己也可以动手敲一敲哦~~~✌✌✌


test('测试computed函数结果', () => {
    const a = computed(() => 1)
    expect(a.value).toBe(1)
  })
复制代码


要实现上面内容是不是和ref是一样的,只不过传的内容不一样而已,这里就省略了哈,


来一个复杂一点点的测试用例


 it('computed', () => {
    const value = reactive({})
    const getter = jest.fn(() => value.foo)
    const cValue = computed(getter)
    // lazy
    expect(getter).not.toHaveBeenCalled()
    expect(cValue.value).toBe(undefined)
    expect(getter).toHaveBeenCalledTimes(1)
    // should not compute again
    cValue.value
    expect(getter).toHaveBeenCalledTimes(1)
    // should not compute until needed
    value.foo = 1
    expect(getter).toHaveBeenCalledTimes(1)
    // now it should compute
    expect(cValue.value).toBe(1)
    expect(getter).toHaveBeenCalledTimes(2)
    // // should not compute again
    cValue.value
    expect(getter).toHaveBeenCalledTimes(2)
  })
复制代码


分析


根据上面的测试用例,咋们可以分析以下需求:


1.computed接收一个fn函数,并且一开始该函数不执行,最后函数返回一个对象 ,带有 .value


2.第一次调用 .value fn会执行一次,等后续调用则不执行


3.改变fn内响应式对象的值,fn还是不执行


4.当调用 .value 时, fn则会执行


综上所述, computed会对fn进行缓存,只有内容变化,且调用了computed的返回值的.value才会去执行fn


对应的解决措施


1.需要带有 .value 那咋就给他用 class来进行包裹一下,并且处理value方法的get和set


2.这里需要控制fn的执行,需要对fn进行依赖收集和依赖触发,收集肯定是在get中进行收集,触发的话咋们可以在 EffectReactive class 钩子函数的 scheduler 函数中进行触发,对于scheduler 有不清楚的请查看 手写vue3源码——reactive, effect ,scheduler, stop


3.数据没有变化的时候需要做缓存,那么可以使用flag来解决


编码


export function computed(fn) {
  return new ComputedRefImpl(fn)
}
class ComputedRefImpl{
  // 传入的fn
  private getter: any
  private readonly setter: any
  private _value: any
  // 是否可以执行
  private _dirty = true
  // 收集getter的依赖
  private deps;
  // 当前的effect
  private effect
  constructor(getter) {
    this.getter = getter
    this.deps = new Set()
    this.effect = new EffectReactive(this.getter, () => {
      if (!this._dirty) {
        this._dirty = true
        // 触发依赖
        triggerEffect(this.deps)
      }
    })
  }
  get value() {
    // 收集依赖
    if (tracking()) {
      trackEffect(this.deps)
    }
    // 用于缓存执行结果
    if (this._dirty) {
      this._dirty = false
      this._value = this.effect.run()
    }
    // 返回结果
    return this._value
  }
}
复制代码


这里咋们会发现computed设计的非常巧妙,如下图


e1938800924d14d2521c7fdfcf757846.png


这里来分析一下:


1.在初始化 ComputedRefImpl 的时候就完成, deps, effect, getter的初始化,但是对于EffectReactive而言的话,完成了 fn, scheduler的初始化


2.当第一次调用 get value的时候判断当前的activeEffect是否存在,存在的话对收集依赖


3.然后判断get .value是否第一次调用,第一次的话则进行effect中调用run方法,否则返回历史run方法值的结果,然后把dirty设置为false作为缓存fn执行的结果


4.调用run方法的时候会把activeEffect赋值为this, 执行fn函数,最后反正fn函数的结果


5.执行fn函数的通知,会对 fn内部使用到的响应式数据进行track


6.等待fn内响应式数据发生变化,然后触发在fn函数内收集到的effect函数并且进行遍历执行


7.在遍历的过程中需要判断effect中是否存在 scheduler函数,存在的话则执行该函数


8.在scheduler函数中,会把dirty重置为true,标志着computed内容fn数据有变化,需要重新执行fn,并且会把在get value中收集到的依赖进行trigger


9.等待 执行 get value,更新数据


彩蛋🎈🎈🎈


computed 其实可以存入一个对象,对象中可以自己定义get,set,自己可以实现下,有兴趣的同学可以查看源码,源码中实现了get和set哦~~~

相关文章
|
5天前
|
缓存 JavaScript UED
Vue3中v-model在处理自定义组件双向数据绑定时有哪些注意事项?
在使用`v-model`处理自定义组件双向数据绑定时,要仔细考虑各种因素,确保数据的准确传递和更新,同时提供良好的用户体验和代码可维护性。通过合理的设计和注意事项的遵循,能够更好地发挥`v-model`的优势,实现高效的双向数据绑定效果。
109 64
|
5天前
|
JavaScript 前端开发 API
Vue 3 中 v-model 与 Vue 2 中 v-model 的区别是什么?
总的来说,Vue 3 中的 `v-model` 在灵活性、与组合式 API 的结合、对自定义组件的支持等方面都有了明显的提升和改进,使其更适应现代前端开发的需求和趋势。但需要注意的是,在迁移过程中可能需要对一些代码进行调整和适配。
|
18天前
|
存储 缓存 JavaScript
在 Vue 中使用 computed 和 watch 时,性能问题探讨
本文探讨了在 Vue.js 中使用 computed 计算属性和 watch 监听器时可能遇到的性能问题,并提供了优化建议,帮助开发者提高应用性能。
|
18天前
|
存储 缓存 JavaScript
Vue 中 computed 和 watch 的差异
Vue 中的 `computed` 和 `watch` 都用于处理数据变化,但使用场景不同。`computed` 用于计算属性,依赖于其他数据自动更新;`watch` 用于监听数据变化,执行异步或复杂操作。
|
27天前
|
JavaScript 数据管理 Java
在 Vue 3 中使用 Proxy 实现数据双向绑定的性能如何?
【10月更文挑战第23天】Vue 3中使用Proxy实现数据双向绑定在多个方面都带来了性能的提升,从更高效的响应式追踪、更好的初始化性能、对数组操作的优化到更优的内存管理等,使得Vue 3在处理复杂的应用场景和大量数据时能够更加高效和稳定地运行。
45 1
|
27天前
|
JavaScript 开发者
在 Vue 3 中使用 Proxy 实现数据的双向绑定
【10月更文挑战第23天】Vue 3利用 `Proxy` 实现了数据的双向绑定,无论是使用内置的指令如 `v-model`,还是通过自定义事件或自定义指令,都能够方便地实现数据与视图之间的双向交互,满足不同场景下的开发需求。
49 1
|
JavaScript 容器
【Vue源码解析】mustache模板引擎
【Vue源码解析】mustache模板引擎
64 0
|
JavaScript 前端开发
vue源码解析之mustache模板引擎
vue源码解析之mustache模板引擎
105 0
|
JavaScript
01 - vue源码解析之vue 数据绑定实现的核心 Object.defineProperty()
01 - vue源码解析之vue 数据绑定实现的核心 Object.defineProperty()
89 0
|
JavaScript 索引
Vue $set 源码解析(保证你也能看懂)
说明这个key本来就在对象上面已经定义过了的,直接修改值就可以了,可以自动触发响应
128 0
Vue $set 源码解析(保证你也能看懂)