likes
comments
collection
share

04.vue3源码学习之ref实现

作者站长头像
站长
· 阅读数 16

前言:

在实现refcomputed之前,先对以下问题做出一些思考

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,numberboolean等类型是无效

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)

得到显示页面

04.vue3源码学习之ref实现

结论:这样直接取state的属性来赋值给一个变量,变量会失去响应式连接,参考基本数据类型的赋值

let a = 10;
let b = a;
b = 20;
console.log(a); // 此时a的值是多少,是 10?还是 20?
//10

04.vue3源码学习之ref实现

总结: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