Vue3 响应式原理之 ref

简介: 实现 isProxy 非常简单,就我们之前实现的 isReactive 和 isReadonly,只要满足其中之一即为已经代理的对象

isProxy

实现 isProxy 非常简单,就我们之前实现的 isReactive 和 isReadonly,只要满足其中之一即为已经代理的对象

it('isProxy', () => {
  const original = { foo: 1, bar: { baz: 2 } };
  const wrapped = readonly(original);
  const observed = reactive(original);
  expect(isProxy(wrapped)).toBe(true);
  expect(isProxy(observed)).toBe(true);
})
复制代码

代码实现

export function isProxy(value: unknown) {
  return isReadonly(value) || isReactive(value);
}
复制代码

ref

ref和 reactive 类似,也是一个实现相应是的 API,区别在于 ref 针对基础类型,reactive 针对的是引用类型,但是其实 ref 也可以传参引用类型,但是其背后还是会转到 reactive 来完成。

收线我们来看一下 ref 的测试用例:

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);
  a.value = 2;
  expect(calls).toBe(2);
  expect(dummy).toBe(2);
});
复制代码

被 ref 修饰过的数据需要通过.value来访问,赋值同样也是;ref也需要进行依赖收集和依赖触发。

然后我们来根据测试用来完成代码,由于之前的依赖收集针对的是多依赖,但是这里 ref 只有一个value, 所有只有一个 dep,不再需要之前的 Map 结构,所以这里对之前的依赖收集重构一下(重构只要要运行测试保证之前的功能不受影响)

export function track<T extends object>(target: T, key: keyof T) {
  if (!isTracking()) return
  // target -> key -> dep
  let depsMap = targetMap.get(target)
  if (!depsMap) {
    targetMap.set(target, depsMap = new Map())
  }
  let dep = depsMap.get(key)
  if (!dep) {
    depsMap.set(key, dep = new Set<ReactiveEffect>())
  }
  trackEffects(dep)
}
function trackEffects(dep: Set<ReactiveEffect>) {
  if (dep.has(activeEffect))
    // 如果已经被收集, 中断
    return
  // 收集依赖
  dep.add(activeEffect)
  // 反向收集依赖
  activeEffect.deps.push(dep)
}
复制代码

同样的一来触发的过程我们也可以抽离出来

export function trigger<T extends object>(target: T, key: keyof T) {
  let depsMap = targetMap.get(target)
  let dep = depsMap!.get(key) as Set<ReactiveEffect>
  trackEffects(dep)
}
export function triggerEffects(dep: Set<ReactiveEffect>) {
  for (const effect of dep) {
    if (effect.scheduler)
      effect.scheduler()
    else
      effect.run()
  }
}
复制代码

由于有了这两个函数的封装,所以这里 ref 的实现也变得非常简单,只需要在 get 时触发 track,set 时触发 trigger 即可

class RefImpl {
  private _value: any;
  public dep: Set<ReactiveEffect>;
  constructor(value: any) {
    this._value = value;
    this.dep = new Set();
  }
  get value() {
    // 在 tracking 阶段收集依赖
    if(isTracking())
      trackEffects(this.dep)
    return this._value
  }
  set value(newValue) {
    // 先修改 value 再触发依赖
    this._value = newValue;
    triggerEffects(this.dep)
  }
}
export function ref(value: any) {
  return new RefImpl(value);
}
复制代码

此时运行测试,通过

1682566251(1).png

此时并没有结束,我们测试用例中还有一个条件没有写入,当设置重复值的时候不要再次触发 trigger

// ...省略之前的测试代码
// same value should not trigger
a.value = 2;
expect(calls).toBe(2);
expect(dummy).toBe(2);
复制代码

这里也很简单,只需要判断 set 的新值和旧值是否相同即可

set value(newValue) {
  if (hasChange(newValue, this._value))
    return
  // 先修改 value 再触发依赖
  this._value = newValue;
  triggerEffects(this.dep)
}
// shared/index.ts
export const hasChange = (newValue: any, value: any) => Object.is(newValue, value)
复制代码

上面我们已经说过了,ref 可以借用 reactive 对引用类型进行处理,所以接下来我们完善一下对象类型的调用。

it("should make nested properties reactive", () => {
  const a = ref({
    count: 1,
  });
  let dummy;
  effect(() => {
    dummy = a.value.count;
  });
  expect(dummy).toBe(1);
  a.value.count = 2;
  expect(dummy).toBe(2);
});
复制代码

然后我们来实现功能,就是在构造函数中判断一下 value 的类型,对不同的类型进行不同的处理即可

constructor(value: any) {
  this._value = isObject(value) ? reactive(value) : value;
  this.dep = new Set();
}
复制代码

这里已经实现了代理,但是还有一个问题,就是 set 的时候,如果传入的值是一个对象,那么 this._value的值是一个 proxy 类型,即便是相同的对象在比较是否改变时也会返回 true,所以我们需要在比较时返回代理对象的原对象

