vue2响应式系统是如何实现的(手写)
引言
喜欢请点赞,支持点在看。 关注牛马圈,干货不间断。
忆往昔
回头看,已经使用vue2多年,虽然也掌握了其他几种前端框架,但对vue2是真爱。
核心概念
-
Object.defineProperty
Vue 2的响应式系统核心是使用了Object.defineProperty
来劫持(或观察)每个组件的data
对象的属性。Object.defineProperty
可以给对象的属性添加getter和setter,从而实现对数据的读取和写入进行监听。 -
依赖收集(Dependency Collection) 当组件进行渲染时,Vue会记录所有被访问的属性,这些属性被称为“依赖”。每个属性都有一个或多个“观察者”(Watcher),当属性值发生变化时,Vue会通知所有依赖于该属性的观察者。
-
观察者(Watcher) 观察者是Vue响应式系统中用于更新视图的机制。当依赖的属性值发生变化时,观察者会被通知,并执行一个函数来更新DOM。
实现步骤
-
步骤一:数据劫持(Data Hijacking)
function defineReactive(obj, key, val) {
const dep = new Dep();
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
if (Dep.target) {
dep.depend();
}
return val;
},
set: function reactiveSetter(newVal) {
if (val === newVal) return;
val = newVal;
dep.notify();
}
});
}
-
步骤二:依赖收集
class Dep {
constructor() {
this.subs = [];
}
addSub(sub) {
this.subs.push(sub);
}
removeSub(sub) {
remove(this.subs, sub);
}
depend() {
if (Dep.target) {
Dep.target.addDep(this);
}
}
notify() {
const subs = this.subs.slice();
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update();
}
}
}
Dep.target = null;
-
步骤三:观察者(Watcher)
class Watcher {
constructor(vm, expOrFn, cb) {
this.vm = vm;
this.cb = cb;
this.expOrFn = expOrFn;
this.depIds = new Set();
this.value = this.get();
}
get() {
Dep.target = this;
const value = this.vm._data[this.expOrFn];
Dep.target = null;
return value;
}
addDep(dep) {
const id = dep.id;
if (!this.depIds.has(id)) {
this.depIds.add(id);
dep.addSub(this);
}
}
update() {
const oldValue = this.value;
this.value = this.get();
this.cb.call(this.vm, this.value, oldValue);
}
}
-
步骤四:观察整个数据对象
function observe(value) {
if (!value || typeof value !== 'object') {
return;
}
Object.keys(value).forEach(key => {
defineReactive(value, key, value[key]);
});
}
-
步骤五:将数据对象转换为响应式
function Vue(options) {
this._data = options.data;
observe(this._data);
}
局限性
-
对象属性的响应性
-
新增属性:Vue不能检测对象属性的添加或删除。必须使用 Vue.set
方法来确保新属性是响应式的,或者通过替换整个对象来触发更新。 -
删除属性:同样地,使用 Vue.delete
来删除属性以确保视图更新。
-
数组的响应性
-
索引赋值:Vue不能检测通过索引直接设置数组项的操作,例如 vm.items[indexOfItem] = newValue
。 -
长度修改:Vue不能检测通过修改数组长度的操作,例如 vm.items.length = newLength
。 -
解决方法:使用 Vue.set
来更新数组项,或者使用数组的splice
方法。
-
对象的响应性
-
直接替换:直接替换一个对象或数组,例如 vm.myObject = newObject
,Vue将无法保持原有对象属性的响应性。必须用新对象与旧对象的属性逐一赋值来保持响应性。
-
嵌套对象/数组
-
深层响应性:Vue的响应式系统可以自动侦测嵌套对象或数组的变化,但如果数据结构非常深,性能可能会受到影响。
-
异步更新队列
-
DOM更新时机:Vue在观察到数据变化时并不会立即更新DOM,而是开启一个队列,并缓冲在同一个事件循环中发生的所有数据改变。如果同一个watcher被多次触发,只会被推入到队列中一次。这种在异步队列中批量更新的行为可能会导致我们无法立即看到数据变化后的结果。
-
Object.defineProperty
的限制
-
仅限属性:Vue的响应式系统基于 Object.defineProperty
,这意味着它只能侦测属性的变化,而不是对象或数组的变化。 -
**无法侦测到ES6的 Map
和Set
**:Object.defineProperty
无法侦测到Map
和Set
这类数据结构的变化。
-
性能开销
-
大量数据:对于拥有大量数据的对象,每个属性都通过 Object.defineProperty
进行代理,可能会导致性能问题。
-
不可响应的数据类型
-
原始类型:基本数据类型(如字符串、数字、布尔值)是响应式的,但它们是不可变的,这意味着你不能通过直接修改它们来触发更新。 -
冻结对象:被 Object.freeze()
冻结的对象无法被设置为响应式。
-
计算属性和侦听器
-
计算属性:计算属性依赖于其响应式依赖项。如果依赖项未发生变化,计算属性不会重新计算。 -
侦听器:侦听器可以观察数据变化,但过度使用可能导致性能问题。
为了解决这些限制,Vue 3引入了基于Proxy的响应式系统,它解决了上述许多问题,例如对属性的动态添加和删除、更好的性能以及原生支持Map
和Set
等。
vue3虽好,但唯独钟情于vue2,我是不是有点守旧
本文由 mdnice 多平台发布