一起学Vue3源码,实现最简Vue3 - 实现 reactive 和 readonly 嵌套对象转换功能

一起学Vue3源码,实现最简Vue3 - 实现 reactive 和 readonly 嵌套对象转换功能

一、背景与问题

在Vue3中,响应式系统是其核心特性之一。reactive和readonly是两个关键函数,分别用于创建响应式对象和只读对象。在实际开发中,我们常常需要处理嵌套对象,比如:

const data = {
  user: {
    name: 'Alice',
    profile: {
      age: 25
    }
  }
};

如果直接使用reactive(data),所有层级的属性都会被转换为响应式对象;而使用readonly(data)则会创建一个完全不可变的对象。但实际开发中,我们可能遇到以下问题:

  1. 嵌套对象的响应式转换不完全
  2. 非对象类型(如数组、字符串)的处理不统一
  3. 在修改只读对象时需要明确的异常提示
  4. 性能开销控制问题

这些场景都需要我们深入理解Vue3的响应式系统实现原理。

二、基本原理

Vue3的响应式系统基于ES6的Proxy对象,通过重写get和set方法来实现响应式。对于嵌套对象的处理,需要递归地对每个属性进行代理。同时,readonly需要完全阻止任何修改操作。

关键原理包括:

  1. 使用Reflect API进行属性访问和修改
  2. 递归处理嵌套对象
  3. 使用WeakMap缓存代理对象
  4. 异常处理机制

三、环境准备

# 假设使用Node.js环境
node -v
# 应该 >= 14.17.0

开发工具建议:

  • VS Code
  • Chrome DevTools
  • Postman(用于测试API)

四、核心实现

1. 基础响应式实现

function reactive(obj) {
  if (typeof obj !== 'object' || obj === null) {
    return obj;
  }
  
  if (obj.__proxy__) {
    return obj.__proxy__;
  }
  
  const proxy = new Proxy(obj, {
    get(target, key) {
      const value = Reflect.get(target, key);
      if (typeof value === 'object' && value !== null) {
        return reactive(value); // 递归处理嵌套对象
      }
      return value;
    },
    set(target, key, value) {
      const oldValue = Reflect.get(target, key);
      const result = Reflect.set(target, key, value);
      
      // 检测值是否变化
      if (oldValue !== value) {
        console.log(`Property "${key}" changed from ${oldValue} to ${value}`);
      }
      
      return result;
    }
  });
  
  Object.defineProperty(obj, '__proxy__', {
    value: proxy,
    writable: true,
    enumerable: false,
    configurable: true
  });
  
  return proxy;
}

2. 只读对象实现

function readonly(obj) {
  if (typeof obj !== 'object' || obj === null) {
    return obj;
  }
  
  if (obj.__proxy__) {
    return obj.__proxy__;
  }
  
  const proxy = new Proxy(obj, {
    get(target, key) {
      const value = Reflect.get(target, key);
      if (typeof value === 'object' && value !== null) {
        return readonly(value); // 递归处理嵌套对象
      }
      return value;
    },
    set(target, key, value) {
      throw new TypeError(`Cannot assign to read-only property "${key}"`);
    },
    deleteProperty(target, key) {
      throw new TypeError(`Cannot delete property "${key}"`);
    }
  });
  
  Object.defineProperty(obj, '__proxy__', {
    value: proxy,
    writable: true,
    enumerable: false,
    configurable: true
  });
  
  return proxy;
}

3. 复合处理函数

function createReactive(obj, isReadonly = false) {
  if (typeof obj !== 'object' || obj === null) {
    return obj;
  }
  
  const proxy = new Proxy(obj, {
    get(target, key) {
      const value = Reflect.get(target, key);
      if (typeof value === 'object' && value !== null) {
        return createReactive(value, isReadonly);
      }
      return value;
    },
    set(target, key, value) {
      const oldValue = Reflect.get(target, key);
      const result = Reflect.set(target, key, value);
      
      if (oldValue !== value) {
        console.log(`Property "${key}" changed from ${oldValue} to ${value}`);
      }
      
      return result;
    }
  });
  
  Object.defineProperty(obj, '__proxy__', {
    value: proxy,
    writable: true,
    enumerable: false,
    configurable: true
  });
  
  return proxy;
}

五、完整案例

1. Todo应用示例

<!DOCTYPE html>
<html>
<head>
  <title>Todo App</title>
</head>
<body>
  <div id="app">
    <input type="text" id="new-todo" placeholder="New todo">
    <button onclick="addTodo()">Add</button>
    <ul id="todo-list"></ul>
  </div>

  <script>
    const data = {
      todos: [
        { id: 1, text: 'Learn Vue3', completed: false },
        { id: 2, text: 'Implement reactive', completed: false }
      ]
    };

    const reactiveData = createReactive(data);

    function addTodo() {
      const input = document.getElementById('new-todo');
      const text = input.value.trim();
      if (text) {
        const newTodo = {
          id: Date.now(),
          text,
          completed: false
        };
        reactiveData.todos.push(newTodo);
        input.value = '';
      }
    }

    function renderTodos() {
      const list = document.getElementById('todo-list');
      list.innerHTML = '';
      reactiveData.todos.forEach(todo => {
        const li = document.createElement('li');
        li.textContent = `${todo.text} - ${todo.completed ? 'Completed' : 'Pending'}`;
        list.appendChild(li);
      });
    }

    // 初始渲染
    renderTodos();
  </script>
</body>
</html>

2. 嵌套对象测试

const data = {
  user: {
    name: 'Alice',
    profile: {
      age: 25,
      address: {
        city: 'Beijing',
        zip: '100000'
      }
    }
  }
};

const reactiveData = createReactive(data);