class RefImpl {
  private _value: any;
  private _rawValue: any;
  public dep: Set<ReactiveEffect>;
  constructor(value: any) {
    this._rawValue = value;
    this._value = isObject(value) ? reactive(value) : value;
    this.dep = new Set();
  }
  get value() {
    // 收集依赖
    trackRefValue(this)
    return this._value
  }
  set value(newValue) {
    if (hasChange(newValue, this._rawValue)) {
      this._rawValue = newValue;
      // 先修改 value 再触发依赖
      this._value = isObject(newValue) ? reactive(newValue) : newValue;
      triggerEffects(this.dep)
    }
  }
}
复制代码

到这里 ref 的功能已经实现了。

isRef

isRef 用于判断目标是否为 ref 响应式对象

it("isRef", () => {
  const a = ref(1);
  const user = reactive({
    age: 1,
  });
  expect(isRef(a)).toBe(true);
  expect(isRef(1)).toBe(false);
  expect(isRef(user)).toBe(false);
});
复制代码

这里判断是否为代理对象的思路和前面的判断 reactive 对象一样,添加一个标识字段即可,只要是使用 ref 代理的都会有这个标识,在判断时只需要返回标识即可。

export function isRef(value: any) {
  return !!value.__v_isRef
}
class RefImpl {
  private _value: any;
  private _rawValue: any;
  public dep: Set<ReactiveEffect>;
  private __v_isRef = true;
  constructor(value: any) {
    this._rawValue = value;
    this._value = convert(value);
    this.dep = new Set();
  }
  // 省略原来的代码
}
复制代码

unRef

unRef 用于返回被 ref 代理的原始对象

it("unRef", () => {
  const a = ref(1);
  expect(unRef(a)).toBe(1);
  expect(unRef(1)).toBe(1);
});
复制代码

这里的实现也很简单,我们之前的 RefImpl 对象中已经保存了原始value,这里只需要判断是否为 ref 对象然后分别返回对应结果即可。

export function unRef(value: any) {
  return isRef(value) ? value._rawValue : value
}


相关文章
|
18天前
|
存储 JavaScript 前端开发
vue3的脚手架模板你真的了解吗?里面有很多值得我们学习的地方!
【10月更文挑战第21天】 vue3的脚手架模板你真的了解吗?里面有很多值得我们学习的地方!
vue3的脚手架模板你真的了解吗?里面有很多值得我们学习的地方!
|
15天前
|
JavaScript 前端开发 开发者
Vue 3中的Proxy
【10月更文挑战第23天】Vue 3中的`Proxy`为响应式系统带来了更强大、更灵活的功能,解决了Vue 2中响应式系统的一些局限性,同时在性能方面也有一定的提升,为开发者提供了更好的开发体验和性能保障。
34 7
|
19天前
|
缓存 JavaScript 搜索推荐
Vue SSR(服务端渲染)预渲染的工作原理
【10月更文挑战第23天】Vue SSR 预渲染通过一系列复杂的步骤和机制,实现了在服务器端生成静态 HTML 页面的目标。它为提升 Vue 应用的性能、SEO 效果以及用户体验提供了有力的支持。随着技术的不断发展,Vue SSR 预渲染技术也将不断完善和创新,以适应不断变化的互联网环境和用户需求。
33 9
|
16天前
|
前端开发 数据库
芋道框架审批流如何实现(Cloud+Vue3)
芋道框架审批流如何实现(Cloud+Vue3)
38 3
|
15天前
|
JavaScript 数据管理 Java
在 Vue 3 中使用 Proxy 实现数据双向绑定的性能如何?
【10月更文挑战第23天】Vue 3中使用Proxy实现数据双向绑定在多个方面都带来了性能的提升,从更高效的响应式追踪、更好的初始化性能、对数组操作的优化到更优的内存管理等,使得Vue 3在处理复杂的应用场景和大量数据时能够更加高效和稳定地运行。
36 1
|
15天前
|
JavaScript 开发者
在 Vue 3 中使用 Proxy 实现数据的双向绑定
【10月更文挑战第23天】Vue 3利用 `Proxy` 实现了数据的双向绑定,无论是使用内置的指令如 `v-model`,还是通过自定义事件或自定义指令,都能够方便地实现数据与视图之间的双向交互,满足不同场景下的开发需求。
36 1
|
17天前
|
前端开发 JavaScript
简记 Vue3(一)—— setup、ref、reactive、toRefs、toRef
简记 Vue3(一)—— setup、ref、reactive、toRefs、toRef
|
18天前
Vue3 项目的 setup 函数
【10月更文挑战第23天】setup` 函数是 Vue3 中非常重要的一个概念,掌握它的使用方法对于开发高效、灵活的 Vue3 组件至关重要。通过不断的实践和探索,你将能够更好地利用 `setup` 函数来构建优秀的 Vue3 项目。
|
5天前
|
JavaScript 前端开发
如何在 Vue 项目中配置 Tree Shaking?
通过以上针对 Webpack 或 Rollup 的配置方法,就可以在 Vue 项目中有效地启用 Tree Shaking,从而优化项目的打包体积,提高项目的性能和加载速度。在实际配置过程中,需要根据项目的具体情况和需求,对配置进行适当的调整和优化。
|
5天前
|
存储 缓存 JavaScript
在 Vue 中使用 computed 和 watch 时,性能问题探讨
本文探讨了在 Vue.js 中使用 computed 计算属性和 watch 监听器时可能遇到的性能问题,并提供了优化建议,帮助开发者提高应用性能。