手动打造Vue中的reactive函数:探秘数据变化的魔法

前言

在ES6中,"reactive"通常指的是创建具有响应式特性的对象,使得这些对象的属性可以自动地响应数据变化而更新视图。Vue.js的响应式系统就是基于这个概念构建的。在ES6中,我们可以通过使用Proxy对象来手动创建具有类似响应式特性的对象。

下面我们将逐步创建一个简单的reactive函数,来手动实现一个类似Vue.js中响应式对象的功能(只实现了set及get功能):

1、 reactive.js:

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

export const reactiveMap = new WeakMap(); // 创建一个WeakMap用来存储已经代理过的对象,WeakMap对内存的回收更加友好

export function reactive(target) {
  // 将target变成响应式对象
  return createReactiveObject(target, reactiveMap, mutableHandlers); // 创建响应式对象
}

export function createReactiveObject(target, proxyMap, proxyHandlers) {
  // 创建响应式的函数,参数分别是目标对象,存储代理对象的map,代理对象的处理函数
  // 判断target是不是引用类型
  if (typeof target !== "object" || target === null) {
    // 不是引用类型直接返回
    return target;
  }

  // 该对象是否已经被代理过(已经是响应式对象)
  const existingProxy = proxyMap.get(target); // 从reactiveMap中获取target
  if (existingProxy) {
    return existingProxy; // 如果已经代理过了,直接返回
  }
  // 执行代理操作(将target变成响应式对象)
  const proxy = new Proxy(target, proxyHandlers); //第二个参数:当target被读取值,设置值,判断值等等操作时,会触发的函数


  // 往 reactiveMap中增加 proxy,把已经代理过的对象存储起来
  proxyMap.set(target, proxy);
  return proxy; // 返回代理对象
}

在上述代码中,主要是实现reactive函数的主要功能之一,将引用类型转换为响应式对象,下面让我逐步解释这段代码的功能:

  1. 创建 reactiveMap :这行代码创建了一个 WeakMap 对象,用于存储已经代理过的对象。 WeakMap 中的键是弱引用的,这意味着如果键对象没有其他引用,因此对内存的回收更加友好,当被代理的对象被垃圾回收时,相应的条目也会被自动移除。
  2. reactive 函数 :这个函数接受一个普通对象作为参数,并将其转换为具有响应式特性的对象。它调用了 createReactiveObject 函数来创建响应式对象,并传入了目标对象、reactiveMapmutableHandlers 作为参数,参数分别是目标对象,存储代理对象的map,代理对象的处理函数。
  3. createReactiveObject 函数 :这个函数用于创建响应式对象。
    • 首先它判断传入的目标对象是否是引用类型(即对象或数组),如果不是,则直接返回目标对象本身。
    • 然后它检查目标对象是否已经被代理过,如果是,则直接返回之前创建的代理对象。如果目标对象尚未被代理过,则使用 Proxy 对象创建一个新的代理对象,并将其存储到 reactiveMap 中。
    • 最后返回创建的代理对象。

2、baseHandlers.js

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


const get = createGetter(); // 创建一个get函数
const set = createSetter(); // 创建一个set函数

function createGetter() {
  return function get(target, key, receiver) {
    // console.log('target被读取值');
    const res = Reflect.get(target, key, receiver); // 获取源对象中的键值
    // 这个属性究竟还有哪些地方用到了,(副作用函数的收集,computed,watch...)
    track(target, key); // 依赖收集


    return res;
  }
}

function createSetter() {
  return function set(target, key, value, receiver) {
    // console.log('target被设置值', key, value);
    const res = Reflect.set(target, key, value, receiver); // 设置源对象中的键值 === target[key] = value

    trigger(target, key); // 触发副作用函数
    // 需要记录下来此时是哪一个key的值变更了,再去通知其他依赖该值的函数生效,更新浏览器的视图(响应式)
    // 触发被修改的属性身上的副函数 依赖收集(被修改的key在哪些地方被使用了)发布订阅


    return res;
  }
}


export const mutableHandlers = {
  // get: function (target, key, receiver) { // target 被代理的源对象,key是源对象中的键,receiver是代理后对象
  //   console.log('target被读取值');
  //   const res = Reflect.get(target, key, receiver); // 获取源对象中的键值
  //   return res;
  // },
  get,
  set,
  // set: function (target, key, value, receiver) {
  //   console.log('target被设置值', key, value);
  //   const res = Reflect.set(target, key, value, receiver); // 设置源对象中的键值 === target[key] = value
  //   return res;
  //   // 更新浏览器的视图(响应式)
  // },
}

