vue 3.x 响应式系统的实现

一、简单版本的响应式系统

javascript 复制代码
// 存储依赖的全局桶
const bucket = new WeakMap();

// 当前激活的副作用函数
let activeEffect;

// 响应式对象
const data = { text: 'hello' };
const obj = new Proxy(data, {
  get(target, key) {
    track(target, key);
    return target[key];
  },
  set(target, key, newVal) {
    target[key] = newVal;
    trigger(target, key);
    return true;
  }
});

// 依赖收集
function track(target, key) {
  if (!activeEffect) return;
  let depsMap = bucket.get(target);
  if (!depsMap) {
    bucket.set(target, (depsMap = new Map()));
  }
  let deps = depsMap.get(key);
  if (!deps) {
    depsMap.set(key, (deps = new Set()));
  }
  deps.add(activeEffect);
}

// 依赖触发
function trigger(target, key) {
  const depsMap = bucket.get(target);
  if (!depsMap) return;
  const effects = depsMap.get(key);
  effects && effects.forEach(fn => fn());
}

// 副作用函数注册逻辑(示例)
function effect(fn) {
  activeEffect = fn;
  fn(); // 首次执行以触发依赖收集
}

验证

javascript 复制代码
// 示例:验证响应式更新
effect(() => {
  console.log('Effect triggered:', obj.text);
});

obj.text = 'vue3'; // 输出 "Effect triggered: vue3"

存在的问题

js 复制代码
const data = {ok:true, text: 'hello world'}
const obj = new Proxy(data,{/**/})
effect(function effectFn() {
document.body.innerText = obj.ok ? obj.text : 'not'
})

当 effectFn 执行时会触发 obj.ok 和 obj.text 的读取操作,进而该副作用函数会被这两个字段收集进依赖集合。

当修改 obj.ok 为 false,并触发副作用函数重新执行后,document.body.innerText 必为 'not',无论 obj.text 如何修改。

但是事实并非如此,当我们修改 obj.text 时,副作用函数会被重新执行。

以上说明副作用函数中的分支切换会产生遗留的副作用函数,遗留的副作用函数会导致不必要的更新。

解决这个问题就是要在每次副作用函数执行时,先把该副作用函数从所有与之有关的依赖集合里删除,当副作用函数执行完毕后,会重新建立联系。

于是我们可以设计 effectFn.deps 属性来存储包含当前副作用函数的依赖集合。

二、优化后的响应式系统

js 复制代码
// 存储依赖关系的容器(WeakMap<target, Map<key, Set<effect>>>)
const targetMap = new WeakMap()

// 当前激活的副作用函数
let activeEffect = null

// 副作用函数包装器
function effect(fn) {
  const effectFn = () => {
    cleanup(effectFn) // 执行清理
    activeEffect = effectFn
    fn()
  }
  effectFn.deps = [] // 存储关联的依赖集合
  effectFn()
}

// 清理函数
function cleanup(effectFn) {
  for (const dep of effectFn.deps) {
    dep.delete(effectFn)
  }
  effectFn.deps.length = 0
}

// 依赖收集
function track(target, key) {
  if (!activeEffect) return
  
  let depsMap = targetMap.get(target)
  if (!depsMap) {
    depsMap = new Map()
    targetMap.set(target, depsMap)
  }
  
  let dep = depsMap.get(key)
  if (!dep) {
    dep = new Set()
    depsMap.set(key, dep)
  }
  
  dep.add(activeEffect)
  activeEffect.deps.push(dep) // 反向记录依赖集合
}

// 触发更新
function trigger(target, key) {
  const depsMap = targetMap.get(target)
  if (!depsMap) return
  
  const effects = depsMap.get(key)
  if (effects) {
    // 创建副本防止无限循环
    const effectsToRun = new Set(effects)
    effectsToRun.forEach(effect => effect())
  }
}

// 创建响应式对象
function reactive(data) {
  return new Proxy(data, {
    get(target, key) {
      track(target, key)
      return target[key]
    },
    set(target, key, newValue) {
      target[key] = newValue
      trigger(target, key)
      return true
    }
  })
}

三、关键流程总结

首次执行流程

分支切换后重新执行流程

依赖关系变化对比

阶段 obj.ok 的依赖集合 obj.text 的依赖集合
首次执行后 [effectFn] [effectFn]
修改 obj.ok=false [effectFn](重新添加) [](已清理)

四、关键优化点说明

  1. 双向依赖记录

    • 每个属性维护自己的依赖集合(Set<effect>
    • 每个effect维护自己关联的依赖集合(deps: Set[]
  2. cleanup机制

    scss 复制代码
    function cleanup(effectFn) {
      // 遍历所有关联的依赖集合
      for (const dep of effectFn.deps) {
        // 从依赖集合中移除当前effect
        dep.delete(effectFn)
      }
      // 清空关联记录
      effectFn.deps.length = 0
    }
  3. 执行时序控制

    • 在每次effect执行前先清理旧依赖
    • 执行时重新建立新依赖
    • 通过activeEffect标记当前激活的effect

五、总结

  1. 首次执行时机effect 在定义时立即执行,确保首次渲染和依赖收集。
  2. cleanup 机制:每次重新执行副作用函数前,清理所有旧依赖,避免冗余更新。
  3. 动态依赖更新:重新执行时,根据当前条件分支访问的属性,动态建立新的依赖关系。

通过这一机制,响应式系统能够精确追踪依赖,避免不必要的计算和更新,从而显著提升性能。

相关推荐
a濯5 小时前
element plus el-table多选框跨页多选保留
javascript·vue.js
九月TTS6 小时前
开源分享:TTS-Web-Vue系列:Vue3实现固定顶部与吸顶模式组件
前端·vue.js·开源
H309196 小时前
vue3+dhtmlx-gantt实现甘特图展示
android·javascript·甘特图
CodeCraft Studio6 小时前
数据透视表控件DHTMLX Pivot v2.1发布,新增HTML 模板、增强样式等多个功能
前端·javascript·ui·甘特图
llc的足迹7 小时前
el-menu 折叠后小箭头不会消失
前端·javascript·vue.js
九月TTS7 小时前
TTS-Web-Vue系列:移动端侧边栏与响应式布局深度优化
前端·javascript·vue.js
曾经的你d7 小时前
【electron+vue】常见功能之——调用打开/关闭系统软键盘,解决打包后键盘无法关闭问题
vue.js·electron·计算机外设
积极向上的龙8 小时前
首屏优化,webpack插件用于给html中js自动添加异步加载属性
javascript·webpack·html
Bl_a_ck8 小时前
开发环境(Development Environment)
开发语言·前端·javascript·typescript·ecmascript
田本初9 小时前
使用vite重构vue-cli的vue3项目
前端·vue.js·重构