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
}


相关文章
|
11天前
|
缓存 JavaScript 搜索推荐
Vue SSR(服务端渲染)预渲染的工作原理
【10月更文挑战第23天】Vue SSR 预渲染通过一系列复杂的步骤和机制,实现了在服务器端生成静态 HTML 页面的目标。它为提升 Vue 应用的性能、SEO 效果以及用户体验提供了有力的支持。随着技术的不断发展,Vue SSR 预渲染技术也将不断完善和创新,以适应不断变化的互联网环境和用户需求。
30 9
|
10天前
|
前端开发 JavaScript
简记 Vue3(一)—— setup、ref、reactive、toRefs、toRef
简记 Vue3(一)—— setup、ref、reactive、toRefs、toRef
|
13天前
|
API
vue3知识点:reactive对比ref
vue3知识点:reactive对比ref
26 3
|
14天前
|
API
vue3知识点:响应式数据的判断
vue3知识点:响应式数据的判断
24 3
|
13天前
|
JavaScript API
vue3知识点:ref函数
vue3知识点:ref函数
26 2
|
13天前
|
JavaScript 前端开发 API
vue3知识点:Vue3.0中的响应式原理和 vue2.x的响应式
vue3知识点:Vue3.0中的响应式原理和 vue2.x的响应式
22 0
|
JavaScript 前端开发 API
Vue3入门到精通--ref以及ref相关函数
Vue3入门到精通--ref以及ref相关函数
|
11天前
|
数据采集 监控 JavaScript
在 Vue 项目中使用预渲染技术
【10月更文挑战第23天】在 Vue 项目中使用预渲染技术是提升 SEO 效果的有效途径之一。通过选择合适的预渲染工具,正确配置和运行预渲染操作,结合其他 SEO 策略,可以实现更好的搜索引擎优化效果。同时,需要不断地监控和优化预渲染效果,以适应不断变化的搜索引擎环境和用户需求。
|
4天前
|
JavaScript
Vue基础知识总结 4:vue组件化开发
Vue基础知识总结 4:vue组件化开发
|
4天前
|
存储 JavaScript
Vue 状态管理工具vuex
Vue 状态管理工具vuex