Skip to content

Ref 全家桶 & 源码解析 ​

示例 ​

{ "name": "小满" }
{ "name": "小满" }


{ "name": "小满2" }


小满


我是dom

源码解析 ​

Vue Core: https://github.com/vuejs/core

ref 跟 shallowRef ​

packages/reactivity/src/ref.ts

ts
// ...
export function ref(value?: unknown) {
  return createRef(value, false);
}
// ...
export function shallowRef(value?: unknown) {
  return createRef(value, true);
}
// ...

都是调用createRef()

先调用一次isRef判断值是否 ref, 返回原始值或者一个RefImpl对象

ts
// ...
function createRef(rawValue: unknown, shallow: boolean) {
  if (isRef(rawValue)) {
    return rawValue;
  }
  return new RefImpl(rawValue, shallow);
}
// ...

ref和shallowRef: 实例化RefImpl时向构造函数参数__v_isShallow传入不同布尔值(false/true)

  • 如果是shallowRef, 值(_value)和原始值(_rawValue)均为值本身
  • 如果是ref, 值(_value)会调用一次toReactive将传入值转换为响应式对象, 原始值(_rawValue)调用toRaw返回值本身

读取ref和shallowRef时分别返回响应式_value和原始value

更新时会先判断是否为浅响应式对象, 对新值使用toReactive再包装一遍或直接使用新值, 最后对视图进行更新(triggerRefValue)

WARNING

由于ref跟shallowRef都使用triggerRefValue更新视图

如果shallowRef跟ref同时设置值, shallowRef作用跟ref一样

ts
// ...
class RefImpl<T> {
  private _value: T;
  private _rawValue: T;

  public dep?: Dep = undefined;
  public readonly __v_isRef = true;

  constructor(value: T, public readonly __v_isShallow: boolean) {
    this._rawValue = __v_isShallow ? value : toRaw(value);
    this._value = __v_isShallow ? value : toReactive(value);
  }

  get value() {
    trackRefValue(this);
    return this._value;
  }

  set value(newVal) {
    const useDirectValue =
      this.__v_isShallow || isShallow(newVal) || isReadonly(newVal);
    newVal = useDirectValue ? newVal : toRaw(newVal);
    if (hasChanged(newVal, this._rawValue)) {
      this._rawValue = newVal;
      this._value = useDirectValue ? newVal : toReactive(newVal);
      triggerRefValue(this, newVal);
    }
  }
}
// ...

triggerRefValue

若ref.dep存在则进行依赖更新

ts
// ...
export function triggerRefValue(ref: RefBase<any>, newVal?: any) {
  ref = toRaw(ref);
  const dep = ref.dep;
  if (dep) {
    if (__DEV__) {
      triggerEffects(dep, {
        target: ref,
        type: TriggerOpTypes.SET,
        key: "value",
        newValue: newVal,
      });
    } else {
      triggerEffects(dep);
    }
  }
}
// ...

packages/reactivity/src/effect.ts

triggerEffects

触发依赖更新

TIP

原代码

ts
for (const effect of isArray(dep) ? dep : [...dep]) {
    if (effect !== activeEffect || effect.allowRecurse) {
      if (__DEV__ && effect.onTrigger) {
        effect.onTrigger(extend({ effect }, debuggerEventExtraInfo))
      }
      if (effect.scheduler) {
        effect.scheduler()
      } else {
        effect.run()
      }
    }
}

这个变更来源于一次Issues:
Github Issues: Computed values are not consistent in watch callback

Evan you进行了一次修复:
Github Commit: fix(reactivity): ensure computed is invalidated before other effects

  1. 首先将传入的 dep 转换为数组形式。
  2. 第一次遍历执行计算属性的副作用更新。
  3. 第二次遍历执行普通的副作用更新。
ts
// ...
export function triggerEffects(
  dep: Dep | ReactiveEffect[],
  debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
  // spread into array for stabilization
  const effects = isArray(dep) ? dep : [...dep];
  for (const effect of effects) {
    if (effect.computed) {
      triggerEffect(effect, debuggerEventExtraInfo);
    }
  }
  for (const effect of effects) {
    if (!effect.computed) {
      triggerEffect(effect, debuggerEventExtraInfo);
    }
  }
}
// ...

