响应式实现与设计
前端开发中,“响应式”是实现数据与视图同步的核心能力。这种能力我首先想到是Vue的双向绑定,那就看看其是如何实现的
一、Vue2中
Vue 2 主要通过
Object.defineProperty对数据进行劫持,监听属性的读取(get)和修改(set)操作,结合依赖收集和触发更新的机制实现双向绑定。
1. 核心工具:Object.defineProperty
Object.defineProperty(obj, prop, descriptor)可以给对象的属性添加 getter(读取时触发)和 setter(修改时触发),从而拦截对属性的访问和修改操作。
2. 关键模块
Observer(观察者):负责递归遍历对象的所有属性,为每个属性添加 getter 和 setter,实现数据劫持。
Dep(依赖收集器):每个属性对应一个 Dep,用于收集所有依赖该属性的 Watcher(观察者)。
Watcher(观察者):作为桥梁,连接 Dep 和视图/计算属性等。当数据变化时,Dep 会通知所有关联的 Watcher 触发更新。
3. 具体流程
数据劫持
Vue 实例创建时,会对
data中的对象进行递归遍历,为每个属性调用Object.defineProperty添加 getter 和 setter:
function defineReactive(obj, key, val) {
const dep = new Dep(); // 每个属性对应一个 Dep
Object.defineProperty(obj, key, {
get() {
// 读取属性时,收集依赖(将当前 Watcher 加入 Dep)
if (Dep.target) {
dep.depend();
}
return val;
},
set(newVal) {
if (val === newVal) return; // 值未变化时不触发更新
val = newVal;
// 修改属性时,通知所有依赖的 Watcher 更新
dep.notify();
}
});
}依赖收集
当视图渲染(如
{{ message }}或v-model)时,会创建对应的 Watcher。Watcher 在初始化时会读取相关数据(触发 getter),此时 Dep 会将当前 Watcher 记录为自己的依赖(即该视图依赖这个数据)。
数据修改触发更新
当数据被修改时(如
this.message = 'new value'),会触发 setter,通知 Dep 调用所有关联 Watcher 的update方法。Watcher 收到通知后,会重新渲染视图,从而实现数据到视图的同步。
二、Vue 3
Vue 3 为了更高效地处理对象和数组的响应式,以及支持更复杂的场景(如深层对象、动态新增属性),改用
Proxy替代Object.defineProperty。
1. 核心工具:Proxy
Proxy可以代理整个对象,拦截对象的各种操作(如读取、修改、删除属性,甚至遍历等),比 Object.defineProperty更灵活和全面。
2. 关键升级点
无需递归初始化:
Proxy代理的是整个对象,只有在访问具体属性时才会触发拦截,因此不需要在初始化时递归遍历所有子属性(懒加载)。支持数组的原生操作:
Object.defineProperty需要重写数组的push/pop/shift/unshift等方法来触发更新,而Proxy可以直接拦截数组的操作。支持动态新增/删除属性:
Object.defineProperty无法检测对象新增或删除属性(需手动调用Vue.set/Vue.delete),而Proxy可以通过拦截set和deleteProperty操作实现响应式。
3. 核心逻辑(简化版)
Vue 3 的响应式系统核心是 reactive函数,返回一个 Proxy 代理对象:
function reactive(target) {
if (typeof target !== 'object' || target === null) {
return target;
}
const handler = {
get(target, key, receiver) {
// 收集依赖(类似 Vue 2 的 Dep)
track(target, key);
// 递归代理子属性(实现深层响应式)
return Reflect.get(target, key, receiver);
},
set(target, key, value, receiver) {
// 触发更新(类似 Vue 2 的 notify)
trigger(target, key);
return Reflect.set(target, key, value, receiver);
}
};
return new Proxy(target, handler);
}4. 依赖收集与触发
Vue 3 用 Effect函数替代了 Watcher,通过 track(收集依赖)和 trigger(触发更新)函数管理依赖关系:
当读取响应式数据时(如模板渲染或计算属性),调用
track记录当前正在执行的Effect。当修改响应式数据时(如赋值操作),调用
trigger找到所有关联的Effect并重新执行,从而更新视图
总的来说,Vue 的双向绑定通过 数据劫持 实现对数据的监听,通过 依赖收集 确定哪些视图依赖该数据,最终通过 发布-订阅模式 在数据变化时触发视图更新,同时通过 v-model等指令绑定视图事件,实现视图到数据的反向更新。
看完Vue的实现方式,顺便看看最近在使用的MST(MobX-State-Tree)中是如何实现响应式的
三、MST( MobX-State-Tree)文档
1. 核心工具:Proxy
MST和Vue一样 的响应式能力均依赖 ES6 的
Proxy两者通过Proxy拦截对象的操作(如属性读取、修改、删除等),实现对数据变化的追踪和响应。MST 的状态树(State Tree)由多个“节点”(Nodes)组成,每个节点都是一个被
Proxy代理的可观察对象。MST 的Proxy不仅追踪数据变化,还额外记录状态的“类型定义”(如模型字段的类型、动作(Actions)的约束等),以实现更复杂的状态管理逻辑(如状态校验、历史回溯、持久化等)
2. 关键模块
MST 的关键模块围绕“结构化状态管理”展开,通过:
依赖追踪:依赖追踪是响应式的核心:当数据被访问时,需要知道“哪些逻辑依赖它”,以便数据变化时触发这些逻辑。MST 的依赖追踪是显式结构关联的:状态的修改会触发与该状态节点关联的“副作用”(如
watch、actions中的逻辑),但依赖关系由状态的类型定义和action的作用域显式决定。不可变性:MST 强制要求所有状态修改必须通过
action(类似 Redux 的reducer),确保修改逻辑集中且可调试。action可以被标记为async,支持异步操作,且内部修改状态时会自动触发响应式更新。状态快照与时间旅行:MST 内置了状态快照(Snapshot)和时间旅行(Time Travel)能力,通过
onSnapshot监听状态变化并记录历史,方便调试和回滚状态。
使用示例
import { types } from 'mobx-state-tree';
// 定义子模型:地址
const Address = types.model({
city: types.string,
street: types.string,
zipCode: types.string
}).actions(self => ({
updateCity(newCity: string) {
self.city = newCity; // 通过 action 修改子模型状态
}
}));
// 定义主模型:用户
const User = types.model({
id: types.identifier, // 标识符(唯一键)
name: types.string,
age: types.number,
address: Address, // 嵌套子模型
hobbies: types.array(types.string) // 字符串数组
}).props({
// 可选:静态属性(不可修改)
createdAt: types.string
}).defaults({
// 默认值(工厂函数)
age: 18,
hobbies: [] as string[]
});
// 创建模型实例(状态树节点)
const user = User.create({
id: 'u1',
name: 'Alice',
address: { city: 'Beijing', street: 'Main St', zipCode: '100000' },
createdAt: new Date().toISOString()
});