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

上一章:手把手搭建Vue轮子从0到1:3. 响应系统的核心设计原则

响应式核心:

  1. reactive
  2. ref
  3. computed
  4. watch

4.1. 阅读源码

4.1.1. Reactive 做了什么?

记得我们在 2.3(手把手搭建Vue轮子从0到1:2. 搭建框架雏形)节 中构建的测试实例 reactive 吗?我们在里面打断点,看看 reactive 究竟做了什么。

  1. 触发 reactive 方法
  2. 创建了 reactive 对象:createReactiveObject
  1. new Proxy(target:传入的对象,handler:baseHandlers )

TargetType.COLLECTION = 2; targetTypye = 1; 所以handler 为 baseHandlers

  1. baseHandlers(触发 createReactiveObject 时传递的第三个参数:mutableHandlers)
  2. mutableHandlers 位置:packages/reactivity/src/baseHandlers.ts


  1. 添加 get、set 方法断点(只有在取值、赋值时执行,所以此时这2个断点还不会执行)

get 方法声明在 BaseReactiveHandler 类里

set 方法声明在 MutableReactiveHandler 类里

  1. 在 createReactiveObject 方法最后执行了 proxyMap.set(target, proxy) 方法。
  1. 最后返回代理对象,reactive 方法执行完成。

以上流程可以总结成:

  1. 创建 proxy
  2. 把 proxy 加到 proxyMap 里
  3. 最后返回 proxy

4.1.2. effect

Vue3 Effect源码解析

总结:

  1. 创建 proxy
  2. 收集 effect 的依赖
  3. 触发收集的依赖

4.2. 构建 reactive 函数,获取 proxy 实例

packages\reactivity\src\baseHandlers.ts

js 复制代码
/**
 * 响应性的 handler 函数
 */
export const mutableHandlers: ProxyHandler<object> = {}

packages\reactivity\src\reactive.ts

js 复制代码
import { mutableHandlers } from './baseHandlers'

/**
 * 响应性对象的缓存(Map 缓存对象)
 * key: target 被代理的对象
 * value: proxy 代理对象
 */
export const reactiveMap = new WeakMap<object, any>()


/**
 * 为复杂数据类型,创建响应性对象
 * @param target 被代理的对象
 * @returns 如果 target 是复杂数据类型,则返回代理对象;如果 target 是简单数据类型,则返回 target
 */
export function reactive(target: object) {
    return createReactiveObject(target, mutableHandlers, reactiveMap)
}


/**
 * 创建响应性对象
 * @param target 被代理的对象
 * @param baseHandlers 基础的代理处理函数
 * @param proxyMap 响应性对象的缓存
 * @returns 
 */
export function createReactiveObject(target: object, baseHandlers: ProxyHandler<object>, proxyMap: WeakMap<object, any>) {
    // 如果该实例已经被代理,则直接读取即可
    const existingProxy = proxyMap.get(target)
    if (existingProxy) {
        return existingProxy
    }

    // 如果该实例没有被代理,则创建代理,生成 proxy 实例
    const proxy = new Proxy(target, baseHandlers)

    // 将 proxy 实例缓存起来
    proxyMap.set(target, proxy)
    return proxy
}

packages\reactivity\src\index.ts

js 复制代码
export { reactive } from './reactive'

packages\vue\src\index.ts

js 复制代码
export { reactive } from '@vue/reactivity'

执行 npm run build 进行打包,生成 vue.js

packages\vue\dist\vue.js

js 复制代码
var Vue = (function (exports) {
    'use strict';

    /**
     * 响应性的 handler 函数
     */
    var mutableHandlers = {};

    /**
     * 响应性对象的缓存(Map 缓存对象)
     * key: target 被代理的对象
     * value: proxy 代理对象
     */
    var reactiveMap = new WeakMap();
    /**
     * 为复杂数据类型,创建响应性对象
     * @param target 被代理的对象
     * @returns 如果 target 是复杂数据类型,则返回代理对象;如果 target 是简单数据类型,则返回 target
     */
    function reactive(target) {
        return createReactiveObject(target, mutableHandlers, reactiveMap);
    }
    /**
     * 创建响应性对象
     * @param target 被代理的对象
     * @param baseHandlers 基础的代理处理函数
     * @param proxyMap 响应性对象的缓存
     * @returns
     */
    function createReactiveObject(target, baseHandlers, proxyMap) {
        // 如果该实例已经被代理,则直接读取即可
        var existingProxy = proxyMap.get(target);
        if (existingProxy) {
            return existingProxy;
        }
        // 如果该实例没有被代理,则创建代理,生成 proxy 实例
        var proxy = new Proxy(target, baseHandlers);
        // 将 proxy 实例缓存起来
        proxyMap.set(target, proxy);
        return proxy;
    }

    exports.reactive = reactive;

    return exports;

})({});
//# sourceMappingURL=vue.js.map

