一起学Vue3源码,实现最简Vue3 - 实现 reactive 和 readonly 嵌套对象转换功能
一起学Vue3源码,实现最简Vue3 - 实现 reactive 和 readonly 嵌套对象转换功能
一、背景与问题
在Vue3中,响应式系统是其核心特性之一。reactive和readonly是两个关键函数,分别用于创建响应式对象和只读对象。在实际开发中,我们常常需要处理嵌套对象,比如:
const data = {
user: {
name: 'Alice',
profile: {
age: 25
}
}
};如果直接使用reactive(data),所有层级的属性都会被转换为响应式对象;而使用readonly(data)则会创建一个完全不可变的对象。但实际开发中,我们可能遇到以下问题:
- 嵌套对象的响应式转换不完全
- 非对象类型(如数组、字符串)的处理不统一
- 在修改只读对象时需要明确的异常提示
- 性能开销控制问题
这些场景都需要我们深入理解Vue3的响应式系统实现原理。
二、基本原理
Vue3的响应式系统基于ES6的Proxy对象,通过重写get和set方法来实现响应式。对于嵌套对象的处理,需要递归地对每个属性进行代理。同时,readonly需要完全阻止任何修改操作。
关键原理包括:
- 使用
ReflectAPI进行属性访问和修改 - 递归处理嵌套对象
- 使用
WeakMap缓存代理对象 - 异常处理机制
三、环境准备
# 假设使用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可能带来性能开销,特别是处理大量数据时。可以通过以下方式优化:
- 使用缓存机制
- 避免不必要的代理创建
- 对大对象进行分块处理
十、最佳实践
- 使用
createReactive代替单独的reactive和readonly - 对复杂对象进行递归处理
- 对用户输入进行严格校验
- 使用只读模式保护核心数据
- 对关键操作添加异常处理
- 对大型对象进行分块处理
十一、总结
通过实现最简版的Vue3响应式系统,我们深入理解了reactive和readonly的实现原理。在实际开发中,这种模式适用于需要严格控制数据变更的场景,如:
- 需要防止意外修改的配置数据
- 需要保护核心业务逻辑的只读数据
- 需要递归处理嵌套结构的复杂数据
但需要注意避免在以下场景使用:
- 需要频繁修改的数据
- 对性能要求极高的场景
- 需要处理大量数据的场景
在实际开发中,建议结合Vue3的官方API使用,同时理解其底层原理,以便在需要时进行定制化开发。通过合理使用响应式系统,可以显著提升开发效率和代码可维护性。
评论已关闭