ref和reactive的实现原理

引言

ref与reactive都是vue3中的响应式api,用于创建响应式数据。所谓响应式就是在适当的时间去触发某个函数。

ref与reactive的区别:

  1. ref 常用于基本数据类型,当接受的是一个对象时,内部会使用reactive机制来处理。reactive 常用于对象和数组
  2. ref 使用原生js的getter和setter机制, reactive 使用es6上的proxy代理机制
  3. ref需要.value , reactive不用

reactive 原理

手搓一个reactive:创建一个文件夹reactivity,在里面创建一个reactive.js,在里面抛出一个函数reactive(),然后返回一个响应式对象createReactiveObject()函数。这样就可以在其它地方调用手写的方法了。

js 复制代码
// reactive.js
export function reactive(target) {
  return createReactiveObject(target)
}

function createReactiveObject(target) {

}

createReactiveObject函数里面首先判断target是否为一个对象,不是则直接返回,反之则使用proxy代理这个传进来的值,但是呢,为了更好维护就将proxy里面的set,get等方法放到另外的文件里了,vue3源码将其命名为mutableHandlers。如下;

js 复制代码
// reactive.js
import { mutableHandles } from './baseHandlers.js'

export function reactive(target) {
  return createReactiveObject(target, mutableHandles)
}

function createReactiveObject(target, proxyHandlers) {   // 创建响应式对象
  if (typeof target !== 'object') {
    return target
  }
  const proxy = new Proxy(target, proxyHandlers)
  
  return proxy
}

但是呢,还考虑到代理过的对象就不需要再代理了,但是代理过的对象与原对象没有什么不同的特征,于是官方又创建了一个WeakMap对象,用来标记是否已经被代理过。

js 复制代码
// reactive.js
import { mutableHandles } from './baseHandlers.js'   // 将proxy里面的set,get等方法封装
export const reactiveMap = new WeakMap()  // 对象用来标记是否被代理过

export function reactive(target) {
  return createReactiveObject(target, mutableHandles, 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
}

接下来就是写baseHandlers.js里面的代码了。

js 复制代码
// baseHandlers.js
const get = createGetter()
const set = createSetter()

function createGetter() {
  return function (target, key, receiver) {

  }
}

function createSetter() {
  return function (target, key, value, receiver) {
    
  }
}

export const mutableHandles = {
  get,
  set
}

为什么要这样写,我也不知道,大佬是这样写的,为了更好维护.然后再在里面return出值就可以了。

js 复制代码
function createGetter() {
  return function (target, key, receiver) {
    return Reflect.get(target, key, receiver)
  }
}

function createSetter() {
  return function (target, key, value, receiver) {
     return Reflect.set(target, key, value, receiver)
  }
}

但是呢?如果代理的对象的属性是一个对象的话,get值时就需要接着往下响应,子对象递归。所以需要先判断是否为一个对象。

js 复制代码
// shared/index.js
export function isObject(val) {
  return val !== null && typeof val === 'object'
}
js 复制代码
// baseHandlers.js
import { isObject } from '../shared/index.js'
import { reactive } from './reactive.js'

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

function createGetter() {
  return function (target, key, receiver) {
    const res = Reflect.get(target, key, receiver)

    if (isObject(res)) {
      return reactive(res)   // 子对象再代理
    }
    return res
  }
}

function createSetter() {
  return function (target, key, value, receiver) {
    const res = Reflect.set(target, key, value, receiver)
    return res
  }
}

export const mutableHandles = {
  get,
  set
}

然后就是触发响应式了,当reactive里面的值变更后,会带来视图更新,compute计算属性,watch监听属性触发,页面上用到这个值会更新,这些统称为依赖,那么就需要实现当值改变时这些依赖都重新触发。

比如:

js 复制代码
// App.vue
import { reactive } from './reactivity/reactive.js'
const state = reactive({
  count: 0,
  like: {
    a: 2
  }
})

const res = computed(() => {  // 依赖于state.count
  return state.count + 1
})

const add = () => {
  state.count++     // 当state.count改变会导致computed触发
}

然后就是要收集全部依赖,也就是副作用函数,跟这些变量绑在一起的函数。当App.vue加载完就需要知道state.count在哪些地方用到了,那么怎么知道呢?就是当state.count被用到的时候就相当于state.count被读取到的时候,就会触发get函数,所以就可以在get里面进行依赖收集。

代码如下:

js 复制代码
// baseHandlers.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 mutableHandles = {
  get,
  set
}

这样基本就完成了,get里面调用track进行依赖收集,set里面调用trigger触发依赖。将这两个函数放在./effect.js里面。接下来就是./effect.js文件里面的track和trigger了。

如何进行依赖收集,大佬将依赖收集成如下:

js 复制代码
targetMap = {   // 收集到一个map对象里面
  obj1: {       // 第一个reactive接收的对象
    // 对象里面的属性作key
    key1: [effect1, effect2, ...],     // effect为该属性的依赖也就是用到这个属性的函数
    key2: [effect1, effect2, ...],
  },
  obj2:{        // 第二个reactive接收的对象
    key:  [effect1, effect2, ...]
  }
}
js 复制代码
// vue3 中
effect(() => {
  console.log('effect', state.count);
})

