ref -> 响应式数据创建
功能概述
ref和reactive一样,都是创建响应式数据的方法,reactive是创建复杂数据类型的响应式代理,而ref则是创建简单数据类型的响应式代理;
当然ref也可以处理复杂数据类型的响应式代理,只不过需要进行单独转换;
其核心就是通过维护一个对象,该对象只有一个
value属性,然后给该对象设置proxy代理;
- 需要具备的响应式功能
- 和
reactive类似 只不过在进行依赖收集或触发时只通过单一的.value的方式(reactive收集依赖是需要根据key收集很多的deps,ref只有一个key,也就只有一个deps) - 当进行更新时如果值没有改变就不进行依赖触发,不进行更新操作
- 和
依赖收集
ref和reactive一样,也是需要进行依赖收集的,只是由于ref只需要维护一个
value的属性,其依赖只会是value的,所以不用像reactive那样走很复杂的逻辑(targetMap -> depsMap -> deps)
依赖触发
依赖触发是在set的时候进行的,但是不需要进行复杂的key值捕获
ref常用API拓展
isRef
是用于判断一个对象是否被ref实例对象所包裹;
有了
isReactive和isReadonly的基础isRef也是同样的处理方式,通过添加一个public __v_isRef = true的属性进行后续判断
unRef
省略ref调用值时的
.value操作,直接进行数据的操作获取;
当使用.value太频繁的时候,不知道后面的值到底有没有.value,此时就可以用该API进行包裹访问
proxyRefs
当访问的是 ref 类型的值时 返回.value;
当访问的不是 ref 类型的值时 直接返回value;
帮助我们脱离ref的value限制,让我们可以直接访问响应式数据,而不需要通过value间接访问
在Vue3中的setup中,不管在模板中有没有使用
.value,都可以进行.value的省略,因为setup内部返回的结果就调用了这个API
- proxyRefs需求分析
proxyRefs可以对数据进行get和set,并且还有对应的处理- 在
get的时候,会自动把.value省略掉 - 在
set的时候,需要区分是普通值还是ref;proxyUser.age = ref(10)
- 功能实现
- 由于需要劫持对象,因此需要使用
Proxy来进行代理劫持 - 在进行
get的时候,判断获取的结果是ref还是普通的,如果是ref的话,默认调用.value - 对于
set的话,同样需要判断set的内容是不是ref,且需要判断set之前的值是什么类型的set的是内容是普通值,且原来的值是ref,需要调用.value来赋值- 否则,直接替换即可
/** * * @param obj * @returns */ export function proxyRefs(obj) { return new Proxy(obj, { get(target, key) { const val = Reflect.get(target, key) // 返回的结果类型判断 return isRef(val) ? val.value : val }, set(target, key, val) { // set需要判断新内容和set之前的内容是否为ref if (isRef(target[key]) && !isRef(val)) { // 新值是普通值,原来的值是ref类型 - 调用.value来替换 return target[key].value = val } else { // 直接替换 return Reflect.set(target, key, val) } } }) } - 由于需要劫持对象,因此需要使用
shallowRef
- 只处理基本数据类型的响应式,不进行对象的响应式处理
- 有一个对象数据,后续功能不会修改该对象中的属性,而是生成新的对象来替换时使用
shallowRef - 当使用该API代理对象的时候,对象默认是不带有响应式的,需要进行强制更新页面DOM的操作 -
triggerRef
- 有一个对象数据,后续功能不会修改该对象中的属性,而是生成新的对象来替换时使用
const state = shallowRef({ count: 1 })
// 不会触发更改
state.value.count = 2
// 会触发更改
state.value = { count: 2 }
<button @click="changeNage">重命名</button>
<div>{{name}}</div>
type Obj = {
name: string
}
let name:Ref<Obj> = shallowRef({
name: "Lbxin"
})
const changeName = () => {
name.value.name = "Lbxin-11"
triggerRef(message)
}
toRef - 访问代理:解决响应式数据丢失的问题
可以为源响应式对象上的某个属性新创建一个ref,且ref可以被
传递,会保持对源属性的响应式连接
import { ref, toRef } from "vue";
// 使用 toRef 后 两个变量数据 会产生链式关系 互相响应 一个数据发送改变 另一个也会跟随 改变
const user = ref({
name: "Lbxin",
age: 22,
});
const newAge = toRef(user.value, "age");
newAge.value = 20;
console.log(user.value.age); // 20
user.value.age = 18;
console.log(newAge.value); // 18
内部实现
- 实现思路
- 接收两个参数,第一个是响应式数据,第二个是响应式数据的一个键,通过返回类似
Ref结构的对象进行响应式获取
- 接收两个参数,第一个是响应式数据,第二个是响应式数据的一个键,通过返回类似
- 代码逻辑
function toRef(obj,key){
const result = {
get value(){
return obj[key]
},
set value(val){
obj[key] = val
}
}
// 为了与真正的ref数据同步,需要增加__v_isRef的标识
Object.defineProperties(result,'__v_isRef',{
value: true
})
return result
}
const newObj = {
userName: toRef(userInfo,'name'),
userAge: toRef(userInfo,'age'),
}
toRefs - 访问代理:解决响应式数据丢失的问题
将一个响应式对象转换为普通对象,但是内部的数据还是响应式的
import { ref, toRefs } from "vue";
// 使用 toRef 后 两个变量数据 会产生链式关系 互相响应 一个数据发送改变 另一个也会跟随 改变
const user = ref({
name: "Lbxin",
age: 22,
});
const newAge = toRefs(user.value);
newAge.age.value = 20;
console.log(user.value.age); // 20
user.value.age = 18;
console.log(newAge.value); // 18
内部实现
- 解决思路
- 将响应式数据转换为类似于
ref结构的数据 - 在某些方面上来说,
toRefs转换后的结果要视为真正的ref数据
- 将响应式数据转换为类似于
- 实现代码
function toRefs(obj) {
const result = {};
for (const key in obj) {
if (Object.hasOwnProperty.call(obj, key)) {
// 通过调用`toRef`来实现响应式转换
result[key] = toRef(obj, key);
}
}
}
const newObj = {...toRefs(userInfo)}
存在的问题
- 通过
toRefs转换的响应式数据的访问还是需要通过繁琐的xxx.value的方式进行实现,而常规的数据是可以直接通过键值对的方式进行访问的,这时就需要用到下文说的proxyRefs来包裹实现了,内部是在读取ref的值时,直接返回ref的value属性值,这样就实现了自动脱ref;- Vue中的setup函数返回的数据会传递给
ProxyRefs函数进行处理
- Vue中的setup函数返回的数据会传递给
- 同样的,在进行设置值时也需要通过繁琐的
xxx.value的方式进行实现,为了解决这个问题,可以通过proxyRefs的Set拦截进行实现,在进行设置时如果是__v_isRef标识,则直接设置xxx.value = newValue属性值即可;
ref的作用不仅仅是实现基本数据类型的响应式方案,它还用来解决响应式数据丢失的问题(丢失一般都是拓展运算、浅克隆等造成的)
自动脱ref的操作不仅仅在上述的场景中有应用,在reactive函数中也有自动脱ref的能力
ref及相关API内部实现与测试
原理解析
Vue3响应式数据通过ref、reactive实现、监听函数通过effect实现,reactive借助proxy实现,computed借助effect实现
ref和effect内部是借助
track跟踪和trigger触发代理来实现响应式的;
测试用例
describe('ref', () => {
it('happy path', () => {
// 用ref包裹一个数字1 期待.value返回值1
const r = ref(1)
expect(r.value).toBe(1)
});
it('should be a reactive', () => {
// 在effect中获取用ref包裹起来的r.value的值 期望dunny返回值为1
// 在进行重复复制同样的值时 期望结果值不再变化
const r = ref(1)
let dunny,
calls = 0;
effect(() => {
calls++;
dunny = r.value
})
expect(calls).toBe(1)
expect(dunny).toBe(1)
r.value = 2
expect(calls).toBe(2)
expect(dunny).toBe(2)
//验证在数据没有变化的前提下 effect中额的逻辑不会执行
r.value = 2
expect(calls).toBe(2)
expect(dunny).toBe(2)
});
it('should make nested propeerties reactive', () => {
// ref也可以进行传递对象进行响应式代理
const r = ref({
count: 1
})
let dummy
effect(() => {
dummy = r.value.count
})
expect(dummy).toBe(1)
r.value.count = 2
expect(dummy).toBe(2)
});
it('isRef', () => {
// 判断当前数据是否为ref类型
const r = ref(1)
const user = reactive({
name: "Lbxin"
})
expect(isRef(r)).toBe(true)
expect(isRef(user)).toBe(false)
expect(isRef(1)).toBe(false)
});
it('unRef', () => {
// 如果参数为ref,则返回内部的值,否则返回参数本身
const r = ref(1)
const user = reactive({
name: "Lbxin"
})
expect(unRef(r)).toBe(1)
expect(unRef(1)).toBe(1)
});
it('proxyRefs', () => {
// 当访问的是 ref 类型的值时 返回.value
// 当访问的不是 ref 类型的值时 直接返回value
const user = {
age: ref(20),
name: "Lbxin"
}
const proxyUser = proxyRefs(user)
expect(proxyUser.age).toBe(20)
expect(proxyUser.name).toBe('Lbxin')
expect(user.age.value).toBe(20)
proxyUser.age = ref(18)
expect(proxyUser.age).toBe(18)
expect(user.age.value).toBe(18)
proxyUser.age = ref(22)
expect(proxyUser.age).toBe(22)
expect(user.age.value).toBe(22)
});
});
内部实现
class RefImpl {
private _value:any;
public dep;
public __v_isRef = true //用于表明是否是ref类的响应式代理
private _rawValue:any; //保存旧值 防止reactive转换后与value不一样
constructor(value){
this._rawValue = value
// 这里需要对传入的值进行数据类型判断 对象时需要依赖reactive
this._value = convert(value)
// dep 初始化
this.dep = new Set()
}
get value() {
// 依赖收集
trackRefValue(this);
return this._value
}
set value(newValue) {
// hasChanged 只有在新值发生变化的时候才进行依赖触发
if(hasChanged(newValue,this._rawValue)){
this._rawValue = newValue
this._value = convert(newValue)
// 触发依赖更新
triggerEffects(this.dep)
}
}
}
export function trackRefValue(ref) {
// 和effect一样 当没有通过effect进行代理监听时 就不用进行依赖收集 不然会找不到需要手机的effect实例 即 activeEffect为undefined
if (isTracking()) {
// 进行依赖收集 ref是单一的.value进行操作的 所以直接进行依赖收集即可
// 在trackEffects中需要进行添加的前置判断 没有再进行依赖收集
trackEffects(ref.dep);
}
}
export function triggerRefValue(ref) {
triggerEffects(ref.dep);
}
function convert(value) {
//对象类型的数据需要转换为 reactive 对象代理
return isObject(value) ? reactive(value) : value;
}
export function ref(value) {
return new RefImpl(value)
}
export function isRef(value) {
// 判断当前数据是否为ref类型
return !!value['__v_isRef']
}
export function unRef(value) {
// 如果参数为ref,则返回内部的值,否则返回参数本身
return isRef(value) ? value.value : value
}
export function proxyRefs(obj) {
return new Proxy(obj, {
get(target, key) {
const val = Reflect.get(target, key)
// 返回的结果类型判断
return isRef(val) ? val.value : val
},
set(target, key, val) {
// set需要判断新内容和set之前的内容是否为ref
if (isRef(target[key]) && !isRef(val)) {
// 新值是普通值,原来的值是ref类型 - 调用.value来替换
return target[key].value = val
} else {
// 直接替换
return Reflect.set(target, key, val)
}
}
})
}
export function proxyRefs(value) {
return new Proxy(value, {
get(target,key){
return unRef(Reflect.get(target,key))
},
set(target,key,value){
if(isRef(target[key]) && !isRef(value)){
target[key] = value
} else {
return Reflect.set(target,key,value)
}
}
})
}
注意点
- 当ref传入的是对象的时候需要进行复杂处理
- 传入值为对象的时候可以利用
reactive进行代理处理 -convert函数的左右 _rawValue的私有属性,保存最新的value值,方便后续的新旧值进行比较(set时用到),当没有此值的时候,新的值就被处理成了reactive,无法进行相等比较了 -hasChange函数的作用
- 传入值为对象的时候可以利用