likes
comments
collection
share

vue3源码阅读与实现: 响应式系统-ref模块

作者站长头像
站长
· 阅读数 47

ref模块

模块总览

vue3源码阅读与实现: 响应式系统-ref模块

ref函数既可以创建基本数据类型的响应式数据,也可以创建复杂类型的响应式数据.可能有以下这些疑问:

  1. ref是如何分别处理简单数据类型和复杂数据类型的?
  2. ref的对简单数据类型的响应式是基于什么实现的?proxy吗?
  3. 为什么ref构建的响应式数据需要通过.value访问?

让我们带着问题来查看vue是如何实现ref函数的

debugger

通过debugger查看ref的源码,同样的有以下三个关注点:

  1. ref创建响应式数据
  2. 访问响应式数据
  3. 设置响应式数据

重点关注在这三个点,vue做了哪些事情

处理复杂数据类型

使用以下测试用例:

<!doctype html>
<html>
  <head>
    ...
    <script src="../../dist/vue.js"></script>
  </head>
  <body>
    <div id="app"></div>
    <script>
      // 测试ref处理复杂数据类型

      const { ref, effect } = Myvue;
      debugger
      const obj = ref({ name: "响应式" }); // 关注点1: 创建响应式主句

      debugger;
      effect(() => {
        document.querySelector("#app").innerHTML = obj.value.name; // 关注点2:访问响应式数据
      });

      debugger;
      setTimeout(() => {
        obj.value.name = "修改之后值"; // 关注点3:设置响应式数据
      }, 2000);
...
</html>

关注点1:创建ref

先来进入第一个debugger:

vue3源码阅读与实现: 响应式系统-ref模块

  1. 点击setp next into function,进入ref函数,直接调用createRef(),进入这个createRef函数

vue3源码阅读与实现: 响应式系统-ref模块

  1. createRef函数中,通过RefImpl创建了ref实例

vue3源码阅读与实现: 响应式系统-ref模块

  1. 进入RefImpl类中,在这里判断是深层响应式处理时,会调用toReactive函数,进而使用reactive对数据进行响应式处理

vue3源码阅读与实现: 响应式系统-ref模块

vue3源码阅读与实现: 响应式系统-ref模块

  1. 最后将这个由reactive函数创建的响应式数据存放在了RefImpl实例的_value属性中

到这里ref创建复杂数据类型响应式的主要逻辑就结束了,由于reactive函数在上一节中已经描述并实现过了这里就不再赘述,

总结

ref处理复杂类型响应式数据主要做了1件事情:

  1. 判断数据类型为复杂数据类型时,使用reactive进行数据响应式处理

关注点2:ref的依赖收集

接着进入第二个debugger,看看访问响应式数据时,vue时怎么处理的:

vue3源码阅读与实现: 响应式系统-ref模块

  1. 首先会触发effect函数,这个函数在上一节中已经讲过了,主要用来创建ReactiveEffect实例,并赋值给activeEffect,用来保存effect的参数fn,也就是所谓的依赖,

  2. 之后obj.value触发了.valueget函数,因此会进入RefImplget函数中,在这个函数里,会调用trackRefValue进行依赖收集

vue3源码阅读与实现: 响应式系统-ref模块

  1. 进入trackRefValue,判断当前activeEffect是否有值,有值则表示需要进行依赖收集,于是通过trackEffects将依赖收集并保存在实例属性dep中,trackEffects函数在上一节已经实现过了,这里不再重复查看

vue3源码阅读与实现: 响应式系统-ref模块

  1. 最后get value()方法返回对应的值,此时代码还没有执行完毕,因为此时只访问了value,还没访问到name,然后obj.value.name,就触发了.nameget函数,这个get是由createGetter函数创建的,在上一节中详细讲过

到此访问ref响应式数据的流程全部结束了

总结

ref创建的复杂数据类型响应式数据被访问时,会做两件事情:

  1. 触发get value(),收集依赖,这里收集的依赖,对于复杂类型的数据暂时是没用的,ref中复杂类型都交给reactive处理了,其依赖会被保存在targetMap
  2. 触发reactive创建的proxyget,进行依赖收集工作

关注点3:ref的依赖触发

在这里,我们通过obj.value.name重新设置了响应式数据的值,同样的这里.value会先触发get value()

  1. get value()中调用trackRefValue,判断activeEffectundefined,因此什么都不做返回this._value,然后.name会触发set,接下来就和reactive创建的响应式数据一样了,去触发依赖

vue3源码阅读与实现: 响应式系统-ref模块

总结

到这里我们可以知道,对于响应式数据,ref实际是交给reactive处理的.

处理简单数据类型

将上面的测试用例进行简单修改:

<!doctype html>
<html>
  <head>
    ...
    <script src="../../dist/vue.js"></script>
  </head>
  <body>
    <div id="app"></div>
    <script>
      // 测试ref处理复杂数据类型

      const { ref, effect } = Myvue;
      debugger
      const obj = ref("响应式"); // 关注点1: 创建响应式主句

      debugger;
      effect(() => {
        document.querySelector("#app").innerHTML = obj.value; // 关注点2:访问响应式数据
      });

      debugger;
      setTimeout(() => {
        obj.value = "修改之后值"; // 关注点3:设置响应式数据
      }, 2000);
    </script>
  </body>
</html>

同样,从三个关注点入手:

  1. ref创建响应式数据
  2. 访问响应式数据
  3. 设置响应式数据

重点关注在这三个点,vue做了哪些事情

关注点1:创建ref

进入第一个debugger,和处理复杂数据类型相似,会创建RefImpl实例,但不会使用toReactive处理数据,而是直接保存到实例的_value属性上

