从 ref 和 reactive 入手,理解 Vue 响应式数据的创建方式
实现响应式
要实现响应式,第一步是先得到一个“响应式数据”。
在 Vue 3 中,最常见的创建方式就是 ref 和 reactive:
const count = ref(0)
const state = reactive({
name: 'vue',
age: 3
})
这两个 API 表面上用法不太一样:
ref通常用来包装基础类型,也可以包装对象reactive只能接收对象类型- 使用
ref时需要通过.value读取和修改数据 - 使用
reactive时可以直接通过对象属性访问
但它们要解决的问题是一样的:拦截数据的读取和修改。
在 js 中,想要拦截数据变化,常见方式主要有两种:
Object.definePropertyProxy
Vue 2 使用的是 Object.defineProperty,而 Vue 3 的响应式系统主要基于 Proxy 实现。
从源码中复制了一下并精简
ref 的实现
// ref 的实现
function ref(value) {
if (isRef(value)) {
return value
}
return new RefImpl(value)
}
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() {
// 依赖收集
}
set value(newVal) {
// 派发更新
}
}
export function toReactive<T extends unknown>(value: T): T {
return isObject(value) ? reactive(value) : value
}
ref 的核心其实就是创建了一个 RefImpl 实例。
这也是为什么使用 ref 时需要 .value 的原因:真正被拦截的不是变量 count 本身,而是 RefImpl 实例上的 value 属性。
读取 count.value 时,会进入 get value();修改 count.value 时,会进入 set value()。
这两个入口就是做依赖收集和派发更新的关键。
reactive 的实现
再看 reactive。
// reactive 的实现
function createReactiveObject(
target: Target,
isReadonly: boolean,
baseHandlers: ProxyHandler<any>,
collectionHandlers: ProxyHandler<any>,
proxyMap: WeakMap<Target, any>
) {
// ...
const proxy = new Proxy(
target,
targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
)
proxyMap.set(target, proxy)
return proxy
}
export function reactive(target: object) {
// ...
return createReactiveObject(
target,
false,
mutableHandlers,
mutableCollectionHandlers,
reactiveMap
)
}
reactive 的核心是通过 new Proxy() 创建代理对象。
当我们读取属性时,会触发 Proxy 的 get 拦截;当我们修改属性时,会触发 Proxy 的 set 拦截。
到这里可以得到一个结论:
ref是通过get value和set value拦截.valuereactive是通过Proxy拦截对象属性- 虽然它们的实现方式不同, 但最终都是在
读取和修改这两个时机在处理
响应式的本质
响应式的核心可以拆成两件事:
- 依赖收集
- 派发更新
依赖收集
简单来说,就是在读取响应式数据时,把当前正在执行的副作用函数记录下来。
effect(() => {
console.log(state.count)
})
执行这段代码时会读取 state.count,所以响应式系统需要知道:
这个
effect依赖了state.count。
这样后面 state.count 发生变化时,才能重新执行对应的 effect。
派发更新
在修改响应式数据时,找到之前收集过的依赖,并重新执行。
state.count++
总结
当 count 改变后,响应式系统会找到依赖 state.count 的副作用函数,然后触发更新。
所以,一个最简化的响应式流程大概是:
- 执行
effect - 读取响应式数据,触发
get - 在
get中收集当前effect - 修改响应式数据,触发
set - 在
set中找到依赖并重新执行