triggerEffect

触发副作用

  1. 如果当前的 effect 不是激活状态下的 activeEffect,或者 effect 允许递归调用,则执行下一步操作。
  2. 如果 effect 设置了 scheduler,则调用 scheduler 函数,否则直接调用 effect.run() 执行 effect 副作用函数。
  3. 如果设置了 effect.onTrigger,则调用 effect.onTrigger() 函数,并传入 effect 和 debuggerEventExtraInfo 两个参数。

其中,effect.allowRecurse 属性用来控制副作用函数是否可以递归调用。默认情况下,为 false,也就是不允许递归调用。这是为了避免无限递归调用的情况发生。

debuggerEventExtraInfo 参数用于调试目的,可以在触发副作用函数时传递一些额外信息,用于调试和分析代码的执行情况。

所以,triggerEffect 的作用是触发副作用函数 effect,根据 effect 的属性和设置来控制 effect 函数的执行时机和方式。

ts
// ...
function triggerEffect(
  effect: ReactiveEffect,
  debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
  if (effect !== activeEffect || effect.allowRecurse) {
    if (__DEV__ && effect.onTrigger) {
      effect.onTrigger(extend({ effect }, debuggerEventExtraInfo));
    }
    if (effect.scheduler) {
      effect.scheduler();
    } else {
      effect.run();
    }
  }
}
// ...

代码 ​

vue
<template>
  <!-- ref -->
  <div>{{ Man }}</div>
  <div>
    {{ Man1 }}
  </div>
  <hr />
  <button @click="change">修改</button>

  <hr />

  <!-- shallowRef -->
  <div>{{ Man4 }}</div>
  <hr />
  <button @click="change1">修改</button>
  <hr />

  <!-- customRef -->
  <div>{{ Man5 }}</div>
  <hr />
  <button @click="change2">修改</button>
  <hr />

  <!-- ref获取dom元素 -->
  <div ref="dom" @click="change3">我是dom</div>
  <hr />
</template>

<!-- <script lang="ts">
export default {
  data() {
    return {
      // 只有data()中return的元素才是响应式的
      age: 18
    }
  }
}
</script> -->

<script setup lang="ts">
// 深层次响应式
import { ref } from "vue";
// 浅层次响应式
import { shallowRef } from "vue";
// 检查是否为ref对象
import { isRef } from "vue";
// 更新视图
import { triggerRef } from "vue";
// 自定义Ref
import { customRef } from "vue";
import type { Ref } from "vue";
const Man = { name: "小满" };
type M = {
  name: string;
};
// 自动类型推导
const Man1 = ref({ name: "小满" });
// Ref
const Man2: Ref<M> = ref({ name: "小满" });
// 自己写
const Man3 = ref<M>({ name: "小满" });

const Man4 = shallowRef({ name: "小满2" });

const change = () => {
  Man.name = "大满";
  Man1.value.name = "大满";
  console.log(Man);
  console.log(isRef(Man1));
};

const change1 = () => {
  // shallowRef无法响应到value内部
  // (如果跟ref一起用, 造成视图更新(调用了triggerRef), shallowRef就会响应到value内部)
  Man4.value.name = "大满2";
  // 通过triggerRef强制更新视图
  triggerRef(Man4);
  // 需要修改整个value
  /* Man4.value = {
    name: '大满2'
  } */
};

const Man5 = MyRef<string>("小满");

// 自己实现ref
function MyRef<T>(value: T) {
  // customRef接收一个回调函数
  // track收集依赖
  // trigger触发依赖
  return customRef((track, trigger) => {
    // 要求返回一个包含get,set的对象
    return {
      get() {
        track();
        return value;
      },
      set(newVal) {
        value = newVal;
        trigger();
      },
    };
  });
}
const change2 = () => {
  Man5.value = "customRef修改啦";
};

const dom = ref<HTMLDivElement>();
const change3 = () => {
  console.log(dom.value?.innerText);
};
</script>