vue3源码阅读与实现: 响应式系统-ref模块

总结

ref在创建简单数据类型响应式数据时,直接将值保存在_value属性中

关注点2:ref的依赖收集

进入第二个debugger,这里访问了响应式数据的值

vue3源码阅读与实现: 响应式系统-ref模块

  1. 首先触发effect函数,为activeEffect赋值,然后通过data.value触发了RefImpl实例的get value()

vue3源码阅读与实现: 响应式系统-ref模块

  1. 接下来和处理复杂数据类型时相同,通过trackRefValue收集依赖

  2. 执行后,RefImpl实例中的dep属性将保存着所有依赖

总结

访问ref响应式数据时,主要做了一件事情:

  1. 触发get value()进行依赖收集,并保存在实例的dep属性中

关注点3:ref的依赖触发

进入第三个debugger,这里设置了响应式数据的值,因此会触发set value(),

vue3源码阅读与实现: 响应式系统-ref模块

  1. set value()函数中,判断值是否改变,如果改变,触发triggerRefValue,触发收集的依赖

vue3源码阅读与实现: 响应式系统-ref模块

  1. triggerRefValue中,将收集的依赖dep,传递给triggerEffects(在分析reactive模块时讲解过),统一进行触发

vue3源码阅读与实现: 响应式系统-ref模块

总结

设置ref属性时,vue主要做了1件事情:

  1. 触发dep中收集的所有依赖

总结

整个流程走完了,对其中重要的点总结以下:

重要的变量/类:

  1. RefImpl: ref函数返回的便是该类的实例,其中get valueset value是实现简单数据类型响应式的关键,
  2. dep属性: 简单数据类型的所有依赖都存放在了实例的dep属性中,而复杂数据类型的依赖都存放在全局变量targetMap

整体流程:

  1. 创建响应式数据时,首先会创建RefImpl实例
  2. 如果是复杂数据类型,会交给reactive进行处理
  3. 如果是简单数据类型,则将值保存在实例的_value属性上,将依赖保存在dep属性上,
  4. 通过属性访问器,来模拟proxygetset,在get value()中收集依赖,在set value()中触发依赖

实现ref模块

ref函数

主要用来创建ref响应式数据

packages/reactivity/src/ref.ts中:

import { isRef } from "@vue/shared";
/**
 * @message: 常见深层响应式数据
 */
export function ref(value?: unknown) {
  // 创建深层响应式数据
  return createRef(value, false);
}
/**
 * @message: 创建RefImpl实例,shallow是否创建浅层响应式数据
 */
function createRef<T = any>(rawValue: any, shallow: boolean): RefImpl<T> {
  if (isRef(rawValue)) {
    return rawValue;
  }
  return new RefImpl(rawValue, shallow);
}

RefImpl类

每个ref响应式数据都是RefImpl的实例

packages/reactivity/src/ref.ts中:

import { hasChange, isRef } from "@vue/shared";
import { createDep, Dep } from "./deps";
import { toReactive } from "./reactive";
import { activeEffect, trackEffets, triggerEffects } from "./effect";
/**
 * @message: ref类,用来生成ref数据实例
 */
class RefImpl<T> {
  public _rawValue: T;
  private _value: T;
  public dep?: Dep = undefined; // 存放该实例的依赖
  public readonly __v_isRef = true; // 标志位,用来判断数据是否为ref实例
  constructor(
    rawValue: T,
    public readonly __v_isShallow: boolean
  ) {
    this._rawValue = rawValue;
    this._value = __v_isShallow ? rawValue : toReactive(rawValue);
  }
  get value() {
    trackRefValue(this);
    return this._value;
  }
  set value(newValue) {
    // 值有变化时,设置新值并触发依赖
    if (hasChange(this._value, newValue)) {
      this._rawValue = newValue;
      this._value = toReactive(newValue);
      triggerRefValue(this);
    }
  }
}

/**
 * @message: 收集依赖
 */
function trackRefValue(ref) {
  if (activeEffect) {
    if (!ref.dep) {
      ref.dep = createDep();
    }
    trackEffets(ref.dep);
  }
}
/**
 * @message: 触发收集的依赖
 */
function triggerRefValue(ref) {
  triggerEffects(ref.dep);
}

toReactive函数

packages/reactivity/src/reactive.ts中:

/**
 * @message: 如果是object类型的数据,直接使用reactive创建响应式数据
 */
export function toReactive(value: any) {
  return isObject(value) ? reactive(value) : value;
}

新增工具函数

packages/shared/src/index.ts中:

/**
 * @message: 是否是ref创建的响应式数据
 */
export const isRef = (r: any): boolean => {
  return !!(r && r._v_isRef === true);
};
/**
 * @message: 检测两个值是否有差异
 */
export const hasChange = (v1: any, v2: any) => {
  return !Object.is(v1, v2);
};

总结

看完代码,就会发现刚开始的问题都有了答案:

  1. ref是如何分别处理简单数据类型和复杂数据类型的?

    ref函数根据数据类型的不同使用处理方式

    • 复杂数据类型: 交给reactive函数处理
    • 简单数据类型: 生成RefImpl实例,在实例中保存值和依赖
  2. ref的对简单数据类型的响应式是基于什么实现的?proxy吗?

    简单数据类型的响应式并不是使用proxy实现的,而是通过js的属性访问器get(),set()来实现依赖收集和依赖触发的

  3. 为什么ref构建的响应式数据需要通过.value访问?

    因为在实现简单数据类型的响应式中,定义了名为value的属性访问器,所以,访问数据时,必须通过.value才可以触发属性访问器,从而完成依赖收集和触发

转载自:https://juejin.cn/post/7396248017022025791
评论
请登录