ref和reactive实现原理剖析

前言

vue3中,refreactive是我们最常用的两个方法,它们的作用都是为了实现将一个普通的变量变为响应式变量,只是ref可以将对象和基本类型都变为响应式,而reactive只能将对象变为响应式。

reactive

reactive是通过使用proxy去代理一个对象,从而监听这个对象的十三中行为get, set, has, deleteProperty, apply, construct, ownKeys, getOwnPropertyDescriptor, defineProperty, isExtensible, preventExtensions, getPrototypeOf, setPrototypeOf,然后去收集它的副作用函数,并将它们都执行掉,从而实现这个将传入对象变为响应式

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

 const reactiveMap = new WeakMap()       

export function reactive(target) {              
  return createReactiveObject(target, mutableHandlers, reactiveMap)    
}

function createReactiveObject(target, proxyHandlers, proxyMap) {  
  if (typeof target !== 'object') {                    
    return target                                      
  }
  const existingProxy = proxyMap.get(target)         
  if (existingProxy) {
    return existingProxy                                 
  }

  const proxy = new Proxy(target, proxyHandlers)           
  proxyMap.set(target, proxy)                                
  return proxy                                                 
}

首先我们要抛出一个reactive方法,并且接收一个参数,接收的这个参数就是传入的普通变量,我们需要将其变为响应式变量,所以我们要创建一个createReactiveObject的方法,将传入的对象进行代理。

不过我们要首先判断传入的参数是否为一个对象和这个对象是否已经被代理了,所以我们创建了一个reactiveMap的实例对象,将每次被代理的对象传入,使用proxyMap.set(target, proxy)方法往对象里面存值,使用proxyMap.get(target) 判断对象里面是否有这个值,有的话说明它已经是代理对象了。 new Proxy(target, proxyHandlers)后面的proxyHandlers是我们代理对象后,对象发生读取或修改属性等行为时,我们进行拦截读取操作。

所以我们要打造一个mutableHandlers函数去监听和拦截目标对象的多种操作行为。

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

const get = createGetter()
const set = createSetter()

function createGetter() { 
  return function(target, key, receiver) {
    const res = Reflect.get(target, key, receiver)
    // 在值初次被读取时就要进行依赖收集
    track(target, 'get', key)

    if (isObject(res)) { // 子对象递归
      return reactive(res)
    }
    return res
  }
}

function createSetter() {
  return function(target, key, value, receiver) {
    const res = Reflect.set(target, key, value, receiver)
    // 触发依赖
    trigger(target, 'set', key)
    return res
  }
}

export const mutableHandlers = {
  get,
  set
}

由于写多了可能更难以理解,这里我们就演示代理对象get和set的两种行为,Reflect.get(target, key, receiver)用来获得被读取的对象key对应的值,并且在读取值的时候我们要对这个对象的副作用函数进行收集,并且读取的key如果是一个对象我们要进行递归操作,将这个key也变为响应式对象

Reflect.set(target, key, value, receiver)是获取被修改的key的值,value是要赋给属性的新值。然后对依赖这个对象被修改的key的函数进行触发。

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

export function effect(fn, options = {}) {
  const effectFn = () => {
    try {
      activeEffect = fn
      return fn()
    } catch (error) {
      activeEffect = null
    }
  }
  effectFn()
}

// 依赖收集
export function track(target, type, key) {
  let depsMap = targetMap.get(target)
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()))
  }
  let deps = depsMap.get(key)
  if (!deps) {
    deps = new Set()
  }
  if (!deps.has(activeEffect) && activeEffect) {
    deps.add(activeEffect)
  }
  depsMap.set(key, deps)
}

// 触发依赖
export function trigger(target, type, key) {
  const depsMap = targetMap.get(target)
  if (!depsMap) return

  const deps = depsMap.get(key)
  if (!deps) return

  deps.forEach(effect => {
    effect()
  })
}

因为要做函数的依赖收集和触发,所以我们来打造这两个方法,我们创建一个targetMap对象用来收集传入的对象,防止重复进行对象的依赖收集,depsMap也是一个对象,用来收集副作用函数。

effect函数是用来被当成副作用函数收集触发的演示,最后将被收集的副作用函数全部放入deps这个set的数据结构中去,将deps内的副作用函数全部触发。

js 复制代码
<template>
  <div @click="add">
    <p>count:{{state.count}}</p>

  </div>
</template>

<script setup>
import { reactive } from './reactivity/reactive.js'
import { effect } from './reactivity/effect.js'