创建测试实例:

packages\vue\examples\reactive.html

html 复制代码
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>reactive 测试实例</title>
    <script src="../dist/vue.js"></script>
  </head>
  <body></body>
  <script>
    const { reactive } = Vue

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

    console.log(obj)
  </script>
</html>

运行 Live Server 可以看到打印了一个 proxy 对象实例

以上我们已经得到了一个基础的 reactive 函数,但是在 reactive 函数中还存在三个问题:

  1. WeakMap 是什么?它和 Map 有什么区别?
  2. mutableHandlers 应该如何实现?
  3. 如何每次测试时,不用重新打包?

4.3. WeakMap 是什么?它和 Map 有什么区别?

WeakMap 是什么?它和 Map 有什么区别?

4.4. createGetter 和 createSetter 的实现

对于 Proxy 来说,它的 handler 可以监听代理对象的 getter 和 setter,那么此时 mutableHandlers 就是监听代理对象 getter 和 setter 的核心部分。

4.4.1. track、trigger

packages\reactivity\src\effect.ts

js 复制代码
/**
 * 收集依赖
 * @param target 被代理的对象(WeakMap 的 key)
 * @param key 被代理对象的属性(被代理对象 的 key,当依赖被触发时,需要根据 key 找到对应的 effect 函数)
 */
export function track(target: object, key: string | symbol) {
    console.log('收集依赖')
}

/**
 * 触发依赖
 * @param target 被代理的对象(WeakMap 的 key)
 * @param key 被代理对象的属性(Map 的 key,当依赖被触发时,需要根据 key 找到对应的 effect 函数)
 * @param newValue 新值
 */
export function trigger(target: object, key?: string | symbol, newValue?: any) {
    console.log('触发依赖')
}

4.4.2. getter、setter

packages\reactivity\src\baseHandlers.ts

js 复制代码
import { track, trigger } from './effect'

/**
 * getter 回调方法
 */
const get = createGetter()

/**
 * 创建 getter 回调函数
 * @returns 
 */
function createGetter() {
  return function get(target: object, key: string | symbol, receiver: object) {
    // 利用 Reflect.get 获取 target 对象的 key 属性值(得到返回值)
    const res = Reflect.get(target, key, receiver)
    // 收集依赖
    track(target, key)
    return res
  }
}

/**
 * setter 回调方法
 */
const set = createSetter()

/**
 * 创建 setter 回调函数
 * @returns 
 */
function createSetter() {
  return function set(target: object, key: string | symbol, value: any, receiver: object) {
    // 利用 Reflect.set 设置 target 对象的 key 属性值(设置新值)
    const res = Reflect.set(target, key, value, receiver)
    // 触发依赖
    trigger(target, key)
    return res
  }
}

/**
 * 响应性的 handler 函数
 */
export const mutableHandlers: ProxyHandler<object> = {
  get,
  set
}

4.4.3. 测试

packages\vue\examples\reactive.html

html 复制代码
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>reactive 测试实例</title>
    <script src="../dist/vue.js"></script>
  </head>
  <body></body>
  <script>
    const { reactive } = Vue

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

    console.log(obj.name) // 触发 track
    obj.name = '李四' // 触发 trigger
    console.log(obj.name) // 触发 track
  </script>
</html>

4.5. 热更新的开发时(提升开发体验)

在 package.json 中添加 dev 指令:

rollup -c -w:-c 读取配置文件,-w 监听源文件是否有改动,如果有改动就重新打包

json 复制代码
"dev": "rollup -c -w",

执行 npm run dev ,然后修改源代码,就可以发现项目被重新打包了。这样就可以得到一个 dev 的热更新状态。

4.6. 构建 effect 函数,生成 ReactiveEffect 实例

packages\reactivity\src\effect.ts

js 复制代码
/**
 * 创建响应式 effect 函数
 * @param fn 执行方法
 * @returns 以 ReactiveEffect 实例为 this 的执行函数
 */
export function effect<T>(fn: () => T) {
    // 生成 ReactiveEffect 实例
    const _effect = new ReactiveEffect(fn)
    // 执行 ReactiveEffect 实例的 run 方法
    _effect.run()
}

