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
}


相关文章
|
3天前
|
JavaScript
Vue3中props的原理与使用
Vue3中props的原理与使用
10 0
|
3天前
|
JavaScript 前端开发
vue中nextTick使用以及原理
vue中nextTick使用以及原理
7 0
|
2天前
|
资源调度 JavaScript 前端开发
将Elementui里的Table表格做成响应式并且和Pagination分页组件封装在一起(Vue实战系列)
将Elementui里的Table表格做成响应式并且和Pagination分页组件封装在一起(Vue实战系列)
|
3天前
|
JavaScript 测试技术 API
Vue3中定义变量是选择ref还是reactive?
Vue3中定义变量是选择ref还是reactive?
15 2
|
3天前
|
JavaScript 前端开发
深入了解前端框架Vue.js的响应式原理
本文将深入探讨Vue.js前端框架的核心特性之一——响应式原理。通过分析Vue.js中的数据绑定、依赖追踪和虚拟DOM等机制,读者将对Vue.js的响应式系统有更深入的理解,从而能够更好地利用Vue.js构建灵活、高效的前端应用。
|
3天前
|
JavaScript
vue3中reactive和ref函数及对比
vue3中reactive和ref函数及对比
10 1
|
3天前
|
JavaScript API
【vue实战项目】通用管理系统:api封装、404页
【vue实战项目】通用管理系统:api封装、404页
39 3
|
3天前
|
人工智能 JavaScript 前端开发
毕设项目-基于Springboot和Vue实现蛋糕商城系统(三)
毕设项目-基于Springboot和Vue实现蛋糕商城系统
|
3天前
|
JavaScript Java 关系型数据库
毕设项目-基于Springboot和Vue实现蛋糕商城系统(一)
毕设项目-基于Springboot和Vue实现蛋糕商城系统
|
3天前
|
JavaScript 前端开发
报错:关于Vue项目下载swiper插件时没有dist文件夹的问题
报错:关于Vue项目下载swiper插件时没有dist文件夹的问题