手把手搭建Vue轮子从0到1:5. Ref 模块的实现

上一章:# 手把手搭建Vue轮子从0到1:4. Reactivity 模块的实现

先思考下:

  1. ref 函数是如何进行实现的?
  2. ref 可以构建简单数据类型的响应性吗?
  3. 为什么 ref 类型的数据,必须要通过 .value 访问值呢?

这节我们就需要解决以上三个问题。

5.1. 阅读源码:ref 复杂数据类型的响应性

  1. 创建测试实例

创建 packages\vue\examples\my-examples\ref.html 文件

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Ref的响应性</title>
    
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
</head>
<body>
    <div id="app"></div>
</body>

<script>
    const { effect, ref } = Vue;

    const obj = ref({
        count: 0
    });

    effect(() => {
        document.querySelector('#app').innerHTML = `count is: ${obj.value.count}`;
    });

    setTimeout(() => {
        obj.value.count++;
    }, 1000);
    
</script>
</html>

在 packages\reactivity\src\ref.ts 中打断点

5.1.1. ref 函数

  1. 可以看到,在ref 函数中,直接触发了 createRef 函数
  1. 在 createRef 中,进行了判断:如果当前已经是一个 ref 类型数据则直接返回,否则返回 RefImpl 类型的实例。
  1. 那么 RefImpl 是什么?

    a. RefImpl 是同样位于 packages\reactivity\src\ref.ts 之下的一个类

    b. 该类的构造函数中,执行了一个 toReactive 的方法,传入了 value 并把返回值赋值给了 this._value

    toReactive的作用:将数据分成了两种类型

  1. 复杂数据类型:调用了 reactive 函数,即把 value 变为响应性的。
  2. 简单数据类型:直接把 value 原样返回

c. 提供了一个分别被 get 和 set 标记的函数 value

  1. 当执行 xxx.value 时,会触发 get 标记
  2. 当执行 xxx.value = xxx 时,会触发 set 标记
  1. ref 函数执行完成

总结:

  1. 对于 ref 而言,主要生成了 RefImpl 的实例
  2. 在构造函数中对传入的数据进行了处理:
  1. 复杂数据类型:转为响应性的 proxy 实例
  2. 简单数据类型:不去处理
  1. RefImpl 分别提供了 get value、set value 以此来完成对 getter 和 setter 的监听,需要注意这里并没有使用 proxy

5.1.2. effect函数

当 ref 函数执行完成之后,测试实例开始执行 effect 函数。

effect 函数我们之前跟踪过它的执行流程,我们知道整个 effect 主要做了3件事情:

  1. 生成 ReactiveEffect 实例
  2. 触发fn方法,从而激活 getter
  3. 建立了 targetMap 和 activeEffect 之间的联系
  1. dep.add(activeEffect)
  2. activeEffect.deps.push(dep)

通过以上可知,effect 中会触发 fn 函数,也就是说会执行 obj.value.name,那么根据 get value 机制,此时会触发 Refimpl 的 get value 方法。

5.1.3. get value()

  1. 在 get value 中会触发 trackRefValue 方法
  1. 触发 trackEffects 函数,并且在此时为 ref 新增了一个 dep 属性:
js 复制代码
trackEffects(ref.dep || (ref.dep = createDep())...)
  1. trackEffects 的主要作用就是:收集所有的依赖
  1. get value 执行完成

总结:整个 get value 的处理逻辑还是比较简单的,主要还是通过之前的 trackEffects 属性来收集依赖。

5.1.4. 再次触发 get value()

最后就是在1秒后,修改数据源

js 复制代码
 obj.value.count++;

这里需要思考一个很关键的问题:此时会触发 get value 还是 set value?

以上代码可以解析成:

js 复制代码
const value = obj.value
value.count = value.count + 1

通过以上代码,可以清晰的知道触发的应该是 get value 函数。

在 get value 函数中:

  1. 再次执行 trackRefValue 函数:

