老生常谈响应式之 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的构建和解析,未完待续...

相关推荐
栈老师不回家43 分钟前
Vue 计算属性和监听器
前端·javascript·vue.js
前端啊龙1 小时前
用vue3封装丶高仿element-plus里面的日期联级选择器,日期选择器
前端·javascript·vue.js
一颗松鼠1 小时前
JavaScript 闭包是什么?简单到看完就理解!
开发语言·前端·javascript·ecmascript
小远yyds1 小时前
前端Web用户 token 持久化
开发语言·前端·javascript·vue.js
吕彬-前端2 小时前
使用vite+react+ts+Ant Design开发后台管理项目(五)
前端·javascript·react.js
学前端的小朱2 小时前
Redux的简介及其在React中的应用
前端·javascript·react.js·redux·store
guai_guai_guai2 小时前
uniapp
前端·javascript·vue.js·uni-app
帅比九日3 小时前
【HarmonyOS Next】封装一个网络请求模块
前端·harmonyos
bysking3 小时前
【前端-组件】定义行分组的表格表单实现-bysking
前端·react.js
王哲晓4 小时前
第三十章 章节练习商品列表组件封装
前端·javascript·vue.js