从零实现一个vue3(三) 实现ref

317 阅读2分钟

在上一节中, 实现了 使用proxy 生成reactive,

proxy可以处理复杂数据 比如对象, 但是对简单数据 比如 数组number 无法处理, 就需要通过ref方式对简单数据进行处理

ref的本质 和 proxy是相同的, 但是 ref目标是 对简单数据 进行处理

实现目标

<div id="app"></div>
<script>
  const { ref, effect } = Vue

  const obj = ref({
    name: '张三'
  })

  // 调用 effect 方法
  effect(() => {
    document.querySelector('#app').innerText = obj.value.name
  })

  setTimeout(() => {
    obj.value.name = '李四'
  }, 2000)
</script>

实现原理

除了 proxy object.defineProperty 还有什么函数 可以 实现 读 写的时候 获取 值呢

可以通过 class 的 set get 方式 对value 进行读取

class Person {
      constructor(name, age) {
          this.name = name
          this.age = age
      }

  //__age__ = 0

  get age() {
      console.log("get is called")

      //return this.__age__
      return this.__proto__.__age__
  }
  set age(value) {
      console.log("set is called")
  
      if (value < 0) {
          //this.__age__ = value * -1
          this.__proto__.__age__ = value * -1
      } else {
          //this.__age__ = value
          this.__proto__.__age__ = value
      }
  }
}
Person.prototype.__age__ = 0
let p1 = new Person("tom", -18)
p1.age=19
console.log(p1.age, p1)

创建 packages/reactivity/src/ref.ts

import { createDep, Dep } from './dep'
import { activeEffect, trackEffects, triggerEffects } from './effect'
import { toReactive } from './reactive'
import { hasChanged } from '@vue/shared'

export interface Ref<T = any> {
  value: T
}

/**
 * ref 函数
 * @param value unknown
 */
export function ref(value?: unknown) {
  return createRef(value, false)
}

/**
 * 创建 RefImpl 实例
 * @param rawValue 原始数据
 * @param shallow boolean 形数据,表示《浅层的响应性(即:只有 .value 是响应性的)》
 * @returns
 */
function createRef(rawValue: unknown, shallow: boolean) {
  if (isRef(rawValue)) {
    return rawValue
  }
  return new RefImpl(rawValue, shallow)
}

class RefImpl<T> {
  private _value: T
  private _rawValue: T

  public dep?: Dep = undefined

  // 是否为 ref 类型数据的标记
  public readonly __v_isRef = true

  constructor(value: T, public readonly __v_isShallow: boolean) {
    // 如果 __v_isShallow 为 true,则 value 不会被转化为 reactive 数据,即如果当前 value 为复杂数据类型,则会失去响应性。对应官方文档 shallowRef :https://cn.vuejs.org/api/reactivity-advanced.html#shallowref
    // 原始数据
    this._rawValue = value
    this._value = __v_isShallow ? value : toReactive(value)
  }

  /**
   * get语法将对象属性绑定到查询该属性时将被调用的函数。
   * 即:xxx.value 时触发该函数
   */
  get value() {
    trackRefValue(this)
    return this._value
  }

  set value(newVal) {
    /**
     * newVal 为新数据
     * this._rawValue 为旧数据(原始数据)
     * 对比两个数据是否发生了变化
     */
    if (hasChanged(newVal, this._rawValue)) {
      // 更新原始数据
      this._rawValue = newVal
      // 更新 .value 的值
      this._value = toReactive(newVal)
      // 触发依赖
      triggerRefValue(this)
    }
  }
}

/**
 * 为 ref 的 value 进行依赖收集工作
 */
export function trackRefValue(ref) {
  console.log(activeEffect, '执行依赖工作')
  if (activeEffect) {
    trackEffects(ref.dep || (ref.dep = createDep()))
  }
}

/**
 * 为 ref 的 value 进行触发依赖工作
 */
export function triggerRefValue(ref) {
  if (ref.dep) {
    triggerEffects(ref.dep)
  }
}

/**
 * 指定数据是否为 RefImpl 类型
 */
export function isRef(r: any): r is Ref {
  return !!(r && r.__v_isRef === true)
}

在packages/reactivity/src/index.ts

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

在 packages/vue/src/index.ts 中

export { reactive,  effect, ref } from '@vue/reactivity';

创建example/reactivity/ref.html

<!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>
    <script src="../../dist/vue.js"></script>
  </head>
  <body>
    <div id="app">
      <p id="p1"></p>
      <p id="p2"></p>
    </div>
  </body>

  <script>
    const { ref, effect } = Vue

    const obj = ref({
      name: '张三'
    })

    console.log(obj, 66)

    const obj2 = ref('张三')
    console.log(obj2.value)

    // 调用 effect 方法
    // effect(() => {
    //   document.querySelector('#app').innerText = obj.value.name
    // })

    effect(() => {
      document.querySelector('#p2').innerText = obj2.value
    })

    setTimeout(() => {
      // obj.value.name = '李四'
      obj2.value = "李思思"
    }, 2000)

    
  </script>
</html>

运行npm run build 可以看到 ref功能实现成功

代码地址

github.com/beewolf233/…

参考文章

vue3 源码学习,实现一个 mini-vue(三):ref 的响应式