/**
 * 单例的,当前的 effect
 */
export let activeEffect: ReactiveEffect | undefined

/**
 * 响应性触发依赖时的执行类
 */
export class ReactiveEffect<T = any> {
    constructor(public fn: () => T) { }

    run() {
        // 为 activeEffect 赋值
        activeEffect = this
        // 执行 fn 方法
        return this.fn()
    }
}

packages\reactivity\src\index.ts

js 复制代码
export { effect } from './effect'

packages\vue\examples

html 复制代码
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>reactive 测试实例</title>
    <script src="../dist/vue.js"></script>
  </head>
  <body>
    <div id="app"></div>
  </body>
  <script>
    const { reactive, effect } = Vue

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

    console.log(obj.name) // 触发 track
    obj.name = '李四' // 触发 trigger
    console.log(obj.name) // 触发 track

    // 触发 effect 方法
    effect(() => {
      document.getElementById('app').innerHTML = obj.name
    })

    setTimeout(() => {
      obj.name = '王五'
    }, 3000)
  </script>
</html>

至此,已经成功 渲染数据到 html 中了。接下来就是要 当 obj.name 触发 setter 时,修改视图以实现 响应性数据变化。

4.7. 实现 tarck、trigger

  1. 触发 getter 行为时,触发 track 方法:依赖收集
  2. 触发 setter 行为时,触发 trigger 方法:触发依赖

4.7.1. 什么是响应性

当响应性数据触发 setter 时执行 fn 函数。

要达到这样的目的,就必须要在 getter 时能够收集当前的fn函数,以便在 setter 的时候可以执行对应的 fn 函数。

但对于收集而言,仅仅把 fn 存起来还不够,还需要知道当前这个 fn 是哪个响应式数据对象的哪个属性所对应的,只有这样在该属性触发 setter 的时候,才能准确的执行响应性。

4.7.2. 如何进行依赖收集

在 packages\reactivity\src\reactive.ts 中我们创建了一个 Map 缓存对象

js 复制代码
/**
 * 响应性对象的缓存(Map 缓存对象)
 * key: target 被代理的对象
 * value: proxy 代理对象
 */
export const reactiveMap = new WeakMap<object, any>()

WeakMap 的 key 必须是一个对象,并且 key 是一个

  1. WeakMap:

a. key:响应性对象

b. value:Map 对象

i. key:响应性对象的指定属性

ii . value:指定对象的指定属性的执行函数

关联上 指定对象的指定属性 与 执行函数 fn 之间的关系,当触发 setter 时,直接执行 对应对象的对应属性的 fn 即可。

4.7.3. 构建 track 依赖收集函数

packages\reactivity\src\effect.ts

js 复制代码
type KeyToDepMap = Map<any, ReactiveEffect>

/**
 * 收集所有依赖的 WeakMap 实例:
 * 1. key: 响应性对象
 * 2. value: 响应性对象的属性与依赖的 Map 实例
 *      2.1 key: 响应性对象的指定属性
 *      2.2 value: 响应性对象的指定属性对应的依赖(执行函数)
 */
const targetMap = new WeakMap<any, KeyToDepMap>()

/**
 * 收集依赖
 * @param target 被代理的对象(WeakMap 的 key)
 * @param key 被代理对象的属性(被代理对象 的 key,当依赖被触发时,需要根据 key 找到对应的 effect 函数)
 */
export function track(target: object, key: string | symbol) {
    // 如果当前不存在执行函数,则直接返回
    if (!activeEffect) return

    // 尝试从 targetMap 中,根据 target 获取对应的 map
    let depsMap = targetMap.get(target)
    // 如果当前不存在 depsMap,则创建 depsMap,并把该对象赋值给对应的 value
    if (!depsMap) {
        targetMap.set(target, (depsMap = new Map()))
    }
    // 为指定 map, 指定 key 设置回调函数
    depsMap.set(key, activeEffect)
    console.log(targetMap)
}

至此,指定对象的指定属性对应的 fn 已经被成功的保存到了 WeakMap 中。

4.7.4. 构建 trigger 触发依赖

packages\reactivity\src\effect.ts

js 复制代码
/**
 * 触发依赖
 * @param target 被代理的对象(WeakMap 的 key)
 * @param key 被代理对象的属性(Map 的 key,当依赖被触发时,需要根据 key 找到对应的 effect 函数)
 * @param newValue 新值
 */
