老生常谈响应式之 reactive & ref

🌬🌬🌬 前言:在Vue3中我们知道可以通过reactive、ref实现响应式数据,本质上来说,这两种方式都是通过依赖收集、依赖触发实现的数据相应,那这两种方式会有什么差异性吗,下面将会从应用场景、实现方式等方面进行解析,借鉴Vue源码并通过ts实现简化版的reactive、ref,春节已经过完啦,新的一年一起加油吧~💪🏻💪🏻💪🏻

一、构建基础列表

大致目录结构
diff 复制代码
---| packages
---|---| reactivity // 响应性模块
---|---|---| src
---|---|---|---| index.ts 出口文件
---|---|---|---| ref.ts
---|---|---|---| reactive.ts
---|---|---|---| effect.ts
---|---|---|---| dep.ts
---|---|---|---| baseHandlers.ts
---|---| shared // 公共方法模块
---|---|---| src
---|---|---|---| index.ts 出口文件
---|---|---|---| shapeFlags.ts
---|---| vue // 打包、测试实例、项目整体入口模块
---|---|---| dist
---|---|---| examples
---|---|---| src
---|---|---|---| index.ts 出口文件

二、开整

reactive 目标:构建 reactive 函数,获取 proxy 实例
1. 创建 packages/reactivity/src/reactive.ts 模块
typescript 复制代码
import { mutableHandlers } from './baseHandlers'

/**
* 响应性 Map 对象 🤔🤔🤔思考一下:这里为什么用new WeakMap进行缓存呢?
* key: target
* val: proxy
*/
export const reactiveMap = new WeakMap<object, any>()

/**
* 返回响应性对象(复杂数据类型)
* @param target 被代理对象
* @returns 代理对象
*/
export function reactive(target: object) {
  return createReactiveObject(target, mutableHandlers, reactiveMap)
}

/**
* 实现创建响应性对象
* @param target 被代理对象
* @param baseHandlers handler
* @returns proxy
*/
function createReactiveObject (
  target: object,
  baseHandlers: ProxyHandler<any>,
  proxyMap: WeakMap<object, any>
) {
   // 如果:实例已被代理,则:直接读取
   const existingProxy = proxyMap.get(target)
   if(existingProxy) {
     return existingProxy
   }
   // 如果:未被代理,则:生成 proxy 实例
   const proxy = new Proxy(target, baseHandlers)
   
   // 缓存代理对象
   proxyMap.set(target, proxy)
   
   return proxy
}
2. 创建 packages/reactivity/src/baseHandlers.ts 模块
arduino 复制代码
/**
* 响应性的 handler
*/
export const mutableHandlers: ProxyHandler<object> = {}
3. 👀 解答时间 WeakMap 👀

通过上面文档对好兄弟 WeakMap 和 Map 的介绍,我们大致可以获取到他们都是 {key, value} 的结构对象,那有什么不一样的地方呢,下面来看一段对于WeakMap的key的描述:

我们可以提取几个关键字,键(即key)必须是对象不会创建对它的键(即key)的强引用不会阻止该对象被垃圾回收,总结一下:1. key 必须是对象,2. key 是弱引用的,下面是搬运的前辈们的文章,希望可以帮助大家理解强引用,弱引用,以及垃圾回收,简单描述的话:

  • 弱引用:不会影响垃圾回收机制。即:WeakMap 的 key 不再存在任何引用时,会被直接回收。
  • 强引用:会影响垃圾回收机制。存在强应用的对象永远 不会 被回收。

javascript弱引用-CSDN博客

垃圾回收 (javascript.info)

WeakMap 数据格式,👣注:后续会用到👣

  • key: 响应性对象
  • value: Map 对象
    • key:响应性对象的指定属性
    • value:指定对象的指定属性的 执行函数

4. baseHandlers.ts 模块 完善

对于 Proxy 来说,它的 handler 可以监听 代理对象gettersetter,下面我们来完善一下 mutableHandlers

csharp 复制代码
/**
* 响应性的 handler
*/
export const mutableHandlers: ProxyHandler<object> = {
  get,
  set
}

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

