一文打通VUE3响应式原理及其简易实现

文件结构

主要源码在 packages/reactivity/src 目录下

主要模块

effect.ts

定义响应式副作用的核心逻辑,包括 watchEffectwatch 的底层实现

核心方法

  • effect(fn, options?): 创建一个响应式副作用。
  • track(target, type, key): 收集依赖。
  • trigger(target, type, key, newValue?, oldValue?): 触发更新。

说明

  • effect.ts 是vue3实现响应式的核心模块
    • reactive.tsref.ts 都依赖 effect.ts。这是因为它们创建的响应式对象和 ref 都需要使用 effect(track | trriger) 来跟踪依赖和触发更新。
    • computed.ts 也依赖 effect.ts,因为它需要创建一个 effect 来运行 getter 函数,并收集 getter 中访问的响应式数据的依赖。
    • watch.ts 依赖 effect.ts来实现其响应式追踪。
  • tracktrigger的实现依赖三层套娃
    • 最底层名为deps使用的是Set,用来存储每个属性的 effect() 可以理解为副作用,使用Set可以去重

    • 中间层名为depsMap,使用的是Map,用来存储数据对象的每个属性的deps

    • 最上层为targetMap,使用的是WeakMap,用来存储多个响应式对象。 响应式关系:

      javascript 复制代码
      targetMap (WeakMap)     // effect.ts - 存储目标对象到 deps 的映射
           │
           └─ depsMap (Map)      // 存储属性到依赖集合的映射
               │
               └─ dep (Set)     // 存储具体依赖项

简易实现

  • 实现 effect(fn), 忽略第二个参数options
js 复制代码
// lazy load
let activeEffect = null;
function effect(fn){
    activeEffect = fn;
    activeEffect();
    activeEffect = null;
}
  • track(target,key)
js 复制代码
const targetMap = new WeakMap();
const track = (target, key) => {
  if (activeEffect) {
    let depsMap = targetMap.get(target)
    if (!depsMap) {
      targetMap.set(target, (depsMap = new Map()))
    }
    let dep = depsMap.get(key)
    if (!dep) {
      depsMap.set(key, (dep = new Set()))
    }
    dep.add(activeEffect)
  }
}
  • trigger(target,key)
js 复制代码
const trigger = (target, key) => {
  const depsMap = targetMap.get(target);
  if (!depsMap) {
    return;
  }
  const dep = depsMap.get(key);
  if (dep) {
    dep.forEach(effect => effect());
  }
}

reactive.ts

核心方法

  • reactive(target): 创建一个响应式代理对象。
  • createReactiveObject(...): 创建响应式对象的内部实现。
  • isReactive(value):判断一个对象是否是reactive对象
  • shallowReactive(target): 创建一个浅层响应式对象(只有根级别的属性是响应式的)。

说明