export function trigger(target: object, key?: string | symbol) {
    // 依据 target 获取对应的 map 实例
    const depsMap = targetMap.get(target)
    // 如果 map 不存在,则直接返回
    if (!depsMap) return
    // 依据 key ,从 depsMap 中取出value, 该value 是一个 ReactiveEffect 实例
    const effect = depsMap.get(key)
    // 如果 effect 不存在,则直接返回
    if (!effect) return
    // 执行 effect 中保存的 fn 函数
    effect.fn()
}

运行测试实例(packages\vue\examples\reactive.html),等待3s发现视图发生变化

至此就完成了一个简单的 响应式依赖数据处理。

4.8. 总结:单一依赖的 reactive

以上我们构建了一个简单的 reactive 函数,使用 reactive 函数,配合 effect 可以实现出一个 响应式数据渲染功能。

  1. 首先在 packages\reactivity\src\reactive.ts 中,创建一个 reactive 函数,用于生成一个 proxy 实例对象
  2. 通过该 proxy 实例的 handler 可以监听到对应的 getter 和 setter
  3. 然后再 packages\reactivity\src\effect.ts 中,创建一个 effect 函数,通过该函数创建一个 ReactiveEffect 的实例,该实例的构造函数可以接收传入的回调函数 fn,并提供一个 run 方法
  4. 触发 run 可以为 activeEffect 进行赋值,并执行 fn 函数
  5. 需要再 fn 函数中触发 proxy 的 getter,以此来激活 handler 的 get 函数
  6. 在 handler 的 get 函数中,通过 WeakMap 收集了指定对象,指定属性的 fn,这一步操作叫做 依赖收集
  7. 最后可以在任意时刻,修改 proxy 的数据,就会柴发 handler 的 setter
  8. 在 handler 的 setter 中,根据指定对象 target 的指定属性 key 来获取到保存的 依赖,然后只需要触发依赖,即可达到修改数据的效果

4.9. 响应数据对应多个 effect

新增一个 effect 函数,即 name 属性对应两个 DOM 的变化。

但运行该代码发现, p1 的更新渲染无效。

html 复制代码
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Reactive-Dep</title>
    <script src="../dist/vue.js"></script>
  </head>
  <body>
    <div id="app">
      <p id="p1"></p>
      <p id="p2"></p>
    </div>
  </body>
  <script>
    const { reactive, effect } = Vue

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

    // 调用 effect 方法
    effect(() => {
      document.getElementById('p1').innerHTML = obj.name
    })
    effect(() => {
      document.getElementById('p2').innerHTML = obj.name
    })

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

在构建 KeyToDepMap 对象时,它的 Value 只能是一个 ReactiveEffect,这就导致了一个 key 只能对应一个有效的 effect 函数。

要让一个 key 可以对应多个有效的 effect 函数,需要让 KeyToDepMap 的 Value 对应一个数组。

通过构建一个 Set(set 是一个"数组",值不会重复)类型的对象,作为 Map 的 value。可以把它叫做 Dep,通过 Dep 来保存指定 key 的所有依赖。

Set - JavaScript | MDN

4.10. 构建 Dep 模块,处理一对多的依赖关系

  1. 改造 track 和 trigger

packages\reactivity\src\dep.ts

ts 复制代码
import { ReactiveEffect } from "./effect"

export type Dep = Set<ReactiveEffect>

/**
 * 依据 effects 生成 dep 实例
 * @param effects 
 * @returns 
 */
export const createDep = (effects?: ReactiveEffect[]): Dep => {
    const dep = new Set<ReactiveEffect>(effects)
    return dep
}

packages\reactivity\src\effect.ts

ts 复制代码
import type { Dep } from "./dep"
import { createDep } from "./dep"

type KeyToDepMap = Map<string | symbol | undefined, Dep>

/**
 * 收集所有依赖的 WeakMap 实例:
 * 1. key: 响应性对象
 * 2. value: 响应性对象的属性与依赖的 Map 实例
 *      2.1 key: 响应性对象的指定属性
 *      2.2 value: 响应性对象的指定属性对应的依赖(执行函数)
 */
const targetMap = new WeakMap<object, KeyToDepMap>()

/**
 * 收集依赖
 * @param target 被代理的对象(WeakMap 的 key)
 * @param key 被代理对象的属性(被代理对象 的 key,当依赖被触发时,需要根据 key 找到对应的 effect 函数)
 */
export function track(target: object, key: string | symbol) {
    // 如果当前不存在执行函数,则直接返回
    if (!activeEffect) return

    // 尝试从 targetMap 中,根据 target 获取对应的 map
    let depsMap = targetMap.get(target)
    // 如果当前不存在 depsMap,则创建 depsMap,并把该对象赋值给对应的 value
    if (!depsMap) {
        targetMap.set(target, (depsMap = new Map()))
    }
    // 获取指定 key 的 dep
    let dep = depsMap.get(key)
    if (!dep) {
        depsMap.set(key, (dep = createDep()))
    }
    // 为指定 map, 指定 key 设置回调函数
    trackEffects(dep)
}

/**
 * 利用 dep 一次跟踪指定 key 的所有 effect
 * @param dep 
 */
export function trackEffects(dep: Dep) {
    dep.add(activeEffect!)
}

/**
 * 触发依赖
 * @param target 被代理的对象(WeakMap 的 key)
 * @param key 被代理对象的属性(Map 的 key,当依赖被触发时,需要根据 key 找到对应的 effect 函数)
 * @param newValue 新值
 */
export function trigger(target: object, key?: string | symbol) {
    // 依据 target 获取对应的 map 实例
    const depsMap = targetMap.get(target)
    // 如果 map 不存在,则直接返回
    if (!depsMap) return

    // 依据 key 获取对应的 dep
    let dep: Dep | undefined = depsMap.get(key)
    // 如果 dep 不存在,则直接返回
    if (!dep) return
    // 触发 dep 中保存的依赖
    triggerEffects(dep)
}

/**
 * 依次触发 dep 中保存的依赖
 * @param dep 
 */
export function triggerEffects(dep: Dep) {
    const effects = Array.isArray(dep) ? dep : [...dep]
    for (const effect of effects) {
        triggerEffect(effect)
    }
}

/**
 * 触发指定依赖
 * @param effect 
 */
export function triggerEffect(effect: ReactiveEffect) {
    effect.run()
}

/**
 * 创建响应式 effect 函数
 * @param fn 执行方法
 * @returns 以 ReactiveEffect 实例为 this 的执行函数
 */
export function effect<T>(fn: () => T) {
    // 生成 ReactiveEffect 实例
    const _effect = new ReactiveEffect(fn)
    // 执行 ReactiveEffect 实例的 run 方法
    _effect.run()
}

/**
 * 单例的,当前的 effect
 */
export let activeEffect: ReactiveEffect | undefined

/**
 * 响应性触发依赖时的执行类
 */
export class ReactiveEffect<T = any> {
    constructor(public fn: () => T) { }

    run() {
        // 为 activeEffect 赋值
        activeEffect = this
        // 执行 fn 方法
        return this.fn()
    }
}

4.11. 总结

reactive 响应性函数的实现:

  1. 通过 proxy 的 setter 和 getter 来实现的数据监听
  2. 需要配合 effect 函数进行使用
  3. 基于 WeakMap 完成的依赖收集和处理
  4. 可以存在一对多的依赖关系

这里我们需要先思考一下reactive 的不足,然后下一节我们就要思考如何解决这个问题的(ref)。

reactive函数有以下局限:

  1. 只能对复杂数据类型进行使用:对于 reactive 函数而言,它会把传入的 object 作为 proxy 的 target 参数,而对于 proxy 而言,他只能代理 对象,而不能代理简单数据类型,所以说:不可以使用 reactive 函数构建简单数据的响应性。
  2. reactive 的响应性数据,不可以进行解构:一个数据是否具备响应性的关键在于:是否可以监听它的 getter 和 setter。而只有 proxy 类型的代理对象才可以被监听 getter 和 setter,而一旦结构,对应的属性将不再是 proxy 类型的对象。所以,解构之后的属性,将不具备响应性。
相关推荐
JuneXcy6 分钟前
11.Layout-Pinia优化重复请求
前端·javascript·css
子洋16 分钟前
快速目录跳转工具 zoxide 使用指南
前端·后端·shell
天下无贼!17 分钟前
【自制组件库】从零到一实现属于自己的 Vue3 组件库!!!
前端·javascript·vue.js·ui·架构·scss
CF14年老兵37 分钟前
✅ Next.js 渲染速查表
前端·react.js·next.js
司宸1 小时前
学习笔记八 —— 虚拟DOM diff算法 fiber原理
前端
阳树阳树1 小时前
JSON.parse 与 JSON.stringify 可能引发的问题
前端
让辣条自由翱翔1 小时前
总结一下Vue的组件通信
前端
dyb1 小时前
开箱即用的Next.js SSR企业级开发模板
前端·react.js·next.js
前端的日常1 小时前
Vite 如何处理静态资源?
前端
前端的日常1 小时前
如何在 Vite 中配置路由?
前端