在 Vue 3 中,effect 是一个用于追踪依赖和触发副作用的函数,effect 函数的主要作用是允许你定义一个函数,这个函数会在其依赖的响应式数据发生变化时自动重新执行。

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

export function effect(fn, options = {}) {  // 更新时触发
  const effectFn = () => {
    try {
      activeEffect = fn   // 将回调存到 activeEffect
      return fn()
    } catch (error) {
      activeEffect = null
    }
  }
  effectFn()
}

// 依赖收集
export function track(target, type, key) {
  // targetMap = {
  //   state: {
  //     key: [effect1, effect2, ...]
  //   }
  // }
  let depsMap = targetMap.get(target)      // 先看下该对象是否已经收集过
  if (!depsMap) {         // 如果没有收集过
    targetMap.set(target, (depsMap = new Map()))
  }
  let deps = depsMap.get(key)     // 对象上的属性是否收集过
  if (!deps) {
    deps = new Set()     // 也相当于 [effect1, effect2, ...]
  }
  if (!deps.has(activeEffect) && activeEffect) {      // activeEffect 就是computed里面的回调函数
    deps.add(activeEffect)     
  }
  depsMap.set(key, deps)    // 往map里面添加
}

// 触发依赖
export function trigger(target, type, key) {
  const depsMap = targetMap.get(target)
  if (!depsMap) return  // 定义了一个对象,没有使用过,没有依赖这个target的副作用函数

  const deps = depsMap.get(key)
  if (!deps) return    // 没有依赖这个具体属性的副作用函数

  deps.forEach(effect => {
    effect()   // 这里触发
  })
}

以上就是 reactive 的简版实现过程了。

js 复制代码
// 效果
<template>
  <div @click="add">
    hello world -- {{ state.count }} -- {{ res }}
  </div>
</template>

<script setup>
import { reactive } from './reactivity/reactive.js'
import { effect } from './reactivity/effect.js'
const state = reactive({
  count: 0,
  like: {
    a: 2
  }
})

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

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

当代码通篇被读到的时候,effect函数作为副作用函数被收集起来了,等到修改state.count的时候,就会触发proxy里面的set,set里面触发trigger,trigger就会帮你找出state.count的依赖有哪些,然后遍历执行。

因为官方打造的proxy方法第一个参数不支持原始数据类型,所以reactive不能处理原始类型。

ref 原理

ref接收的如果是一个原始数据类型就走类里面的get和set机制,如果接收的是一个引用类型,则与reactive一样使用proxy函数来实现。

js 复制代码
// ref.js
export function ref(val) {
  return new RefImpl(val)
}

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

  get value() {
    return this._val
  }
}

const n = ref(1)
console.log(n.value);

如果RefImpl类里面的value函数前面不加get就需要n.value()来访问。所以我们使用ref后面接的数据的使用要加.value,这个value其实是个函数体,因为有这个语法(往一个函数前面加get,set调用该函数时就不要打括号)存在所以省略了()。

如果是原始类型:

js 复制代码
set value(newVal) {
  if (newVal !== this._val) {
    this._val = newVal
  }
}

加个这个就结束了,但是如果是个引用类型的话,就需要使用reactive实现响应式的机制了。加个判断就可以了。完整代码如下;

js 复制代码
// 完整 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')
    }
  }
}

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

ref 与 reactive 区别

reactive使用proxy代理了对象上的各种操作行为,在读取属性这个过程当中,为属性去添加副作用函数,在属性修改或删除时去触发该属性身上绑定的副作用函数,ref当参数是引用类型时,直接借助reactive机制来完成,当参数是一个原始值的时候,借助原生js中的getter和setter机制,实现为值做副作用函数收集和触发副作用函数的效果。

相关推荐
Fantasywt2 小时前
THREEJS 片元着色器实现更自然的呼吸灯效果
前端·javascript·着色器
IT、木易3 小时前
大白话JavaScript实现一个函数,将字符串中的每个单词首字母大写。
开发语言·前端·javascript·ecmascript
ZXT4 小时前
面试精讲 - vue3组件之间的通信
vue.js
张拭心5 小时前
2024 总结,我的停滞与觉醒
android·前端
念九_ysl5 小时前
深入解析Vue3单文件组件:原理、场景与实战
前端·javascript·vue.js
Jenna的海糖5 小时前
vue3如何配置环境和打包
前端·javascript·vue.js
uhakadotcom5 小时前
Apache CXF 中的拒绝服务漏洞 CVE-2025-23184 详解
后端·面试·github
uhakadotcom5 小时前
CVE-2025-25012:Kibana 原型污染漏洞解析与防护
后端·面试·github
uhakadotcom5 小时前
揭秘ESP32芯片的隐藏命令:潜在安全风险
后端·面试·github
uhakadotcom5 小时前
Apache Camel 漏洞 CVE-2025-27636 详解与修复
后端·面试·github