reactive 基于ProxyReflect实现,简单陈列以下这两个知识点,如下:

  • Proxy

    • 代理对象:创建一个对象的代理,拦截并自定义对象的基本操作(如属性访问、赋值、函数调用等)
    • 拦截操作 :可以监听 13 种对象操作(如 get, set, has, deleteProperty 等)
    • 示例如下:
    js 复制代码
    const handler = {
        get (target,key){
            return key in target ? target[key] : false
        },
        set (target,key,value) {
            target[key] = parseFloat(value)
        }
    }
    const obj = {};
    const objProxy = new Proxy(obj,handler);
    objProxy.a = '123';
    console.log(objProxy.a) // 123;
    console.log(objProxy.b) // false;
  • Reflect

    • 反射 API:提供一套操作对象的标准化方法(与 Proxy 方法一一对应)

    • 替代隐式操作 :替代直接操作对象的隐式行为(如 obj[prop]Reflect.get(obj,prop)

    • 核心特性

      js 复制代码
      // 传统方式
      obj.name = 'value';
      
      // Reflect 方式
      Reflect.set(obj, 'name', 'value');
      
      // 返回布尔值表示是否成功
      const success = Reflect.deleteProperty(obj, 'prop');
  • Proxy 和 Reflect 结合使用

    • 最佳实践模式
    js 复制代码
    const handler = {
      get(target, prop, receiver) {
        // 自定义逻辑...
        return Reflect.get(target, prop, receiver);
      },
      set(target, prop, value, receiver) {
        // 自定义逻辑...
        return Reflect.set(target, prop, value, receiver);
      }
    };
    • 结合优势

      • 保持默认行为 :通过 Reflect 调用原始操作,避免手动实现基础逻辑
      • 保持 this 绑定receiver 参数确保 this 指向代理对象(避免原型链问题)
      • 统一接口Reflect 方法与 Proxy 陷阱一一对应,代码更规范
      • 示例
      js 复制代码
      // 不使用 Reflect 的 this 问题
      const parent = { a: 1 };
      const child = new Proxy({}, {
        get(target, prop) {
          return target[prop]; // 无法访问父级属性
        }
      });
      Object.setPrototypeOf(child, parent);
      console.log(child.a); // undefined ❌
      
      // 使用 Reflect 修复
      const childCorrect = new Proxy({}, {
        get(target, prop, receiver) {
          return Reflect.get(target, prop, receiver); // ✅
        }
      });
    • 应用场景

      js 复制代码
      // 数据验证代理
      const validator = {
        set(target, prop, value) {
          if (prop === 'age' && !Number.isInteger(value)) {
            throw new TypeError('Age must be an integer');
          }
          return Reflect.set(target, prop, value);
        }
      };
      
      const person = new Proxy({}, validator);
      person.age = 30; // 正常
      person.age = 'young'; // 抛出错误
    • 原理对比

      特性 Proxy Reflect
      核心目的 拦截和自定义对象操作 提供操作对象的标准化方法
      返回值 总是返回代理对象 返回操作结果(布尔值/实际值)
      设计定位 对象的"中间件"层 替代隐式操作的显式 API
      典型使用场景 数据绑定、验证、日志记录 与 Proxy 配合实现默认行为

简易实现

javascript 复制代码
function reactive(obj) {
    const handler = {
        get(target,key,receiver) {
            const result = Relect.get(target,key,receiver);
            // 依赖收集
            track(target,key);
            return result
        },
        set(target,key,value,receiver) {
            const oldValue = target[key];
            if(oldValue !== value) {
              // change
              const result =  Relect.set(target,key,value,receiver)
              if(result) {
                  // 更新成功,执行副作用
                  trigger(target,value)
                  return value
               }
               // 更新失败
               return oldValue
            }
        }
    }
    // 返回一个proxy
    return new Proxy(obj,handler)
}

ref.ts

核心方法

  • ref(value): 创建一个 ref 对象。
  • isRef(value):判断一个对象是否是ref对象
  • unref(value): 如果参数是 ref,则返回内部值,否则返回参数本身。
  • shallowRef(value): 创建一个浅层 ref(不会对嵌套对象进行深度响应式转换)。

说明

实现ref依赖于JavaScript 计算属性,又称 JavaScript 访问器(Getter 和 Setter),注意不是vue的 computed

js是怎么拿到普通对象(不包括proxy对象)上属性的?

  1. 如果对象自身有该属性(通过 Object.hasOwnProperty() 判断),则直接返回该属性的值。
  2. 如果对象自身没有该属性,则会沿着原型链(__proto__Object.getPrototypeOf())向上查找。如果找到该属性,则返回其值;如果直到原型链顶端(null)仍未找到,则返回 undefined
  3. 如果对象的属性是一个访问器属性(即定义了 get 方法),则会调用该 get 方法。- 如果对象自身没有 getter,但原型链上的某个对象定义了 getter,则会调用原型链上的 getter
js 复制代码
const obj = {
  get a() {
    console.log('访问属性 a');
    return 1;
  }
};
console.log(obj.a); // 触发 getter

为什么不直接使用reactive的value属性来定义ref,例如下面这样?

php 复制代码
function ref(raw){
    return reactive({value: raw})
}
  1. ref 只应暴露一个value属性,如果使用reactive,就可以给他添加别的响应式属性,不符合我们的预期
  2. ref 内部有供isRefisShallow去check的额外属性。

简易实现

javascript 复制代码
function ref(raw) {
  const r = {
    // isRef 判断依据
    __v_isRef: true,
    get value() {
      track(r, 'value')
      return raw
    },
    set value(newVal) {
      if (raw !== newVal) {
        raw = newVal
        trigger(r, 'value')
      }
    },
  }
  return r
}
function isRef(target){
    return !!target[__v_isRef]
}

computed.ts

核心方法

  • computed(getterOrOptions,debugOptions?,isSSR=false): 创建一个计算属性 ref。

说明

  • computed 内部实现也使用的是类似refjs访问器
  • 第二个参数对于组件调试很有帮助
ts 复制代码
export interface DebuggerOptions {
  onTrack?: (event: DebuggerEvent) => void
  onTrigger?: (event: DebuggerEvent) => void
}

简易实现

这里就实现一个只传入getter的computed

javascript 复制代码
function computed(getter) {
    const result = ref();
    effect(() => (result.value = getter()))
    return result
}

watch.ts

核心方法

  • watch(source, cb, options?): 监听一个或多个响应式数据源,并在数据变化时执行回调。
  • watchEffect(effect,options?):立即运行一个函数,同时响应式的追踪其依赖,并在依赖更改时重新运行它

说明

简易实现

  • watch
js 复制代码
function watch(getter, callback) {
  let oldValue, newValue
  let firstRun = true
  // 利用 effect 收集 getter 内部对数据的依赖,
  // 数据变化时 effect 会重新运行,并调用 callback
  effect(() => {
    newValue = getter()
    if (firstRun) {
      // 第一次执行不触发回调
      oldValue = newValue
      firstRun = false
    } else {
      callback(oldValue, newValue)
      oldValue = newValue
    }
  })
}
  • watchEffect
javascript 复制代码
function watchEffect(fn) {
  // 利用 effect 收集 fn 内部对数据的依赖,
  // 数据变化时 effect 会重新运行,并调用 fn
  effect(fn)
}

完整示例代码

javascript 复制代码
const ReactiveFlags = {
  SKIP: '__v_skip',
  IS_REACTIVE: '__v_isReactive',
  IS_READONLY: '__v_isReadonly',
  IS_SHALLOW: '__v_isShallow',
  RAW: '__v_raw',
  IS_REF: '__v_isRef',
}

let activeEffect = null
// the code we want to run
function effect(fn) {
  activeEffect = fn
  activeEffect()
  activeEffect = null
}

const targetMap = new WeakMap()
const track = (target, key) => {
  if (activeEffect) {
    let depsMap = targetMap.get(target)
    if (!depsMap) {
      targetMap.set(target, (depsMap = new Map()))
    }
    let dep = depsMap.get(key)
    if (!dep) {
      depsMap.set(key, (dep = new Set()))
    }
    dep.add(activeEffect)
  }
}

const trigger = (target, key) => {
  const depsMap = targetMap.get(target)
  if (!depsMap) {
    return
  }
  const dep = depsMap.get(key)
  if (dep) {
    dep.forEach(effect => effect())
  }
}

function reactive(target) {
  const handler = {
    get(target, key, receiver) {
      const result = Reflect.get(target, key, receiver)
      track(target, key)
      return result
    },
    set(target, key, value, receiver) {
      const oldValue = target[key]
      const result = value
      if (oldValue != value) {
        Reflect.set(target, key, value, receiver)
        trigger(target, key)
      }
      return result
    },
  }
  return new Proxy(target, handler)
}

function ref(raw) {
  const r = {
    __v_isRef: true,
    get value() {
      track(r, 'value')
      return raw
    },
    set value(newVal) {
      if (raw !== newVal) {
        raw = newVal
        trigger(r, 'value')
      }
    },
  }
  return r
}

export function isRef(r) {
  return r ? r[ReactiveFlags.IS_REF] === true : false
}

function computed(fn) {
  const result = ref()
  effect(() => (result.value = fn()))
  return result
}

function watch(getter, callback) {
  let oldValue, newValue
  let firstRun = true
  // 利用 effect 收集 getter 内部对数据的依赖,
  // 数据变化时 effect 会重新运行,并调用 callback
  effect(() => {
    newValue = getter()
    if (firstRun) {
      // 第一次执行不触发回调
      oldValue = newValue
      firstRun = false
    } else {
      callback(oldValue, newValue)
      oldValue = newValue
    }
  })
}

function watchEffect(fn) {
  // 利用 effect 收集 fn 内部对数据的依赖,
  // 数据变化时 effect 会重新运行,并调用 fn
  effect(fn)
}

export { effect, reactive, ref, computed, watch, watchEffect }
相关推荐
发呆的薇薇°4 小时前
vue3 配置@根路径
前端·vue.js
luoluoal4 小时前
基于Spring Boot+Vue的宠物服务管理系统(源码+文档)
vue.js·spring boot·宠物
luckyext4 小时前
HBuilderX中,VUE生成随机数字,vue调用随机数函数
前端·javascript·vue.js·微信小程序·小程序
yangjiajia1234566 小时前
vue3 ref和reactive的区别
前端·javascript·vue.js
诚信爱国敬业友善6 小时前
Vue 基础二(进阶使用)
前端·javascript·vue.js
努力小贼8 小时前
uni-app发起网络请求的三种方式
前端·javascript·vue.js·uni-app
LiuMingXin8 小时前
埋头苦干Vue3项目一年半,总结出了16个代码规范
前端·vue.js·面试
还是鼠鼠8 小时前
详细介绍:封装简易的 Axios 函数获取省份列表
前端·javascript·vscode·ajax·前端框架
Aic山鱼9 小时前
Vue 3最新组件解析与实践指南:提升开发效率的利器
前端·javascript·vue.js