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);
}
// ...

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

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

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

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

WARNING

由于refshallowRef都使用triggerRefValue更新视图

如果shallowRefref同时设置值, 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() 函数,并传入 effectdebuggerEventExtraInfo 两个参数。

其中,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>