/**
* 创建 getter 回调方法
*/
function createGetter() {
  return function get(target: object, key: string | symbol, receiver: object) {
    // 通过 Reflect 得到 返回值
    const res = Reflect.get(target, key, receiver)
    
    // 收集依赖
    track(target, key)
    
    return res
  }
}

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

/**
* 创建 setter 回调方法
*/
function createSetter() {
  return function set(target: object, key: string | symbol, value: unknown, receiver: object) {
    // 通过 Reflect.set 设置新值
    const result =  Reflect.set(target, key, value, receiver)
    
    // 触发依赖
    trigger(target, key, value)
    
    return result
  }
}
5. effect.ts

创建 track && trigger,构建 effect 函数,生成 ReactiveEffect 实例

typescript 复制代码
/**
* 收集依赖
* @param target WeakMap 的 key
* @param key 代理对象的 key,当依赖被触发时,需要根据该 key 获取
*/
export function track(target: object, key: unknown) {
    
}

/**
* 触发依赖
* @param target WeakMap 的 key
* @param key 代理对象的 key,当依赖被触发时,需要根据该 key 获取
* @param newValue 指定 key 的最新值
* @param oldValue 指定 key 的旧值
*/
export function trigger(target: object, key?: unknown, newValue?: unknown) {

}


/**
* effect 函数
* @param fn 执行方法
* @returns 以 ReactiveEffect 实例为 this 的执行函数
*/
export function effect<T = any>(fn: () => T) {
  // 生成 ReactiveEffect 实例
  const _effect = new ReactiveEffect(fn)
  
  // 执行 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()
  }
}
5. effect.ts 完善 track && trigge

从上面我们写的内容来看,接下来我们需要在 getter 收集当前的 fn,在 setter 的时候执行对应的 fn 函数,需要注意的是 fn对应哪个响应数据对象的哪个属性 ,回忆一下我们是不是可以利用WeakMap的特性来进行实现一下呢

typescript 复制代码
type keyToDepMap= Map<any, ReactiveEffect> 

/**
* 收集所有依赖的 WeakMap 实例
*/
const targetMap = new WeakMap<any, keyToDepMap>()

/**
* 收集依赖
* @param target WeakMap 的 key
* @param key 代理对象的 key,当依赖被触发时,需要根据该 key 获取
*/
export function track(target: object, key: unknown) {
  // 如果:不存在执行函数,则:直接 return
  if(!activeEffect) return
  
  // 从 targetMap 中, 通过 target 获取 map
  let depsMap = targetMap.get(target)
  
  // 如果:获取的 map 不存在 则:生成新的 map 对象,并将 map 对象 赋值给对应的 value
  if(!depsMap) {
      targetMap.set(target, (depsMap = new Map()))
  }
  
  // 为指定 map,指定 key 设置回调函数
  depsMap.set(key, activeEffect)
}


/**
* 触发依赖
* @param target WeakMap 的 key
* @param key 代理对象的 key,当依赖被触发时,需要根据该 key 获取
*/
export function trigger(target: object, key?: unknown) {
  // 根据 target 获取存储的 map 实例
  const depsMap = targetMap.get(target)
  
  // 如果:map 不存在,则:直接返回 return
  if(!depsMap) {
    return
  }
  
  // 根据 key,从 depsMap 中获取 value,该 value 是 ReactiveEffect 类型
  const effect = depsMap.get(key) as ReactiveEffect
  
  // 如果:effect 不存在,则:直接返回 return
  if(!effect) {
     return
  }
  
  // 执行 effect 中保存的 fn 函数
  effect.fn()
}

至此我们已经构建出了一个简易版的 reactive 函数,下面来小试牛刀一下,看看有没有问题。

7. 导出
reactive/src/index.ts
javascript 复制代码
export { reactive } from "./reactive";
export { effect } from './effect';
vue/src/index.ts
javascript 复制代码
export { reactive, effect } from '@vue/reactivity'
8. packages/vue/examples/reactivity/reactive.html 应用
xml 复制代码
<body>
  <div id="app"></div>
  <script>
      const { reactive, effect } = Vue
      
      const obj = reactive({
        name: '张三'
      })
      
      effect(() => {
         document.querySelector("#app").innerText = obj.name;
      })
      
      setTimeout(() => {
        obj.name = "李四";
      }, 2000);
      
      console.log('obj', obj)
  </script>
