手动打造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,这里有几十位一心去大厂的友友可以相互鼓励,分享信息,模拟面试,共读源码,齐刷算法,手撕面经。来吧,友友们!

相关推荐
I_Am_Me_10 分钟前
【JavaEE进阶】 JavaScript
开发语言·javascript·ecmascript
℘团子এ20 分钟前
vue3中如何上传文件到腾讯云的桶(cosbrowser)
前端·javascript·腾讯云
学习前端的小z26 分钟前
【前端】深入理解 JavaScript 逻辑运算符的优先级与短路求值机制
开发语言·前端·javascript
前端百草阁1 小时前
【TS简单上手,快速入门教程】————适合零基础
javascript·typescript
彭世瑜1 小时前
ts: TypeScript跳过检查/忽略类型检查
前端·javascript·typescript
Backstroke fish1 小时前
Token刷新机制
前端·javascript·vue.js·typescript·vue
zwjapple1 小时前
typescript里面正则的使用
开发语言·javascript·正则表达式
小五Five1 小时前
TypeScript项目中Axios的封装
开发语言·前端·javascript
临枫5411 小时前
Nuxt3封装网络请求 useFetch & $fetch
前端·javascript·vue.js·typescript
酷酷的威朗普1 小时前
医院绩效考核系统
javascript·css·vue.js·typescript·node.js·echarts·html5