Vue 响应式原理
Vue 的响应式原理是 Vue.js 框架的核心之一,它使得当数据发生变化时,相关的视图会自动更新,从而实现了数据和视图的绑定。
流程图
+-----------------+ +-------------------+
| | | |
| 初始化响应式属性 | | 访问响应式属性触发getter |
| (defineReactive) | | |
| | +-------------------+
+-----------------+ |
| |
| v
| +-------------------+ +---------------+
| | | | |
+--------------------> 收集依赖(Dep.depend) |--------------> 添加Watcher |
| | | (Dep.addSub) |
+-------------------+ | |
| +---------------+
| |
| |
| |
v |
+-------------------+ |
| | |
| 修改响应式属性触发setter |<-------------------------+
| |
+-------------------+
|
|
v
+-------------------+
| |
| 通知依赖更新(Dep.notify) |
| |
+-------------------+
|
|
v
+-------------------+
| |
| Watcher更新(Watcher.update) |
| |
+-------------------+
原理
- 数据劫持(Data Observation):Vue 2利用Object.defineProperty方法对组件的data对象的每个属性进行劫持。这个方法允许Vue为每个属性设置自定义的getter和setter。当组件访问或修改数据时,通过这些getter和setter,Vue能够进行依赖收集和派发更新。
function defineReactive(obj, key, val) {
// 递归子属性
if (typeof val === 'object') {
observe(val);
}
let dep = new Dep();
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function() {
Dep.target && dep.addDep(Dep.target);
return val;
},
set: function(newVal) {
if (val === newVal) return;
val = newVal;
// 对新值进行响应式处理
observe(newVal);
// 通知依赖更新
dep.notify();
}
});
}
function observe(value) {
if (typeof value !== 'object') return;
new Observer(value);
}
class Observer {
constructor(value) {
this.value = value;
this.walk(value);
}
walk(obj) {
Object.keys(obj).forEach(key => defineReactive(obj, key, obj[key]));
}
}
- 依赖收集(Dependency Collection):在组件渲染过程中,Vue 会建立虚拟 DOM 和数据之间的联系,当数据被访问时,会收集对应的依赖(Watcher),建立依赖关系图。
- Watcher:Vue为每个组件实例创建一个或多个观察者(Watcher)实例,用来追踪该组件依赖的所有数据属性。当数据属性被访问时(即触发getter),当前的Watcher会被添加到该属性的依赖列表中(Dep实例)。
- Dep:每个响应式属性都有一个Dep实例(依赖收集器),用于收集所有依赖于该属性的Watcher。当属性变化时(即触发setter),Dep负责通知所有收集到的Watcher进行更新。
class Dep {
constructor() {
this.deps = [];
}
addDep(dep) {
this.deps.push(dep);
}
notify() {
this.deps.forEach(dep => dep.update());
}
}
// 全局的依赖目标,模拟当前正在计算的Watcher
Dep.target = null;
- 派发更新(Dispatching Update):当响应式数据发生变化时,通过setter设置的自定义逻辑会被执行。这个过程中,该数据属性对应的Dep实例会通知它收集的所有Watcher实例,告诉它们所依赖的数据已经变化,每个Watcher随后会执行其update方法,导致组件重新渲染或计算属性重新计算。
class Watcher {
constructor(obj, key, cb) {
Dep.target = this;
this.cb = cb;
this.obj = obj;
this.key = key;
this.value = obj[key]; // 触发getter进行依赖收集
Dep.target = null;
}
update() {
// 数据变化时,执行回调函数
this.value = this.obj[this.key];
this.cb(this.value);
}
}
Watcher类用于创建一个观察者对象,用于在依赖项发生变化时重新执行某个特定的函数(如组件的渲染函数)。
- 虚拟DOM(Virtual DOM):Vue的响应式系统和虚拟DOM紧密结合。当数据变化导致组件需要更新时,Vue首先会生成新的虚拟DOM树,然后与旧的虚拟DOM树进行比较(diff算法),计算出需要在真实DOM上进行的最小更改集合,并执行这些DOM更新。
- 数组的响应式处理:由于JavaScript的限制,Vue无法直接通过索引设置数组项或修改数组的长度来触发更新。Vue 2通过劫持数组的变异方法(如push、pop、splice等)来解决这个问题。当这些方法被调用时,Vue可以手动触发视图更新。
- Vue限制: 新增或删除对象属性
- Vue 2无法自动检测对象属性的添加或删除。为了解决这个问题,Vue提供了Vue.set和Vue.delete方法。
- 直接修改数组索引:由于Vue 2无法检测到通过索引直接设置数组项,推荐使用变异方法或Vue.set。
set 方法中,为什么不写成 obj[key] = newValue;
- 封装内部状态
当你使用Object.defineProperty来定义一个响应式属性时,你实际上是在创建一个封装了内部状态(即那个属性的当前值)的闭包。在这个闭包中,value变量用于存储属性的当前值。这样做的好处是,你可以在getter和setter中访问这个内部状态,而无需直接操作对象本身,这对于添加额外的逻辑和控制如何读取和修改值非常有用。
- 避免无限循环
如果你在setter中使用obj[key] = newValue;,这将会导致setter被再次调用(因为你又一次设置了对象的属性),从而形成一个无限循环。这是因为obj[key] = newValue;这个操作会再次触发这个属性的setter,然后setter又会被调用,如此循环往复。
- 依赖通知
在Vue的响应式系统中,每个响应式属性都关联着一个Dep实例,用于收集当前属性的所有依赖(即Watcher实例)。当属性的值发生变化时,我们需要通知所有依赖这个属性的Watcher进行更新。这个通知过程是在setter中完成的。因此,在setter中,我们首先更新内部状态value,然后调用dep.notify()来通知所有Watcher,而不是直接修改对象的属性。
源码示例
function Dep () {
constructor() {
this.subs = []; // Watcher 实例对象数组
}
depend() {
// 存入当前 Watcher 实例
if(Dep.target) {
this.subs.push(Dep.target);
}
}
notify() {
this.subs.forEach(watcher => {
watcher.update() // 更新
})
}
}
Dep.target = null;
function Watcher() {
constructor(obj, key, callback) {
this.data = obj;
this.key = key;
this.cb = callback;
this.value = this.get()
}
get() {
Dep.target = this; // 1.暂存 Watcher 实例对象
let value = this.data[this.key]; // 2.触发 getter
Dep.target = null;
return value;
}
update() {
const newVal = this.obj[this.key];
if(newVal !== this.value) {
this.value = newVal; // 不使用 obj[key],避免重复触发 setter
this.cb(newValue)
}
}
}
function defineReactive(obj, key) {
const dep = new Dep();
let value = obj[key];
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
dep.depend() // 3.依赖收集,存入当前的 Watcher 实例
return value;
},
set(newValue) {
if(value !== newValue) {
value = newValue // 注意此时不是 obj[key] = newValue 赋值
dep.notify()
}
}
})
}
function observe(obj) {
if(typeof obj !== 'object') return;
Object.keys(obj).forEach(key => {
defineReactive(obj, key)
})
}
const data = { price: 5, quantity: 2 };
observe(data);
// 创建Watcher实例,模拟组件渲染过程
new Watcher(data, 'price', function(newVal, oldVal) {
console.log(`价格从${oldVal}变更为${newVal}`);
});
// 改变data.price,触发Watcher回调
data.price = 20;