</body>

我们看到已经可以实现数据的更新了,是不是感觉很完美了,那现在我们改成以下代码,我们上面的逻辑是否还适用呢?

xml 复制代码
<body>
  <div id="app">
    <p id="p1"></p>
    <p id="p2"></p>
  </div>
</body>

<script>
  const { reactive, effect } = Vue

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

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

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

运行我们可以看到p1并未更新,回忆一下,我们在构建KeyToDepMap对象时,Value 是不是只能是一个ReactiveEffect,那该从哪里入手才能实现一对多呢,请看下图见分晓:

通过改变KeyToDepMap Value 是不是可以对应一个set数组

9. 一对多
dep.ts
typescript 复制代码
import { ReactiveEffect } from './effect'
export type Dep = Set<ReactiveEffect>

/**
* 依据 effects 生成 dep 实例
*/
export const createDep = (effects?: ReactiveEffect[]): Dep => {
  const dep = new Set<ReactiveEffect>(effets) as Dep
  return dep
}
effect.ts
  • 修改 KeyToDepMap 的泛型
typescript 复制代码
import { Dep } from './dep'

type keyToDepMap = Map<any, Dep>
  • 修改 track 方法,处理 Dep 类型数据
scss 复制代码
export function track(target: object, key: unknown) {
  // 获取指定 key 的 dep
  let dep = depsMap.get(key)
  // 如果:dep 不存在, 则:生成一个新的 dep,并放入到 depsMap 中
  if(!dep) {
    depsMap.set(key, (dep = createDep()))
  }
  
  trackEffects(dep)
}

/**
* 利用 dep 依次跟踪 指定 key 的 effect
* @param dep
*/
export function trackEffects(dep: Dep) {
  dep.add(activeEffect!)
}
  • 修改 trigger 方法,依次读取 dep 中保存的依赖
scss 复制代码
export function trigger(target: object, key?: unknown) {
  // 根据 target 获取存储的 map 实例
  const depsMap = targetMap.get(target)
  
  // 如果:map 不存在,则:直接返回 return
  if(!depsMap) {
    return
  }
  
  // 根据指定的 key,获取 dep 实例
  let dep: Dep | undefined = depsMap.get(key)
  
  // 如果:dep 不存在,则:直接返回 return
  if(!dep) {
     return
  }
  
  // 触发 dep
  triggerEffects(dep)
}

/**
* 依次触发 dep 中保存的依赖
*/
export function triggerEffects(dep: Dep) {
  // 把 dep 构建成一个数组
  const effects = isArray(dep) ? dep : [...dep]
  
  // 依次触发
  for (const effect of effects) {
    triggerEffect(effect)
  }
}

/**
* 触发指定的依赖
*/
export function triggerEffect(effect: ReactiveEffect) {
  effect.run()
}
  • shared/src/index.ts
arduino 复制代码
/**
* 判断是否为一个数组
*/
export const isArray = Array.isArray

✨✨✨bingo✨✨✨我们再去运行一下demo就可以正常更新啦~~~


小结一下:目前我们的 reactive 结合 effect 可以响应数据的渲染和更新,实现大致可以分为以下几个步骤:

  • 通过 proxysettergetter 来实现的数据监听

  • 配合 effect 函数进行使用

  • 基于 WeakMap 完成的依赖收集和处理

  • 通过改变KeyToDepMapValue 实现一对多的更新渲染

    注:reactive的局限性
    1. 由于 reactive 是把传入的 objec 作为 proxytarget 参数,对于 proxy 来说,只能代理 对象,并不能代理 基本数据类型
    2. 由于只有 proxy 类型的 代理对象 才可以被监听 gettersetter,一旦解构之后,解构的属性,将不具备响应性.

ps: 以上是对reactive函数的解析,下面是对于ref的构建和解析,未完待续...

相关推荐
崔庆才丨静觅8 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60619 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了9 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅9 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅9 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅10 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment10 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅10 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊10 小时前
jwt介绍
前端
爱敲代码的小鱼10 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax