百行代码实现ref

203 阅读4分钟

上一篇文章已经实现了一个简单的reactive 实现Vue3中的reactive,这篇文章一起实现一下ref

我们先看一下vue官方的ref功能介绍

ref 对象是可更改的,也就是说你可以为 .value 赋予新的值。它也是响应式的,即所有对 .value 的操作都将被追踪,并且写操作会触发与之相关的副作用。

如果将一个对象赋值给 ref,那么这个对象将通过 reactive() 转为具有深层次响应式的对象。这也意味着如果对象中包含了嵌套的 ref,它们将被深层地解包。

若要避免这种深层次的转换,请使用 shallowRef() 来替代。

对于使用ref还是reactive,官方已经给出了答案:建议使用ref。其实ref在对于对象进行处理的时候,底层用到的还是reactive,不过ref可以实现对单个变量实现响应式,同时还可以在对象完全变换的时候进行重新出发依赖。

那我们先看一下vue是怎么实现ref的吧

先说一下如何调试vue源码。

  1. clone vue的core仓库,然后选择指定的版本,我这里选择的是3.2.37
  2. 依赖安装pnpm i
  3. 打包 pnpm build
  4. 然后在packages\vue\examples下建个html,引入dist打包的vue文件,写一些需要调试的代码,然后可以快乐的打断点了。

先写一段调试代码,如下

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>ref</title>
  </head>
  <body>
    <div id="app">
      <p id="p1"></p>
      <p id="p2"></p>
    </div>
    <script src="../../dist/vue.runtime.global.js"></script>

    <script>
      const { ref, effect } = Vue

      const info = ref(12)

      effect(() => {
        document.querySelector('#p1').innerHTML = info.value
      })

      setTimeout(() => {
        info.value = 22
      }, 2000)
    </script>
  </body>
</html>

然后在ref处打上断点

image.png 然后进入ref内部看看是如何实现的

image.png

可以看到ref内部调用了createRef函数,函数接受两个参数一个是当前传入的值,一个是false。

继续进入createRef函数

image.png

这里,我们就不关注那些细节了。createRef函数内部返回一个RefImpl的实例,也是ref实现的核心代码

image.png

这里有个变量isShallow,在开头官方的一段话里就说了,如果不想实现深层的响应式,可以用shallowRef。这个isShallow就是为了实现shallowRef的。

变量dep大家应该很熟悉了,存储相关的依赖函数,不熟悉的可以去看我上一篇文章 点击查看如何实现Vue3中的reactive

变量 __v_isRef 判断当前是不是ref对象

变量 rawValue 存储对象的原始值

变量 value 存储当前的响应性值。

那ref是如何实现既可以响应式单个变量值,可以实现多个变量的值呢?

vue是这样实现的

  1. 如果传入的是对象,则将对象转换为reactive对象并赋值给value。
  2. 如果传入的是单个变量,则直接赋值给value

reactive已经实现了响应式,那如何实现单个变量的响应式呢?

在RefImpl里对于value劫持了get和set行为,当我们去调用ref.value的时候就会收集当前的依赖,给value赋值的时候就会将收集的依赖触发实现响应性。

大致的思维导图如下

image.png

那我们开始实现ref吧,首先建一个ref文件,导出一个ref函数

export function ref(value: any) {
  return createRef(value, false)
}

接着实现createRef函数

function createRef(value: any, isShallow: boolean) {
  if (isRef(value)) {
    return value
  }
  return new RefImpl(value, isShallow)
}

接着实现RefImpl类

class RefImpl {
  public __v_isShallow: boolean
  public dep: undefined | Dep
  public __v_isRef: boolean
  public _value: any

  constructor(value: any, isShallow: boolean) {
    this.__v_isShallow = isShallow
    this.dep = undefined
    this.__v_isRef = isRef(value)
    this._value = isShallow ? value : toReactive(value)
  }

  get value() {
    console.log(this._value)
    trackRefValue(this)
    return this._value
  }

  set value(newValue) {
    this._value = this.__v_isShallow ? newValue : toReactive(newValue)
    triggerRefValue(this)
  }
}

实现toReactive函数

function toReactive(value: any) {
  return isObject(value) ? reactive(value) : value
}

实现trackRefValue依赖搜集

function trackRefValue(ref: RefImpl) {
  if (!ref.dep) {
    ref.dep = createDep()
  }
  if (activeEffect) {
    ref.dep.add(activeEffect)
  }
}

实现triggerRefValue依赖触发

function triggerRefValue(ref: RefImpl) {
  if (ref.dep) {
    triggerEffects(ref.dep)
  }
}

在index.ts文件中引入ref函数并导出

export { effect } from './effect'
export { reactive } from './reactive'
export { ref } from './ref'

然后,我们进行打包,并测试上面写的代码,符合要求则实现了ref功能。具体代码请移步到我的github
github.com/Wkb2317/my-…

总结

有了实现reactive的经验后 (移步文章实现Vue3中的reactive),我们实现ref就很简单了。对于复杂对象底层用reactive进行处理,并将代理对象复制给ref的value字段;对于单一变量直接复制给value字段(这也是为啥ref要通过.value进行获取数据)。然后对于操作value的get和set行为进行拦截,和proxy类似,在get时进行依赖收集,在set进行依赖触发。因为是修改的value下的属性,所以不管是复杂对象还是单一变量都可以触发响应式。

完整代码: github.com/Wkb2317/my-…,欢迎start

实现Vue3中的reactive