摘要

在页面编写中,之前都是使用 reactive 来包裹对象,这样对象属性的值改变,其对应的 effect 包裹渲染动作就会被触发。并且通常有函数解构的操作,例如let people = reactive({name:"张三",age:24}); let {name,age} = people;。如果这么结构操作的话,name 和 age 就会变成普通的变量,那么如果在 effect 中使用的话,就算改变了值也不会触发回调函数。那么为了解决例如此类的需求,vue3 提供了ref、toRef、toRefs

官方中的写法探究

引入官方的包,尝试了几种简单的写法。

<!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 class="" id="app"></div>
  <script src="./dist/reactivity.global.js"></script>
  <script>
    const { reactive, effect,toRef,ref,toRefs,proxyRefs } = VueReactivity
    // let people = reactive({name:"张三",age:99})
   let name = ref("张三")
   let age = ref(23)
   let people = proxyRefs({name,age})
  // let {name,age} = toRefs(people)
    effect(() => {
      app.innerHTML = `${people.name}今年${people.age}岁`
    })

    setTimeout(() => {
      people.name = "利斯"
    },1000)
  </script>
</body>
</html>

可以用 Ref 包裹基础数据类型,也可以包裹引用型数据。让数据变成响应式数据,可以被 effect 收集,然后更新数据触发回调函数。

编写 Ref

首先开始编写 ref.ts,ref 会被传入基础数据类型和引用型数据。所以方法要判断一下数据类型。

import { isArray, isObject } from "@vue/shared";
import { trackEffect, triggerEffect } from "./effect";
import { reactive } from "./reactive";
function toReactive(value) {
  return isObject(value) ? reactive(value) : value;
}

class RefImpl {
  public _value;
  constructor(public rawValue) {
    this._value = toReactive(rawValue);
  }

  get value() {
    return this._value;
  }

  set value(newValue) {
    if (newValue !== this.rawValue) {
      this._value = toReactive(newValue);
      this.rawValue = newValue;
    }
  }
}

function ref(value) {
  return new RefImpl(value);
}

上面是一个初步的对象包裹,但是还没加入依赖收集。现在加入****源码学习-7****中分离的依赖收集函数和更新触发的函数。

......
import { isArray, isObject } from '@vue/shared'
import { trackEffect, triggerEffect } from './effect'
import { reactive } from './reactive'
function toReactive(value){
    return isObject(value) ? reactive(value) : value
}

class RefImpl{
    public _value
    public dep = new Set()
    public __v_isRef = true
    constructor(public rawValue){
        this._value = toReactive(rawValue)
    }

    get value(){
        trackEffect(this.dep)
        return this._value;
    }

    set value(newValue){
        if(newValue !== this.rawValue){
            this._value = toReactive(newValue);
            this.rawValue = newValue;
            triggetEffect(this.dep)
        }
    }
}

function ref(value){
    return new RefImpl(value)
}

这样就完成了 Ref 的基本功能。现在有了包裹对象,那么就有第二中需求,结构这种需求。toRefs 和 toRef 就是 vue3 提供的函数。

......
class ObjectRefImpl {
  constructor(public object, public key) {}

  get value() {
    return this.object[this.key]
  }

  set value(newValue) {
    this.object[this.key] = newValue
  }
}

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

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 的单个版本实现,所以基于 toRefs 就基于 toRef 实现就行。
另外可以观察到 vue3 中 Ref 包裹的对象在 template 中使用发现不需要 xx.value,这是 vue3 官方做的一个优化,实际就像是 ref 包裹的反向过程。

编写 proxyRefs

上面知道了 vue3 提供了一个 proxyRefs 这样的函数,来使得 ref 对象使用更加方便了。

......
export function proxyRefs(object) {
  return new Proxy(object, {
    get(target, key, recevier) {
      let r = Reflect.get(target, key, recevier)
      return r.__v_isRef ? r.value : r
    },

    set(target, key, value, receiver) {
      let oldValue = target[key]
      if (oldValue.__v_isRef) {
        oldValue.value = value
        return true
      } else {
        return Reflect.set(target, key, value, receiver)
      }
    },
  })
}

template 模板上通常都是 get 数据那么就是代理对象反射这个 .value这样,为啥要用 Reflect 来反射,是因为 this 的指向问题。第三篇文章分析了这个问题。

Waiting