ref 函数的实现

ref 的作用很简单,就是让那些原始值(比如数字、字符串)也能变成响应式的。 你可能要问了,reactive 不是已经能处理对象了吗?为什么还要 ref?

其实问题就在这里。reactive 只能处理对象,你给它传个数字或者字符串,它直接原样返回,根本不会变成响应式。但实际开发中,我们经常需要让一个简单的数字或者字符串也能响应式更新,这时候 ref 就派上用场了。

你可能会想,那我把原始值包成对象不就行了?比如 { value: 1 },然后用 reactive 包裹。没错,ref 的核心思路就是这个,只不过它帮你把这个过程封装好了,用起来更方便。

先看个简单的例子,感受一下 ref 的效果:

1
2
3
4
5
6
7
8
9
10
11
12
import { ref, effect } from "vue";

const count = ref(0);

effect(() => {
console.log('count 现在是:', count.value);
});

// 每秒加1
setInterval(() => {
count.value++;
}, 1000);

运行之后,你会看到控制台每秒打印一次,数字从 0 开始递增。注意这里访问值要用 count.value,因为 ref 返回的是一个对象,真正的值在 value 属性里。

为什么需要 ref?

reactive 用 Proxy 来拦截对象的读写操作,但 Proxy 只能代理对象,不能代理原始值。你试试 new Proxy(1, {}),会直接报错。所以对于原始值,我们得换个思路。

ref 的做法很巧妙:用一个对象把原始值包起来,然后对这个对象做 reactive。这样原始值就间接地变成响应式了。

1
2
3
4
// ref 的核心思路
const count = ref(0);
// 等价于
const count = reactive({ value: 0 });

但 ref 还做了更多事情,比如加了个 __isRef 标记,这样就能区分这是 ref 还是普通的 reactive 对象。后面会用到这个标记。

模块化实现

在之前的响应式系统基础上,我们新增一个 ref.js 文件:

1
2
3
4
5
6
7
reactivity/
├── effect.js // ReactiveEffect / effect / scheduler
├── track.js // track / trigger / targetMap
├── reactive.js // reactive / Proxy
├── handlers.js // mutableHandlers
├── ref.js // ref / isRef / unref / toRef / toRefs
└── index.js // 使用示例

reactivity/ref.js

内容与功能说明:

  • ref(raw):把原始值包成 { value: raw, __isRef: true },然后用 reactive 包裹
  • isRef(val):检查是否有 __isRef 标记,判断是否为 ref
  • unref(val):如果是 ref 就返回 .value,否则原样返回
  • toRef(obj, key):把对象某个属性转成 ref,保持响应式连接
  • toRefs(obj):把对象所有属性都转成 ref,常用于解构时保持响应式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// ref.js
import { reactive } from './reactive.js';

// 创建 ref:原始值 -> { value: raw } -> reactive
export function ref(raw) {
// 如果已经是 ref,直接返回
if (isRef(raw)) {
return raw;
}
// 用对象包裹原始值,加上标记
const box = { value: raw, __isRef: true };
// 对这个对象做 reactive,这样 value 的读写就能被拦截了
return reactive(box);
}

// 判断是否为 ref
export function isRef(val) {
return !!(val && val.__isRef === true);
}

// 解包 ref:如果是 ref 就取 value,否则原样返回
export function unref(val) {
return isRef(val) ? val.value : val;
}

// 把对象某个属性转成 ref
// 这样解构时还能保持响应式连接
export function toRef(obj, key) {
const val = obj[key];
// 如果已经是 ref,直接返回
if (isRef(val)) {
return val;
}
// 创建一个 ref,但它的 value 和原对象的属性保持连接
const ref = {
get value() {
return obj[key];
},
set value(newVal) {
obj[key] = newVal;
},
__isRef: true
};
return ref;
}

// 把对象所有属性都转成 ref
// 常用于解构,比如 const { count, name } = toRefs(state)
export function toRefs(obj) {
const ret = {};
for (const key in obj) {
ret[key] = toRef(obj, key);
}
return ret;
}

