简介
Vue2 基于 Object.defineProperty(简称 defineProperty) 实现数据响应式,通过劫持对象属性的 getter 和 setter 来完成依赖收集与更新派发,要实现mvvm的双向绑定,就必须要实现以下几点: 1、实现一个数据监听器Observer,能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知订阅者 2、实现一个指令解析器Compile,对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数 3、实现一个Watcher,作为连接Observer和Compile的桥梁,能够订阅并收到每个属性变动的通知,执行指令绑定的相应回调函数,从而更新视图 4、mvvm入口函数,整合以上三者
代码
原理图示
class MVVM {
constructor(options) {
this.$el = options.el
this.$data = options.data
this.proxyData(this.$data)
observe(this.$data)
if (this.$el) new Compiler(this.$el, this)
}
//...
}
observe(this.$data)
数据劫持:进入vue时,对data的数据进行递归defineReactive,劫持监听所有属性,同时给每个属性new一个Dep类(依赖对象)(收集watcher,通知watcher去更新,但收集并不在这个阶段)
function defineReactive(data, key, val) {
const dep = new Dep()
let childOb = observe(val)
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
if (Dep.target) {
dep.depend()
if (childOb) {
childOb.dep.depend()
if (Array.isArray(val)) {
dependArray(val)
}
}
}
return val
},
set: function reactiveSetter(newVal) {
if (val === newVal) {
return
}
val = newVal
childOb = observe(newVal)
dep.notify()
}
})
}
new Compiler(this.$el, this)
依赖收集:解析模板指令,{{}},为每个指令new一个Watcher(观察者),并在对应data属性的dep中添加这个Watcher;发布通知:当属性值发生变化时,触发set方法,在里面Dep 会通知所有依赖它的 Watcher,Watcher 会触发 DOM 更新
对应关系
- Dep与Watcher之间是多对多的关系
- 一个data属性对应一个Dep,一个Dep对应n个Watcher(属性多次在模板中被使用时n>1:{{a}}/v-text='a')
- 一个表达式对应一个Watcher, 一个Watcher对应n个Dep(多层表达式时n>1:a.b.c)
class Watcher {
constructor(data, expression, cb) {
this.data = data
this.expression = expression
this.cb = cb
this.value = this.get()
}
get() {
Dep.target = this;
const value = parsePath(this.data, this.expression)// 这里会触发属性的getter,从而添加订阅者
Dep.target = null
return value
}
update() {
const oldValue = this.value
this.value = parsePath(this.data, this.expression)
this.cb.call(this.data, this.value, oldValue)
}
}
vue3响应式原理
初始化
- 使用
new Proxy(target, handler)代理目标对象。 - 通过
handler的get和set拦截器捕获属性的读写操作
const original = { count: 0 };
const observed = reactive(original);
// reactive()内部简化实现
function reactive(target) {
return new Proxy(target, {
get(target, key, receiver) {
// 依赖收集逻辑
track(target, key);
return Reflect.get(target, key, receiver);
},
set(target, key, value, receiver) {
const oldValue = target[key];
const result = Reflect.set(target, key, value, receiver);
// 派发更新逻辑
if (oldValue !== value) {
trigger(target, key);
}
return result;
}
});
}
依赖搜集
- 通过全局变量
activeEffect记录当前执行的副作用函数。 - 当副作用函数读取响应式属性时,触发
Proxy的get拦截器。 - 调用track方法,获取目标对象的依赖映射 -> 获取属性的依赖集合 -> 将当前副作用添加到对应属性依赖集合 -> 反向关联:让副作用知道自己依赖了哪些属性
function track(target, key) {
if (activeEffect) {
// 获取目标对象的依赖映射
let depsMap = targetMap.get(target);
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()));
}
// 获取属性的依赖集合
let dep = depsMap.get(key);
if (!dep) {
depsMap.set(key, (dep = new Set()));
}
// 将当前副作用函数添加到依赖集合中
dep.add(activeEffect);
// 反向关联:让副作用函数知道自己依赖了哪些属性
activeEffect.deps.push(dep);
}
}
activeEffect(reactiveEffect)配置项
effect.fn = fn; // 存储原始副作用函数
effect.deps = []; // 存储依赖集合
effect.active = true; // 活跃状态标记
effect.options = options; // 存储配置选项
通知所有依赖
当响应式对象的属性被修改时,触发 Proxy 的 set 拦截器, 调用trigger执行所有依赖(副作用函数,其中包括渲染函数)
function trigger(target, key) {
// 获取目标对象的依赖映射
const depsMap = targetMap.get(target);
if (!depsMap) return;
// 获取属性的依赖集合
const dep = depsMap.get(key);
if (dep) {
// 执行所有依赖(副作用函数)
dep.forEach(effect => effect());
}
}
关键数据结构:
-
targetMap(WeakMap):存储所有响应式对象的依赖关系targetMap = { target(obj1) => Map { 'key1' => Set([effect1, effect2]), 'key2' => Set([effect3]) }, target(obj2) => ... }
响应式对比缺点
- 无法检测属性添加 / 删除:Vue2 无法自动响应对象属性的添加或删除,需要使用
Vue.set()或this.$set()。 - 数组方法限制:Vue2 部分数组方法(如直接通过索引修改元素、修改数组长度)无法触发响应式更新www.cnblogs.com/karthuslori…
- 性能开销大:Vue2 对于大型对象,递归转换所有属性会导致初始渲染性能下降。
- 依赖收集效率
- Vue2:采用 “全量收集” 策略,无论属性是否被使用,初始化时都会绑定依赖,可能产生无效依赖。
- Vue3:采用 “懒收集” 策略,仅当属性被访问时才收集依赖,避免无效依赖记录,内存占用更低。WeakMap 存储:响应式对象作为 key,垃圾回收时自动释放内存。
- 更新性能
- Vue2:属性更新时需通知所有依赖的 Watcher,可能触发不必要的组件更新。
- Vue3:依赖收集更精准,更新时仅触发相关组件重新渲染,减少无效更新。