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. 动态依赖更新:重新执行时,根据当前条件分支访问的属性,动态建立新的依赖关系。

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

相关推荐
雪芽蓝域zzs2 小时前
第4节:el-select 弹窗事件控制(打开 / 关闭、选中事件、弹窗滚动事件)
vue.js·elementui
PYB32 小时前
【Web·JS·基础】函数的使用和展运算符...objs
前端·javascript
默_笙3 小时前
🚥 给 RAG 装一台心电监护仪:LangSmith 全链路观测(上)
前端·javascript
变与不变8065 小时前
js同步和异步难点重点详解
开发语言·javascript·ecmascript
Dovis(誓平步青云)5 小时前
多个链接不等于多份证据,新闻核验看板怎样合并来源
java·服务器·前端·javascript·人工智能·pdf·电脑
Dovis(誓平步青云)6 小时前
几个方案来回选不定?做一个随时切换的候选推荐页
android·java·服务器·开发语言·javascript·数据库·智能化
可乐鸡翅yeah_8 小时前
fmp4 碎片 MP4 格式 HLS 流,和传统 TS 分片有什么不一样
开发语言·javascript·ios·音视频·safari·m3u8·m3u8在线播放
ss2738 小时前
AI全栈实战 | 2.3-01 组件库实战:Element Plus 怎么搭出专业界面,过度封装为什么成维护负担
前端·javascript·elementui·vue
福兮说9 小时前
URL 编码的七个坑:c++ 传到后端变空格、%25 套娃、截断 emoji 直接报错
开发语言·前端·javascript·node.js·url
小的时候可菜了9 小时前
JS 异步的本质:从单线程到 thenable(一条进化链)
开发语言·前端·javascript