更新 reactivity/handlers.js

为了让 ref 在模板中自动解包(Vue3 的特性),我们需要在 handlers 里处理一下。不过这里先实现基础版本,自动解包可以后面再加。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// handlers.js(更新版)
import { track, trigger } from './track.js';
import { reactive } from './reactive.js';
import { isRef, unref } from './ref.js';

const isObject = (v) => v !== null && typeof v === 'object';

export const mutableHandlers = {
get(target, key, receiver) {
const res = Reflect.get(target, key, receiver);
track(target, key);
// 如果返回的是 ref,自动解包(这是 Vue3 的行为)
if (isRef(res)) {
return res.value;
}
return isObject(res) ? reactive(res) : res;
},
set(target, key, value, receiver) {
const old = target[key];
// 如果新值是 ref,需要解包
const newValue = isRef(value) ? value.value : value;
const ok = Reflect.set(target, key, newValue, receiver);
if (old !== newValue) trigger(target, key);
return ok;
}
};

使用示例

1) 基本用法

1
2
3
4
5
6
7
8
9
10
11
import { ref, effect } from './ref.js';

const count = ref(0);
const name = ref('Vue');

effect(() => {
console.log(`${name.value} 的计数是: ${count.value}`);
});

count.value = 1; // 触发 effect
name.value = 'React'; // 触发 effect

2) ref 与 reactive 配合

1
2
3
4
5
6
7
8
9
10
11
import { reactive, ref, effect } from './reactive.js';

const state = reactive({ user: { name: 'Tom' } });
const age = ref(18);

effect(() => {
console.log(`${state.user.name} 今年 ${age.value} 岁`);
});

state.user.name = 'Jerry'; // 触发
age.value = 20; // 触发

3) toRefs 解构保持响应式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { reactive, toRefs, effect } from './reactive.js';

const state = reactive({ count: 0, name: 'Vue' });

// 直接解构会丢失响应式
// const { count, name } = state; // ❌ 不响应

// 用 toRefs 解构,保持响应式
const { count, name } = toRefs(state); // ✅ 响应

effect(() => {
console.log(`${name.value}: ${count.value}`);
});

state.count = 1; // 触发 effect
state.name = 'React'; // 触发 effect

4) toRef 单个属性转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { reactive, toRef, effect } from './reactive.js';

const state = reactive({ count: 0 });

// 把 count 转成 ref
const countRef = toRef(state, 'count');

effect(() => {
console.log('count:', countRef.value);
});

// 修改原对象或 ref 都会触发
state.count = 1; // 触发
countRef.value = 2; // 也触发

一些细节

ref 为什么用 .value?

因为 ref 返回的是一个对象 { value: raw, __isRef: true },真正的值在 value 属性里。这样设计的好处是:

  1. 统一了原始值和对象的响应式方式
  2. 可以通过 isRef 判断是否为 ref
  3. 在模板中 Vue 会自动解包,不需要写 .value

toRef 和 toRefs 的作用

当你从 reactive 对象解构时,会丢失响应式连接:

1
2
3
const state = reactive({ count: 0 });
const { count } = state; // count 不再是响应式的
count++; // 不会触发任何更新

toRefs 就能解决这个问题,解构出来的每个属性都是 ref,保持响应式。

ref 和 reactive 的选择

  • ref:适合原始值,或者单个值的响应式
  • reactive:适合对象,可以一次性让整个对象响应式

实际开发中,Vue3 的 Composition API 里,基本类型用 ref,对象用 reactive,这样用起来最自然。

小结

ref 的实现其实挺简单的,核心就是用对象包裹原始值,然后做 reactive。但这个小技巧解决了 reactive 无法处理原始值的问题,让整个响应式系统更完整了。

  • ref(raw) 把原始值变成响应式
  • isRefunref 用来判断和解包
  • toReftoRefs 解决解构时丢失响应式的问题

配合之前的 reactive 和 effect,现在我们已经有了一个比较完整的响应式系统基础了。后面还可以继续扩展 computed、watch 等功能。