以上代码实现了对代理对象的操作处理,包括了对属性的读取和设置。被代理对象中的任意属性的值发生修改,都应该将用到了这个属性的各个函数重新执行一遍,那么在执行之前就需要先为每一个属性都做好副作用函数的收集,也称为依赖收集.这也是实现 Vue.js 响应式系统的关键之一,它确保了在属性变化时能够正确地触发浏览器视图更新。下面让我来逐步解释这段代码的功能:

  1. createGetter 函数

    • 这个函数返回一个用于处理属性读取的函数。在这个函数内部,首先调用了 Reflect.get() 方法获取目标对象中指定键的值,并将结果存储在变量 res 中。
    • 接着调用了外部导入的 track 函数,用于收集当前属性的依赖关系。这意味着该属性被访问时,需要跟踪它的依赖,以便在属性变化时触发相应的副作用函数。
    • 最后返回属性的值 res
  2. createSetter 函数

    • 这个函数返回一个用于处理属性设置的函数。在这个函数内部,首先调用了 Reflect.set() 方法将指定键的值设置为指定的值,并将结果存储在变量 res 中。
    • 接着调用了外部导入的 trigger 函数,用于触发与当前属性相关联的副作用函数。这意味着当属性被修改时,需要通知所有依赖该属性的函数执行相应的副作用操作。
    • 最后返回设置操作的结果 res
  3. mutableHandlers 对象

    • 这个对象包含了对可变对象的操作处理函数,其中包括了 getset 操作的具体实现。在这里,getset 的处理函数分别由 createGettercreateSetter 函数返回。
    • mutableHandlers 对象被导出,可以被其他模块引用和使用。

3、effect.js

js 复制代码
const targetMap = new WeakMap();
let activeEffect = null; //得是一个副作用函数

export function effect(fn,options={}) { //watch,computed的核心逻辑
  const effectFn = () => {
    try {
      activeEffect = effectFn
      return fn()
    } finally {
      activeEffect = null
    }

  }
  if(!options.lazy){
    effectFn()
  }
  return effectFn
}

// 为某个属性添加effect
export function track(target,key) {
  // targetMap = { //存成这样的结构
  //   target: {
  //     key: [effect1,effect2,...]
  //   }
  // }

  let depsMap = targetMap.get(target)
  if (!depsMap) {
    depsMap = new Map()
    targetMap.set(target, depsMap)
  }
  let dep = depsMap.get(key)
  if (!dep) { //改属性未添加过effect
    dep = new Set()
  }
  if (!dep.has(activeEffect) && activeEffect) {
    // 存入一个effect函数
    dep.add(activeEffect)
  }
  depsMap.set(key, dep)

}

// 触发属性effect
export function trigger(target,key) {
  const depsMap = targetMap.get(target)
  if(!depsMap){ //当前对象中所有的key都没有副作用函数,从来都没有被使用过
    return
  }
  const deps = depsMap.get(key)
  if (!deps) { //这个属性没有依赖
    return
  }

  deps.forEach(effectFn => {
    effectFn() //将该属性上的所有副作用函数全部触发
  });
}