但此时的 activeEffect 为 undefined,所以不会执行后续逻辑

  1. 返回 this._value:

通过 构造函数,我们可知此时的 this._value 是经过 toReactive 函数过滤之后的数据,在当前实例中为 proxy 实例。

  1. get value 执行完成

总结:

  1. const value 是 proxy 类型的实例,即:代理对象,被代理对象为 {count: 0}
  2. 执行 value.count = value.count + 1,本质上是出发了 proxy 的 setter
  3. 根据 reactive 的执行逻辑可知,此时会触发 trigger 触发依赖
  4. 最后,修改视图

5.1.5. 总结:

  1. 对于 ref 函数,会返回 RefImpl 类型的实例
  2. 在该实例中,会根据传入的数据进行分开处理
  1. 复杂数据类型:转化为 reactive 返回的 proxy 实例
  2. 简单数据类型:不做处理
  1. 无论执行 obj.value.count 还是 obj.value.count++ 本质上都是触发了 get value
  2. 之所以会进行响应性是因为 obj.value 是一个 reactive 函数生成的 proxy

5.2. 构建 ref 函数

tsconfig.json

json 复制代码
// 入口
"include": ["packages/*/src", "packages/shared"]

packages\shared\index.ts

ts 复制代码
/**
 * 判断两个值是否相等(对比两个数据是否发生了变化)
 * @param value 
 * @param oldValue 
 * @returns 
 */
export function hasChanged(value: any, oldValue: any): boolean {
    return !Object.is(value, oldValue)
}

packages\reactivity\src\ref.ts

ts 复制代码
import { Dep, createDep } from "./dep"
import { triggerEffects, trackEffects } from "./effect"
import { hasChanged } from "../../shared"
import { reactive } from "./reactive"

/**
 * 响应式转换
 * 将值转换为响应式对象
 * @param value 
 * @returns 
 */
function toReactive(value: any) {
    return typeof value === 'object' && value !== null
        ? reactive(value) // 对象转为响应式 
        : value // 基础类型直接返回
}

class RefImpl<T> {
    // 响应式数据(当前值)
    private _value: T
    // 原始数据
    private _rawValue: T
    // 依赖集合
    public readonly _dep: Dep | null = null

    constructor(value: T, public readonly _shallow: boolean = false) {
        // 原始数据
        this._rawValue = value
        // 初始化 _value,转换为响应式
        this._value = toReactive(value)
        // 初始化依赖
        this._dep = createDep()
    }

    /** 
     * 收集依赖 
     * 当 ref 的 value 属性被访问时,收集依赖
     * @returns 
     */
    get value() {
        // 收集依赖
        trackRefValue(this)
        return this._value
    }


    /**
     * 触发更新
     * 当 ref 的 value 属性被修改时,触发依赖
     * @param newVal 
     */
    set value(newVal) {
        // newVal 为新数据
        // this._rawValue 为原始数据
        // 如果新数据和原始数据不一致,则更新数据
        if (hasChanged(newVal, this._rawValue)) {
            this._rawValue = newVal
            this._value = toReactive(newVal)
            triggerRefValue(this)
        }
    }
}

/**
 * 依赖收集
 * 为 ref 的 value 属性收集依赖
 * @param ref 
 */
export function trackRefValue(ref: RefImpl<any>) {
    if (ref._dep) {
        // 收集当前 effect 到依赖种
        trackEffects(ref._dep)
    }
}

/**
 * 依赖触发
 * 为 ref 的 value 属性触发依赖
 * @param ref 
 */
export function triggerRefValue(ref: RefImpl<any>) {
    if (ref._dep) {
        // 触发所有依赖的 effect
        triggerEffects(ref._dep)
    }
}

/**
 * 创建 ref 对象
 * @param value 
 * @returns 
 */
export function ref<T>(value: T) {
    return new RefImpl(value)
}

packages\reactivity\src\index.ts

ts 复制代码
export { reactive } from './reactive'
export { effect } from './effect'
export { ref } from './ref'

