04.vue3源码学习之ref实现
前言:
在实现ref
和computed
之前,先对以下问题做出一些思考
1.为什么在有reactive
利用了proxy
实现了数据劫持后,还需要用到ref
2.为什么ref
的值调用要用.value的形式来取值
3.ref
的性能比reative
高?这到底是不是真的
带着这些问题,那我们就开始我们的探索之旅
1.reactive()的局限性
1.为什么不用reactive()
一个API把所有的对象,数组,字符串,布尔等类型,全部代理成为响应式?
参考:proxy说明,proxy(target,handler)
,对于target
的说明:要使用 Proxy
包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理),那我们就理解了Proxy
的代理仅对对象类型生效(对象,数组,Map
,Set
这样的集合数组),而对原始类型string
,number
和boolean
等类型是无效
2.为什么对reactive()代理出的响应式对象,在那些时候会失去响应式?
1.代理对象重新赋值
<template>
<div @click="state.count++">测试 {{ state.count }}</div>
</template>
let state = reactive({ count: 0 })
// 上面的引用 ({ count: 0 }) 将不再被追踪(响应性连接已丢失!)
state = { count: 1 }
console.log(state) // { "count": 1}
可以看到,state被重新赋值后,他已经不是proxy会包裹的代理对象了,原因其实很简单:Vue的响应式系统是通过属性访问进行追踪的,因此我们必须始终保持对该响应式对象的相同引用,简单来说,就是我们这样重新赋值,堆内存空间已经改变
2.属性的重新赋值,属性的结构,属性传入一个函数
<template>
<div @click="state.count++">测试 {{ state.count }} {{ n }}</div>
</template>
let state = reactive({ count: 0 })
// n 是一个局部变量,同 state.count
// 失去响应性连接
let n = state.count
// 不影响原始的 state
n++
let { count } = state
// 不会影响原始的 state
count++
// 该函数接收一个普通数字,并且将无法跟踪 state.count 的变化
callSomeFunction(state.count)
得到显示页面
结论:这样直接取state的属性来赋值给一个变量,变量会失去响应式连接,参考基本数据类型的赋值
let a = 10;
let b = a;
b = 20;
console.log(a); // 此时a的值是多少,是 10?还是 20?
//10
总结:reactive()
的种种限制归根到底是js没有可以作用于所有值类型的“引用”机制
2.ref()的诞生
基于reative()的限制,所有Vue提供了一个ref()
方法允许我们可以创建可以使用任何值类型的响应式的ref
const count = ref(0)
console.log(count)
//RefImpl {
// "__v_isShallow": false,
// "__v_isRef": true,
// "_rawValue": 0,
// "_value": 0
// }
console.log(count.value) // 0
count.value++
console.log(count.value) // 1
ref()
将传入参数的值包装成为一个带.value属性的ref对象,和响应式的对象属性类型,ref的.value
属性也是响应式的,
一个包含对象类型值的 ref 可以响应式地替换整个对象:
const objectRef = ref({ count: 0 })
// 这是响应式的替换
objectRef.value = { count: 1 }
ref 被传递给函数或是从一般对象上被解构时,不会丢失响应性:
const obj = {
foo: ref(1),
bar: ref(2)
}
// 该函数接收一个 ref
// 需要通过 .value 取值
// 但它会保持响应性
callSomeFunction(obj.foo)
// 仍然是响应式的
const { foo, bar } = obj
简言之,ref() 让我们能创造一种对任意值的 “引用”,并能够在不丢失响应性的前提下传递这些引用。
但以下情况,仍跟reactive()
一样
let state = ref({ count: 0 })
let n = state.value.count
// 不影响原始的 state
n++
let { count } = state.value
// 不会影响原始的 state
count++
// 该函数接收一个普通数字,并且将无法跟踪 state.count 的变化
callSomeFunction(state.value.count)
3.ref()的实现
1.ref和reactive的区别 reactive内部采用的proxy ref中内部使用的是defineProperty
2.ref的实现过程也是采用了函数柯里化的方式
3.本质上实现响应式,还是在get的时候调用track函数实现依赖收集,在set的时候,调用trigger函数实现依赖更新
- 实现ref函数
export function ref(value){
//将普通类型变成一个对象
return createRef(value) //通过柯里化,可以通过不同的传参,来得到不同的ref
}
function createRef(rawValue,shallow = false){
return new RefImpl(rawValue,shallow)
}
- 实现RefImpl类
const convert = (val) => isObject(val) ? reactive(val) : val
class RefImpl {
public _value //表示 声明了一个_value属性 但是没有赋值
public _v_isRef = true //产生的实例会被添加 _v_isRef 表示是一个ref属性
constructor(public rawValue,public shallow) { // 参数前面增加一个修饰符 标识此属性放到了实例上
this._value = shallow ? rawValue : convert(rawValue) //如果是深度 需要把里面的都变成响应式的
}
//类的属性访问器
get value(){ // 代理 取值取value 会帮我们代理到 _value上
track(this,'get','value')
return this._value
}
set value(newValue){
//比较两个值是否变化
if(hasChanged(newValue,this.rawValue)){
this.rawValue = newValue //新增会作为老值
this._value = this.shallow ? newValue : convert(newValue)
trigger(this,'set',value',newValue)
}
}
}
其实ref代码实现的过程很简单,大致上可以理解为:
1.判断传入进来的值,是不是对象类型的,如果是的话那么就调用reactive()
进行劫持
2.如果不是对象,那么就调用的是defineProperty
实现劫持方法
3.如果在对ref
进行set
的时候,如果赋值的是一个对象,那么就将该对象,再次调用reactive
进行劫持
4.总结
那我们再对开头提出来的问题进行回顾
1..为什么在有reactive
利用了proxy
实现了数据劫持后,还需要用到ref
答:reactive
是有很多局限性的,而且他调用的是proxy进行代理,只能对对象类型的进行代理劫持
2.为什么ref
的值调用要用.value的形式来取值
答:我们希望ref() 让能创造一种对任意值的 “引用”,所以我们在内部将传入的值包装成为一个带.value属性的ref对象,并且让他具备响应式
3.ref
的性能比reative
高?这到底是不是真
答:不是真的,因为ref
中传入的值是对象类型的时候,他内部会调用reactive
,所以并不会存在ref
的性能比reative
高
转载自:https://juejin.cn/post/7248249730480193593