以上代码实现了一个状态管理的功能,用于实现对对象属性的依赖跟踪和副作用函数的执行,并管理对象属性的依赖关系和执行副作用函数。这样,在属性值发生变化时,就能自动执行相应的副作用函数,从而实现数据的响应式处理。下面我将详细的介绍一下以上代码的功能:

  1. effect 函数

    • 这个函数用于创建副作用函数。副作用函数是一个函数,它会在响应式数据发生变化时被执行。
    • effect 函数接受两个参数:fn 是一个函数,代表需要执行的副作用函数;options 是一个包含选项的对象,默认为空对象。其中,lazy 是一个布尔值选项,表示是否延迟执行副作用函数,默认为 false
    • effect 函数内部,首先创建了一个名为 effectFn 的函数,它是一个封装了传入的副作用函数 fn 的函数。在 effectFn 函数内部,通过 try...finally 结构确保了 activeEffect 变量的正确设置和清除。然后根据 options.lazy 的值决定是否立即执行 effectFn 函数,并返回 effectFn 函数。
  2. track 函数

    • 这个函数用于为某个属性添加副作用函数。当某个属性被访问时,需要调用 track 函数来收集对应的依赖关系,以便在属性值发生变化时执行相应的副作用函数。
    • track 函数接受两个参数:target 是目标对象,key 是目标对象的属性名。
    • track 函数内部,首先根据 targettargetMap 中获取对应的依赖映射关系 depsMap,如果不存在,则创建一个新的空映射关系并存储到 targetMap 中。然后根据 keydepsMap 中获取对应的依赖集合 dep,如果不存在,则创建一个新的空集合。接着判断当前副作用函数 activeEffect 是否已经存在于 dep 中,如果不存在且 activeEffect 存在,则将其添加到 dep 中。最后将更新后的 dep 存储回 depsMap 中。
  3. trigger 函数

    • 这个函数用于触发属性的副作用函数。当某个属性的值发生变化时,需要调用 trigger 函数来执行所有依赖于该属性的副作用函数。
    • trigger 函数接受两个参数:target 是目标对象,key 是目标对象的属性名。
    • trigger 函数内部,首先根据 targettargetMap 中获取对应的依赖映射关系 depsMap,如果不存在,则说明目标对象中所有的属性都没有副作用函数,直接返回。然后根据 keydepsMap 中获取对应的依赖集合 deps,如果不存在,则说明该属性没有任何依赖,直接返回。接着遍历 deps 集合,依次执行其中的副作用函数。

值得注意的是,对于effect函数,实际上大部分实现监听效果的函数都会使用到这种类似的函数,就如watchcomputed函数。因为它用于创建副作用函数,而 watchcomputed 又都是基于副作用函数实现的,它们都依赖于副作用函数来实现对数据的观察和计算。

结语

就这样,我们分别通过创建reactive.jsbaseHandlers.jseffect.js三个js文件实现了一个简单的reactive函数。

总结

在ES6中,我们可以通过使用Proxy对象和一些基本的JavaScript技巧手动创建具有响应式特性的数据处理系统。本文介绍了如何使用Proxy对象来实现一个简单的reactive函数,以及如何配合使用WeakMapReflect和副作用函数来构建一个简易的响应式系统。通过对依赖关系的追踪和副作用函数的执行,我们可以实现数据的自动更新和视图的同步更新,这也是现代JavaScript框架中常见的核心功能之一。

补充:什么是副作用函数

副作用函数指的是在函数执行过程中,除了返回一个值之外,还对函数外部的状态产生了影响,或者执行了与函数本身的目的无关的操作。

在响应式系统中,副作用函数通常用于创建观察者模式或实现数据的自动更新。例如,当一个数据发生变化时,会触发与之相关联的副作用函数,从而执行一些预定的操作,比如更新UI界面或触发其他相关的事件。Vue.js中的watchcomputed就是基于副作用函数实现的,它们能够监视数据变化并执行相应的操作。

最后,假如您也和我一样,在准备春招。欢迎加微信shunwuyu,这里有几十位一心去大厂的友友可以相互鼓励,分享信息,模拟面试,共读源码,齐刷算法,手撕面经。来吧,友友们!

相关推荐
La Pulga14 分钟前
【STM32】FLASH闪存
android·c语言·javascript·stm32·单片机·嵌入式硬件·mcu
Nan_Shu_6141 小时前
学习:JavaScript(1)
开发语言·javascript·学习·ecmascript
木木子99991 小时前
Next.js, Node.js, JavaScript, TypeScript 的关系
javascript·typescript·node.js
.生产的驴2 小时前
React 页面路由ReactRouter 路由跳转 参数传递 路由配置 嵌套路由
前端·javascript·react.js·前端框架·json·ecmascript·html5
打小就很皮...2 小时前
PDF 下载弹窗 content 区域可行性方案
前端·javascript·pdf
孤狼warrior9 小时前
爬虫进阶 JS逆向基础超详细,解锁加密数据
javascript·爬虫
前端炒粉10 小时前
18.矩阵置零(原地算法)
javascript·线性代数·算法·矩阵
listhi52010 小时前
利用React Hooks简化状态管理
前端·javascript·react.js
华仔啊10 小时前
这个Vue3旋转菜单组件让项目颜值提升200%!支持多种主题,拿来即用
前端·javascript·css
CsharpDev-奶豆哥11 小时前
JavaScript性能优化实战大纲
开发语言·javascript·性能优化