packages\vue\src\index.ts

ts 复制代码
export { reactive, effect, ref } from '@vue/reactivity'

创建测试实例,运行。测试成功,表示代码完成。

packages\vue\examples\ref-mvp.html

html 复制代码
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Ref MVP 实现演示</title>
    <script src="../dist/vue.js"></script>
  </head>
  <body>
    <div id="app">
      <h2>Ref MVP 实现演示</h2>
      <div id="content"></div>
      <button onclick="changeValue()">改变值</button>
      <button onclick="changeObject()">改变对象</button>
    </div>
  </body>
  <script>
    const { effect, ref } = Vue

    // 基础类型 ref
    const count = ref(0)
    const message = ref('Hello Vue Mini!')

    // 对象类型 ref
    const user = ref({
      name: '张三',
      age: 25
    })

    // 响应式效果
    effect(() => {
      const content = document.getElementById('content')
      content.innerHTML = `
        <p>基础类型 ref:</p>
        <p>count: ${count.value}</p>
        <p>message: ${message.value}</p>
        <p>对象类型 ref:</p>
        <p>user.name: ${user.value.name}</p>
        <p>user.age: ${user.value.age}</p>
      `
    })

    // 改变基础类型值
    function changeValue() {
      count.value++
      message.value = '值已更新: ' + Date.now()
    }

    // 改变对象值
    function changeObject() {
      user.value.name = '李四'
      user.value.age = 30
    }

    // 3秒后自动更新
    setTimeout(() => {
      count.value = 100
      message.value = '自动更新完成!'
    }, 3000)
  </script>
</html>

核心特性:

  1. 基础类型响应式:通过 getter/setter 实现
  2. 对象类型响应式:内部使用 reactive() 转换
  3. 依赖收集:读取 .value 时收集当前 effect
  4. 依赖触发:设置 .value 时触发所有依赖
  5. 值变化检测:使用 hasChanged() 避免不必要的更新

ref 简单数据类型响应性:

简单数据类型,不具备数据件监听的概念,即本身并不是响应性的。只是因为 vue 通过 set value() 的语法,把 函数调用变成了属性调用的形式,让我们能主动调用该函数,来完成一个"类似于"响应性的结构。

5.3. 总结

  1. ref 函数是如何进行实现的?

ref 函数本质是生成了一个 RefImpl 类型的实例对象,通过 get 和 set 标记处理了 value 函数

  1. ref 可以构建简单数据类型的响应性吗?

ref 可以构建简单数据类型的响应性

  1. 为什么 ref 类型的数据,必须要通过 .value 访问值呢?
  1. 因为 ref 需要处理简单数据类型的响应性,但是对于简单数据类型而言,它无法通过 proxy 建立代理
  2. 所以 vue 通过 get value() 和 set value() 定义了两个属性函数,通过 主动 触发这两个函数(属性调用)的形式来进行 依赖收集 和 触发依赖
  3. 所以必须通过 .value 来保证响应性
相关推荐
Antonio9154 分钟前
【网络编程】WebSocket 实现简易Web多人聊天室
前端·网络·c++·websocket
tianzhiyi1989sq1 小时前
Vue3 Composition API
前端·javascript·vue.js
今禾1 小时前
Zustand状态管理(上):现代React应用的轻量级状态解决方案
前端·react.js·前端框架
用户2519162427111 小时前
Canvas之图形变换
前端·javascript·canvas
今禾2 小时前
Zustand状态管理(下):从基础到高级应用
前端·react.js·前端框架
gnip2 小时前
js模拟重载
前端·javascript
Naturean2 小时前
Web前端开发基础知识之查漏补缺
前端
curdcv_po2 小时前
🔥 3D开发,自定义几何体 和 添加纹理
前端
单身汪v2 小时前
告别混乱:前端时间与时区实用指南
前端·javascript
鹏程十八少2 小时前
2. Android 深度剖析LeakCanary:从原理到实践的全方位指南
前端