面试官:请手写一个简化版的Vue 3响应式系统

197 阅读5分钟

导语
你是否好奇Vue/React的响应式原理?想自己实现一个“自动更新”的魔法系统?本文通过最简代码还原响应式核心逻辑,并揭示4个初学者必踩的坑。读完你将掌握:依赖收集、派发更新、嵌套effect等核心概念,写出更健壮的响应式代码!

一、响应式系统的“骨架”

想象一个智能Excel表格:当某个单元格数据变化时,依赖它的图表会自动刷新。响应式系统的核心就是:

  1. 依赖收集(谁用了我?)
  2. 派发更新(我变了,通知谁?)

我们用WeakMap存储依赖关系,Proxy拦截对象操作,先看基础实现👇:

const targetMap = new WeakMap() 
let activeEffect = null 

// 核心三件套
export const effect = (fn) => { /*...*/ }  // 副作用函数
export const reactive = (target) => { /*...*/ } // 响应式对象
const track = () => { /*...*/ }  // 收集依赖
const trigger = () => { /*...*/ } // 触发更新

二、简化版的Vue 3响应式系统

const targetMap = new WeakMap() // 存储依赖关系
let activeEffect = null // 当前执行的副作用函数
// 副作用函数
export const effect = fn => {
  activeEffect = fn
  fn()
  activeEffect = null
}
// 收集依赖
const track = (target, prop) => {
  if (!activeEffect) return
  let depsMap = targetMap.get(target)
  if (!depsMap) targetMap.set(target, (depsMap = new Map()))
  let dep = depsMap.get(prop)
  if (!dep) depsMap.set(prop, (dep = new Set()))
  dep.add(activeEffect)
}
// 触发更新
const trigger = (target, prop) => {
  const depsMap = targetMap.get(target)
  if (!depsMap) return
  const effects = depsMap.get(prop)
  if (effects) effects.forEach(effect => effect())
}
//  响应式对象
export const reactive = target => {
  return new Proxy(target, {
    get(target, prop, receiver) {
      track(target, prop)
      return Reflect.get(target, prop, receiver)
    },
    set(target, prop, value, receiver) {
      const result = Reflect.set(target, prop, value, receiver)
      trigger(target, prop)
      return result
    },
  })
}

三、新手必踩的4个坑(附解决方案)

陷阱1:嵌套Effect导致依赖丢失

场景:组件嵌套时,内部组件渲染会覆盖外部effect
现象:外层数据变化不再触发更新
解法:用“函数调用栈”管理effect层级

const effectStack = [] // 新增栈结构
const execute = () => {
  effectStack.push(execute)  // 入栈
  activeEffect = execute
  fn()
  effectStack.pop()          // 出栈
  activeEffect = effectStack[effectStack.length - 1] // 恢复上一层
}

陷阱2:相同值重复触发更新

场景obj.count = 1多次赋值相同值
现象:引发不必要渲染甚至死循环
解法:新旧值对比后再触发

// set拦截器中添加判断
if (!hadKey || !Object.is(value, oldValue)) {
  trigger(target, prop) 
}

陷阱3:数组长度变化无响应

场景arr[10] = 1导致length变化
现象:监听length的属性不更新
解法:新增元素时主动触发length更新

if (Array.isArray(target) && !hadKey) {
  trigger(target, 'length') // 对数组特殊处理
}

陷阱4:删除属性无响应

场景delete obj.prop
现象:依赖该属性的视图不更新
解法:拦截delete操作

deleteProperty(target, prop) {
  if (hadKey) trigger(target, prop) // 触发更新
  return result
}

四、问题总结及改进

问题总结:

  1. 嵌套 Effect 问题:当 Effect 嵌套时,内部的 Effect 执行后会将 activeEffect 置为 null,导致外层 Effect 后续的依赖收集失败。
  2. 相同值触发更新:设置属性时未检查新旧值是否相同,导致不必要的更新和可能的无限循环。
  3. 数组长度变化未触发更新:通过索引设置数组元素时,若隐式改变 length 属性,未触发相关依赖。
  4. 删除属性未触发更新:未处理 delete 操作,导致属性删除时依赖不更新。

改进后的代码:

const targetMap = new WeakMap(); // 存储依赖关系
let activeEffect = null; // 当前执行的副作用函数
const effectStack = []; // 处理嵌套 Effect

// 副作用函数
export const effect = (fn) => {
  const execute = () => {
    try {
      effectStack.push(execute);
      activeEffect = execute;
      fn();
    } finally {
      effectStack.pop();
      activeEffect = effectStack[effectStack.length - 1] || null;
    }
  };
  execute();
};

// 收集依赖
const track = (target, prop) => {
  if (!activeEffect) return;
  let depsMap = targetMap.get(target);
  if (!depsMap) targetMap.set(target, (depsMap = new Map()));
  let dep = depsMap.get(prop);
  if (!dep) depsMap.set(prop, (dep = new Set()));
  dep.add(activeEffect);
};

// 触发更新
const trigger = (target, prop) => {
  const depsMap = targetMap.get(target);
  if (!depsMap) return;
  const effects = depsMap.get(prop);
  if (effects) {
    // 避免在遍历过程中执行 effect 导致的清理和重新收集问题
    new Set(effects).forEach(effect => effect());
  }
};

// 响应式对象
export const reactive = (target) => {
  return new Proxy(target, {
    get(target, prop, receiver) {
      track(target, prop);
      return Reflect.get(target, prop, receiver);
    },
    set(target, prop, value, receiver) {
      const oldValue = Reflect.get(target, prop, receiver);
      const hadKey = Object.prototype.hasOwnProperty.call(target, prop);
      const result = Reflect.set(target, prop, value, receiver);
      
      // 触发更新条件:新增属性或值发生变化
      if (!hadKey) {
        trigger(target, prop);
        // 处理数组新增元素导致 length 变化
        if (Array.isArray(target)) {
          trigger(target, 'length');
        }
      } else if (!Object.is(value, oldValue)) {
        trigger(target, prop);
      }
      return result;
    },
    deleteProperty(target, prop) {
      const hadKey = Object.prototype.hasOwnProperty.call(target, prop);
      const result = Reflect.deleteProperty(target, prop);
      if (hadKey) {
        trigger(target, prop);
      }
      return result;
    }
  });
};

改进说明:

  1. 嵌套 Effect:使用 effectStack 栈结构管理嵌套 Effect,确保内外层 Effect 正确追踪。
  2. 值变化检查:使用 Object.is 比较新旧值,避免相同值触发更新。
  3. 数组长度处理:在新增数组元素时(属性不存在的情况),主动触发 length 属性的更新。
  4. 删除属性:拦截 deleteProperty 操作并触发更新,确保依赖跟踪的正确性。

注意事项:

  • 数组方法处理:通过 pushpop 等方法修改数组时,需确保所有相关属性(如索引和 length)触发更新。此实现已部分处理,但更复杂的数组操作可能需要额外处理。
  • 性能优化trigger 中使用 new Set(effects) 避免在遍历过程中修改 Set 导致的无限循环问题。
  • 边缘情况:如 NaN 处理、符号属性等,根据需求可进一步扩展。

结语:

响应式系统如同精密的齿轮组,每一个边界case都可能导致“卡壳”。理解这些陷阱后,你不仅能更好地使用Vue/React,更能写出高可靠的底层库。动手实现一次,胜过阅读十篇原理分析

面试过程中一般时间紧,只需要写出简化版的响应式系统原理即可,但里面的坑也要必知必会,才能吊打面试官~~~