本文介绍了vue3中的ref的实现原理,还介绍了响应丢失(toRef、toRefs)的情况,以及自动脱ref是如何实现的,参考《Vue.js设计与实现》 更多Vue源码文章:
1. Vue3 源码解析(一):响应式数据和副作用函数、计算属性原理、侦听器原理
6. Vue3源码解析(三):如何代理Set和Map数据结构
快速回答版
- 介绍vue当中的ref的作用和内部原理
- 将原始数据类型如
数字、字符串、布尔值
等转化为响应式数据
- 内部实现是用一个
对象包裹原始数据
,属性为value,将对象传递给reactive函数
- ref能够解决
响应丢失问题
,toRefs和toRef函数内部的实现原理和ref一致,借助了get拦截器
- vue3如何处理自动脱ref
- 自动脱ref的定义:当访问ref响应式数据时,希望能够直接使用数据,
不通过.value
,在模版中常见 - 实现方式:
- vue3封装了
proxyRefs
函数,判断数据如果是ref数据
(借助__v_isRef标识),则直接返回他的.value
的内容。 setup
里面返回的变量会自动传递
给这个函数
- vue3封装了
reactive
也能够实现自动脱ref
1. ref原理
Proxy无法对原始数据类型(包括number string boolean null undefined bigInt symbol)
做代理,所以ref的实现必须得嵌套一层对象。
其内部实现如下:
js
function ref (val) {
const wrapper = {
value: val
}
Object.defineProperty(wrapper, '__v_ifRef', {
value: true
})
return reactive(wrapper)
}
- 第一,必须用wrapper包裹val的值,属性是
value
,将该值传递给reactive函数转化为响应式数据。这也是使用ref的数据必须用.value
来访问的原因 - 给wrapper创建了一个
__v_ifRef
属性,用来区分是原始数据类型
还是引用数据类型
2. 解决响应丢失问题
如下解构obj对象的值赋值给newObj
对象,并在副作用函数中访问,期待修改obj.foo为10
以后副作用函数重新执行,但是如下并不能执行。因为newObj只是一个普通的对象,不会建立响应联系
js
const obj = reactive({
foo: 1,
bar: 2
})
const newObj = {
...obj
}
effect(() => {
console.log(newObj.foo, 'newObj.foo');
});
obj.foo = 10
toRef函数
和toRefs函数
内部实现原理:
- toRef的返回值就是ref,借助ref来实现响应丢失问题
- toRefs是批量调用toRef
js
function toRef(obj, key) {
const wrapper = {
get value () {
// 访问器函数里面,访问obj[key],访问obj这个proxy对象的某个属性
return obj[key]
}
}
return wrapper
}
function toRefs(obj) {
const ret = {}
// 针对每个属性调用toRef,整个对象都转化为响应式
for (const key in obj) {
ret[key] = toRef(obj, key)
}
return ret
}
此时修改数据能够触发响应
js
const obj = reactive({
foo: 1,
bar: 2
})
const newObj = {
...toRefs(obj)
}
effect(() => {
console.log(newObj.foo.value, 'newObj.foo');
});
3. 自动脱ref
定义:定义一个ref响应式数据,在某些场合希望能够使用数据不用通过.value
。
举例:
- 模版当中,直接访问newObj.foo,而不是newObj.foo.value
- reactive包裹一个ref数据,也能够给他脱ref
这样写就会很麻烦
html
<p>{{ newObj.foo.value }}</p>
希望能够这样写
html
<p>{{ newObj.foo }}</p>
reactive函数的脱ref能力:
js
const count = ref(0)
const obj = reactive({ count })
obj.count // 0
实现原理:
- vue3会把setup函数里面返回的响应式数据传递给proxyRefs函数,进行自动脱ref
- 借助
__v_isRef
标识来判断是否是响应式数据
js
function proxyRefs (target) {
return new Proxy(target, {
get (target, key, receiver) {
const value = Reflect.get(target, key, receiver)
return value.__v_isRef ? value.value : value
}
})
}
4. 请看源码
diff
/packages/reactivity/src/ref.ts
export function ref(value?: unknown) {
return createRef(value, false)
}
function createRef(rawValue: unknown, shallow: boolean) {
if (isRef(rawValue)) {
return rawValue
}
return new RefImpl(rawValue, shallow)
}
class RefImpl<T = any> {
_value: T
private _rawValue: T
dep: Dep = new Dep()
public readonly [ReactiveFlags.IS_REF] = true
public readonly [ReactiveFlags.IS_SHALLOW]: boolean = false
constructor(value: T, isShallow: boolean) {
this._rawValue = isShallow ? value : toRaw(value)
+ this._value = isShallow ? value : toReactive(value) // 这里调用了toReactive
this[ReactiveFlags.IS_SHALLOW] = isShallow
}
......省略下面的代码
}
js
/packages/reactivity/src/reactive.ts
export const toReactive = <T extends unknown>(value: T): T =>
isObject(value) ? reactive(value) : value
proxyRefs
函数
diff
export function proxyRefs<T extends object>(
objectWithRefs: T,
): ShallowUnwrapRef<T> {
return isReactive(objectWithRefs)
? objectWithRefs
+ : new Proxy(objectWithRefs, shallowUnwrapHandlers) // 请关注这个shallowUnwrapHandlers
}
const shallowUnwrapHandlers: ProxyHandler<any> = {
get: (target, key, receiver) =>
key === ReactiveFlags.RAW
? target
+ : unref(Reflect.get(target, key, receiver)), // 这里执行了unref方法
set: (target, key, value, receiver) => {
const oldValue = target[key]
if (isRef(oldValue) && !isRef(value)) {
oldValue.value = value
return true
} else {
return Reflect.set(target, key, value, receiver)
}
},
}
export function unref<T>(ref: MaybeRef<T> | ComputedRef<T>): T {
+ return isRef(ref) ? ref.value : ref // 如果是ref数据,直接返回他.value的值
}