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); });
setInterval(() => { count.value++; }, 1000);
|
运行之后,你会看到控制台每秒打印一次,数字从 0 开始递增。注意这里访问值要用 count.value,因为 ref 返回的是一个对象,真正的值在 value 属性里。
为什么需要 ref?
reactive 用 Proxy 来拦截对象的读写操作,但 Proxy 只能代理对象,不能代理原始值。你试试 new Proxy(1, {}),会直接报错。所以对于原始值,我们得换个思路。
ref 的做法很巧妙:用一个对象把原始值包起来,然后对这个对象做 reactive。这样原始值就间接地变成响应式了。
1 2 3 4
| 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
| import { reactive } from './reactive.js';
export function ref(raw) { if (isRef(raw)) { return raw; } const box = { value: raw, __isRef: true }; return reactive(box); }
export function isRef(val) { return !!(val && val.__isRef === true); }
export function unref(val) { return isRef(val) ? val.value : val; }
export function toRef(obj, key) { const val = obj[key]; if (isRef(val)) { return val; } const ref = { get value() { return obj[key]; }, set value(newVal) { obj[key] = newVal; }, __isRef: true }; return ref; }
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
| 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); if (isRef(res)) { return res.value; } return isObject(res) ? reactive(res) : res; }, set(target, key, value, receiver) { const old = target[key]; 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; name.value = 'React';
|
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 } = toRefs(state);
effect(() => { console.log(`${name.value}: ${count.value}`); });
state.count = 1; state.name = 'React';
|
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 });
const countRef = toRef(state, 'count');
effect(() => { console.log('count:', countRef.value); });
state.count = 1; countRef.value = 2;
|
一些细节
ref 为什么用 .value?
因为 ref 返回的是一个对象 { value: raw, __isRef: true },真正的值在 value 属性里。这样设计的好处是:
- 统一了原始值和对象的响应式方式
- 可以通过
isRef 判断是否为 ref
- 在模板中 Vue 会自动解包,不需要写
.value
toRef 和 toRefs 的作用
当你从 reactive 对象解构时,会丢失响应式连接:
1 2 3
| const state = reactive({ count: 0 }); const { count } = state; count++;
|
用 toRefs 就能解决这个问题,解构出来的每个属性都是 ref,保持响应式。
ref 和 reactive 的选择
- ref:适合原始值,或者单个值的响应式
- reactive:适合对象,可以一次性让整个对象响应式
实际开发中,Vue3 的 Composition API 里,基本类型用 ref,对象用 reactive,这样用起来最自然。
小结
ref 的实现其实挺简单的,核心就是用对象包裹原始值,然后做 reactive。但这个小技巧解决了 reactive 无法处理原始值的问题,让整个响应式系统更完整了。
ref(raw) 把原始值变成响应式
isRef 和 unref 用来判断和解包
toRef 和 toRefs 解决解构时丢失响应式的问题
配合之前的 reactive 和 effect,现在我们已经有了一个比较完整的响应式系统基础了。后面还可以继续扩展 computed、watch 等功能。