// 修改嵌套属性
reactiveData.user.profile.address.city = 'Shanghai';
// 应输出: Property "user.profile.address.city" changed from Beijing to Shanghai

六、源码解析

1. 递归处理机制

在createReactive函数中,get方法处理嵌套对象时,会递归调用createReactive。这确保了所有层级的属性都被代理:

get(target, key) {
  const value = Reflect.get(target, key);
  if (typeof value === 'object' && value !== null) {
    return createReactive(value, isReadonly);
  }
  return value;
}

2. 异常处理机制

在readonly模式下,set、deleteProperty等方法都会抛出异常:

set(target, key, value) {
  throw new TypeError(`Cannot assign to read-only property "${key}"`);
}

3. 缓存机制

使用__proxy__属性缓存代理对象,避免重复创建:

Object.defineProperty(obj, '__proxy__', {
  value: proxy,
  writable: true,
  enumerable: false,
  configurable: true
});

七、进阶使用

1. 响应式计算属性

function computed(fn) {
  const result = {};
  const proxy = new Proxy(result, {
    get(target, key) {
      if (key === 'value') {
        return fn();
      }
      return target[key];
    }
  });
  return proxy;
}

2. 响应式数组

function reactiveArray(arr) {
  return new Proxy(arr, {
    get(target, key) {
      if (key === 'length') {
        return target.length;
      }
      const value = Reflect.get(target, key);
      if (typeof value === 'object' && value !== null) {
        return reactiveArray(value);
      }
      return value;
    },
    set(target, key, value) {
      const oldValue = Reflect.get(target, key);
      const result = Reflect.set(target, key, value);
      
      if (oldValue !== value) {
        console.log(`Property "${key}" changed from ${oldValue} to ${value}`);
      }
      
      return result;
    }
  });
}

八、性能与工程实践

1. 性能优化

  • 避免频繁创建Proxy对象
  • 使用缓存机制(如__proxy__)
  • 对大型对象进行分块处理

2. 异常处理

  • 对非对象类型进行类型校验
  • 添加详细的错误提示
  • 使用try-catch包裹关键逻辑

3. 安全风险

  • 避免暴露内部状态
  • 对用户输入进行过滤
  • 使用只读模式防止数据污染

九、常见问题与踩坑

1. 嵌套对象处理不完整

错误示例:

const data = {
  user: {
    name: 'Alice'
  }
};

const reactiveData = reactive(data);
reactiveData.user.name = 'Bob'; // 正常工作
reactiveData.user = { name: 'Charlie' }; // 会抛出异常

正确处理方式:需要确保对对象的重新赋值也能触发响应式更新。

2. 非对象类型处理

错误示例:

const str = 'Hello';
const reactiveStr = reactive(str); // 会返回原始字符串

解决方案:需要对字符串、数组等类型进行特殊处理。

3. 性能问题

使用Proxy可能带来性能开销,特别是处理大量数据时。可以通过以下方式优化:

  • 使用缓存机制
  • 避免不必要的代理创建
  • 对大对象进行分块处理

十、最佳实践

  1. 使用createReactive代替单独的reactive和readonly
  2. 对复杂对象进行递归处理
  3. 对用户输入进行严格校验
  4. 使用只读模式保护核心数据
  5. 对关键操作添加异常处理
  6. 对大型对象进行分块处理

十一、总结

通过实现最简版的Vue3响应式系统,我们深入理解了reactive和readonly的实现原理。在实际开发中,这种模式适用于需要严格控制数据变更的场景,如:

  • 需要防止意外修改的配置数据
  • 需要保护核心业务逻辑的只读数据
  • 需要递归处理嵌套结构的复杂数据

但需要注意避免在以下场景使用:

  • 需要频繁修改的数据
  • 对性能要求极高的场景
  • 需要处理大量数据的场景

在实际开发中,建议结合Vue3的官方API使用,同时理解其底层原理,以便在需要时进行定制化开发。通过合理使用响应式系统,可以显著提升开发效率和代码可维护性。

最后修改于:2026年09月19日 09:16

评论已关闭

推荐阅读

AIGC实战——Transformer模型
2024年12月01日
Socket TCP 和 UDP 编程基础(Python)
2024年11月30日
python , tcp , udp
如何使用 ChatGPT 进行学术润色?你需要这些指令
2024年12月01日
AI
最新 Python 调用 OpenAi 详细教程实现问答、图像合成、图像理解、语音合成、语音识别(详细教程)
2024年11月24日
ChatGPT 和 DALL·E 2 配合生成故事绘本
2024年12月01日
omegaconf,一个超强的 Python 库!
2024年11月24日
【视觉AIGC识别】误差特征、人脸伪造检测、其他类型假图检测
2024年12月01日
[超级详细]如何在深度学习训练模型过程中使用 GPU 加速
2024年11月29日
Python 物理引擎pymunk最完整教程
2024年11月27日
MediaPipe 人体姿态与手指关键点检测教程
2024年11月27日
深入了解 Taipy:Python 打造 Web 应用的全面教程
2024年11月26日
基于Transformer的时间序列预测模型
2024年11月25日
Python在金融大数据分析中的AI应用(股价分析、量化交易)实战
2024年11月25日
AIGC Gradio系列学习教程之Components
2024年12月01日
Python3 `asyncio` — 异步 I/O,事件循环和并发工具
2024年11月30日
llama-factory SFT系列教程:大模型在自定义数据集 LoRA 训练与部署
2024年12月01日
Python 多线程和多进程用法
2024年11月24日
Python socket详解,全网最全教程
2024年11月27日
python之plot()和subplot()画图
2024年11月26日
理解 DALL·E 2、Stable Diffusion 和 Midjourney 工作原理
2024年12月01日