vue3源码系列(二)——ref, toRef, toRefs篇

224 阅读1分钟

上篇介绍了vue3创建响应式数据的apireactive,接下来将介绍同样可以创建响应式数据的apiref。在介绍ref源码之前先看看reactive和ref有什么区别。

  1. reactive底层是基于Proxy来实现的,而ref底层是通过Object.defineProperty来实现的。
  2. ref可以接受基础类型和对象(走的还是reactive的逻辑),而reactive只能接受对象。
  3. ref取值时会多一层

1、ref

import { trackEffects, triggerEffects } from "./effect";
import { reactive } from "./reactive";

function toReactive(value) { // 判断传入的参数,如果时对象,就走reactive的逻辑,如果是基础类型直接返回值
    return isObject(value) ? reactive(value) : value;
}

// ref函数
export function ref(value) {
    return new RefImpl(value);
}

class RefImpl{
    public _value;
    public dep = new Set;  // 储存依赖的set集合
    public __v_isRef = true; // ref对象的标识
    constructor(public rawValue) {
        this._value = toReactive(rawValue);
    }
    get value() {
        trackEffects(this.dep); // 收集依赖(对应的effe)
        return this._value;
    }
    set value(newValue) {
        if(newValue !== this.rawValue) {

            this._value = toReactive(newValue);

            this.rawValue = this._value;

            triggerEffects(this.dep); // 触发更新(执行对应的函数)
        }
    }
}

// effect.ts的trackEffects,triggerEffects方法
export function trackEffects(dep) {
    if(activeEffect) {
        dep.add(activeEffect);
    }	
}

export function triggerEffects(effects) {
    effects = new Set(effects); // 让循环的effects和上一个effects使用不同的引用,防止循环引用造成死循环
    effects.forEach(effect => {

            // 注意: 这里要避免run触发trigger,要不会造成死循环
        if(effect !== activeEffect) {
            if(effect.scheduler) {
                    effect.scheduler(); // 如果用户传入了调度函数,则用用户的
            }else {
                    effect.run();  // 否则默认刷新视图
            }
        }
    })
}

调试代码:

<!DOCTYPE html>
<html lang="en">
    <head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    </head>
    <body>

    <div id="app"></div>

    <script src="./reactivity.global.js"></script>

    <script>

        const { ref, effect } = VueReactivity

        const personName = ref('张三')

        effect(() => {
          document.getElementById('app').innerHTML = `名字${personName.value}`
        })

        setTimeout(() => {
          personName.value = '李四'
        }, 2000)

    </script>
    </body>
</html>

以上代码可知ref方法返回一个RefImpl实例。需要注意的是在访问该实例的值,或者改变该实例的值时要加上value属性,因为value是该实例上的一个属性。如上测试代码中personName.value会触发get方法,进行以来收集,与reactive不同的是,收集的依赖是储存在该实例的dep属性中。当执行personName.value = '李四'时,会触发set方法,触发更新。ref创建的响应式数据格式如下:

QQ图片20221013183707.png

2、toRef

在介绍源码之前先讲下其功能和具体使用场景。当我们通过reactive创建一个响应式对象时,为了更加方便的去使用该对象的属性值,我们通常会使用解构赋值的方式去拿到该变量,这样我们拿到的值可能就不是一个响应式变量了,如下:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>

  <div id="app"></div>
  
  <script src="./reactivity.global.js"></script>

  <script>

    const { reactive, effect } = VueReactivity

    const personInfo = reactive({
      name: '张三',
      age: 18,
      friendInfo: {
        name: '李四',
        age: '18'
      }
    })

    const {name, age, friendInfo} = personInfo
    console.log(name, age, friendInfo) // name,age已经不是响应式变量了,
  </script>
</body>
</html>

打印结果为:

2222222.png

如上图可以name,age已经不是响应式变量了,而friendInfo仍然还是响应式的,这是为什么呢?其实之前没解构之前name,age可以做到响应式是因为personInfo是响应式对象,在personInfo.name,personInfo.age的时候会触发get从而进行依赖收集。而当从personInfo中解构name,age时,解构的两个值,即当然不会是响应式的了。而friendInfo还是响应式是因为在reactive创建响应式对象的时候,如果其某个属性的值是对象的话会进行递归,让其值也变成响应式对象,所以这时解构出的friendInfo也是一个值,只是这个值本身就是个响应式对象。那怎么让name,age也变成响应式的呢?继续看如下代码:

<!--
 * @Description: 
 * @Version: 2.0
 * @Autor: 杨
 * @Date: 2022-10-11 14:45:45
 * @LastEditors: 杨
 * @LastEditTime: 2022-10-14 14:56:15
-->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>

  <div id="app"></div>
  
  <script src="./reactivity.global.js"></script>

  <script>

    const { reactive, effect, toRef } = VueReactivity

    const personInfo = reactive({
      name: '张三',
      age: 18,
      friendInfo: {
        name: '李四',
        age: '18'
      }
    })

    + const name = toRef(personInfo, 'name') // 让name变成响应式变量
    + const age = toRef(personInfo, 'age') // // 让age变成响应式变量
    
    console.log(name, age)

    effect(() => {
      document.getElementById('app').innerHTML = `名字:${name.value}  年龄:${age.value}`
    })

    setTimeout(() => {
      name.value = '王五'
      age.value = 19
    }, 2000)
    
  </script>
</body>
</html>

接下来看看toRef是怎么做到让name,age变成响应式的。

class ObjectRefImpl{ // 只是将.value属性代理到原始类型上
    constructor(public object, public key) {

    }
    get value() {
        return this.object[this.key]; // 触发get方法时会直接访问源响应式对象
    }
    set value(newValue) {
        this.object[this.key] = newValue; // 触发set时也是直接改变源响应式对象的对应属性的值
    }
}

export function toRef(object, key) {
    return new ObjectRefImpl(object, key);
}

其实toRef做的事很简单,就是在你name.value的时候做了一下劫持,直接去访问源响应式对象的属性,这样就会触发依赖收集,同理给name.value设置值得时候也是直接操作源响应式对象的。

3、toRefs

toRefs其实就是基于toRef来实现的,这里不做过多的赘述。看如下代码:

export function toRefs(object) {
    const result = isArray(object) ? new Array(object.length) : {};

    for(let key in object) {
            result[key] = toRef(object, key);
    }
    return result;
}

toRefs做的就是基于toRef,一次性将解构出来的变量变成响应式的。用法如下:

<!--
 * @Description: 
 * @Version: 2.0
 * @Autor: 杨
 * @Date: 2022-10-11 14:45:45
 * @LastEditors: 杨
 * @LastEditTime: 2022-10-14 15:36:25
-->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>

  <div id="app"></div>
  
  <script src="./reactivity.global.js"></script>

  <script>

    const { reactive, effect, toRef } = VueReactivity

    const personInfo = reactive({
      name: '张三',
      age: 18,
    })

    // const name = toRef(personInfo, 'name')
    // const age = toRef(personInfo, 'age')

    const {name, age} = toRefs(personInfo) 
    
    console.log(name, age)

    effect(() => {
      document.getElementById('app').innerHTML = `名字:${name.value}  年龄:${age.value}`
    })

    setTimeout(() => {
      name.value = '王五'
      age.value = 19
    }, 2000)
    
  </script>
</body>
</html>