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

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

相关推荐
java_jun4 分钟前
pdfjs库使用记录1
javascript
Cutey91623 分钟前
前端SEO优化方案
前端·javascript
八了个戒1 小时前
「数据可视化 D3系列」入门第六章:比例尺的使用
前端·javascript·信息可视化·数据可视化·canvas
CHQIUU2 小时前
PDF.js 生态中如何处理“添加注释\添加批注”以及 annotations.contents 属性
开发语言·javascript·pdf
Json_2 小时前
使用vue2技术写了一个纯前端的静态网站商城-鲜花销售商城
前端·vue.js·html
冴羽2 小时前
SvelteKit 最新中文文档教程(22)—— 最佳实践之无障碍与 SEO
前端·javascript·svelte
ohMyGod_1232 小时前
Vue如何实现样式隔离
前端·javascript·vue.js
Abadbeginning2 小时前
vue3后台管理框架geeker admin 横向布局(了解)
前端·javascript·vue.js
OpenTiny社区2 小时前
直播分享|TinyVue 多端实战与轻量图标库分享
前端·vue.js·开源
WEI_Gaot2 小时前
Promise 的类方法 和 实例方法
前端·javascript