const state = reactive({
  count: 10,
  like: {
    a: 2
  }
})

effect(() => {
  console.log('effect',state.count)
})


const add = () => {
  state.count++
  console.log(state);
  
  console.log(state.count);
}
</script>

<style lang="css" scoped>

</style>

我们给使用自己打造的reactive去将state对象变为响应式,然后看看,我们使用依赖收集的effect函数是否会重新执行,再打印我们使用reactive代理后的对象是什么样的

可以看到每次对象中的count属性发生变化,我们收集的依赖函数effect都会重新执行,且state对象已经不是原来的state对象了,而是一个Proxy代理之后的对象。

ref

我们知道ref是可以代理对象和原始类型的,那他又是怎么实现的呢

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

export function ref(val) {
  return new RefImpl(val)
}


class RefImpl {
  constructor(val) {
    this._val = convert(val)
  }

  get value() {
    track(this, 'get', 'value')
    return this._val
  }
  set value(newVal) {
    if (newVal !== this._val) {
      this._val = newVal
      trigger(this, 'set', 'value')
      return this._val
    }
  }

}

function convert(val) {
  return isObject(val) ? reactive(val) : val  
}

这里直接使用class创建一个类入ref的值作为参数,去返回一个RefImpl的实例对象,在RefImpl类中,我们先判断传入参数是否为对象,如果是对象就用reactive处理,将实例对象中得_val属性变为响应式对象,如果是原始值,则直接存储在对象_val属性上。

外部访问value属性时,我们要做副作用函数收集,并将要我们创建出来得实例对象得_val属性返回。如果外部要修改值,我们要使用newVal去代替原来实例对象上_val的属性,然后将副作用函数全部触发,并将修改后的值返回。

js 复制代码
<template>
  <div @click="add">
    <p>age:{{age}}</p>
  </div>
</template>

<script setup>
import { reactive } from './reactivity/reactive.js'
import { effect } from './reactivity/effect.js'

 import { ref } from './reactivity/ref.js' 


const age = ref(10)


effect(() => {
  console.log('effect', age.value)
})

const add = () => {
  age.value++
  console.log(age);
  
}
</script>

<style lang="css" scoped>

</style>

我们使用直接打造的ref去将age变为响应式变量,effect作为依赖函数被触发

可以看到依赖函数在age每次改变都会进行触发,并且使用ref将age变为了一个RefInpl,_val才是age的值。

总结

reactive:

reactive 的实现原理是通过 JavaScript 的 Proxy 对象拦截对目标对象的操作(如读取、赋值等)。在读取属性时,调用 track 收集依赖;在修改属性时,调用 trigger 触发更新。内部使用 WeakMap 存储依赖关系,确保对象被垃圾回收时释放内存,从而实现高效、精细的响应式系统。

ref:

ref 的实现原理是将值包装进一个包含 getset 访问器的类(如 RefImpl)中,通过访问 .value 来触发依赖的收集(track)和更新(trigger)。对于对象值,则递归调用 reactive 转换为响应式对象。这样,无论是基本类型还是对象,都能实现响应式数据绑定,支持自动追踪变化并通知相关联的副作用函数更新。

相关推荐
日升_rs10 分钟前
NextJS CVE-2025-29927 安全漏洞
前端·javascript
李少兄10 分钟前
跨域问题的解决方案
java·前端·跨域
时雨h17 分钟前
Spring Cloud Gateway 工厂模式源码详解
java·javascript·数据库
前端小万20 分钟前
SVG 使用详解
前端·svg
枫叶丹434 分钟前
【HarmonyOS Next之旅】基于ArkTS开发(三) -> 兼容JS的类Web开发(七) -> JS动画(二)
开发语言·前端·javascript·华为·harmonyos next
郝晨妤35 分钟前
鸿蒙常见面试题(欢迎投稿一起完善持续更新——已更新到62)
服务器·javascript·华为od·华为·harmonyos·鸿蒙
xxin¥37 分钟前
在 Ubuntu上安装 Node js 的三种方法
linux·javascript·ubuntu
Watermelo61743 分钟前
前端实战:基于Vue3与免费满血版DeepSeek实现无限滚动+懒加载+瀑布流模块及优化策略
前端·vue.js·人工智能·语言模型·自然语言处理·人机交互·deepseek
天若有情6731 小时前
《可爱风格 2048 游戏项目:HTML 实现全解析》
前端·游戏·html
tryCbest1 小时前
前端知识-CSS(二)
前端·css