在Vue2中,响应式系统是一切魔法的源头,无论是模板中的数据绑定,还是computed,watch的精准监听,都离不开Vue背后的响应式机制,本文将从源码角度出发,结合实例,深入剖析vue2是如何通过数据劫持(Object.defineProperty)和依赖收集实现响应式的
一.Vue2响应式系统基本原理
vue2中响应式原理主要就是利用object.defineProperty劫持对象属性的读写操作.在读取时进行依赖收集,在写入时进行派发更新
-
数据劫持: Vue2使用Object.defineProperty函数对组件的data对象属性进行劫持,当读取data中的属性时触发get,当修改data中的属性时触发set
-
依赖收集: 当模版或者计算属性等引用了data中响应式数据时,Vue将这些消费者收集起来,建立数据与消费者之间的关联
-
派发更新 当响应式数据变化时,通过dep来执行watcher的notify方法进行通知更新
1.1 数据劫持 Object.defineProperty
Vu2 使用Object.defineProperty 函数对组件data对象的属性进行劫持
局限性: Object.defineProperty只能劫持对象的属性,因此Vue2无法自动侦测到对象属性的添加或是删除,以及直接通过索引修改数组项的情况,Vue解决这个问题的方式是提供了全局方法如Vue.set和Vue.delete, 以及修改数组时应该使用的一系列方法(如push,splice等 )
data() {
return {
user: { name: "Alice" },
list: ['apple','banana']
};
},
methods: {
addAge() {
this.user.age = 25; // ❌ 视图不更新
this.$set(this.user, "age", 25); // ✅ 添加属性
//数组
this.list[1]='grape'; //视图不更新
this.$set(this.list, 1, 'grape'); // ✅ 视图更新
},
deleteName() {
delete this.user.name; // ❌ 视图不更新
this.$delete(this.user, "name"); // ✅ 删除属性
}
二. 响应式的实现关键类: Observer,Dep,Watcher,defineReactive
Vue的响应式系统中主要涉及一下三个核心类:
- Observer: 用于对对象进行递归响应式处理;
- Dep(依赖): 每个被劫持的属性都会对应一个Dep实例,用于收集依赖并在数据变更时通知更新;
- Watcher(观察者): 每个组件或计算属性,侦听器在初始化时会创建一个Watcher,用于响应数据变化
- defineReactive; 具体实现对属性的劫持,依赖收集 & 通知更新
他们之间的关系如下
data.x
|
defineReactive
|
getter <------ Dep.target = 当前 Watcher
|
Dep.depend()
|
Dep ←———— Watcher(视图更新逻辑)
|
setter
|
Dep.notify() —→ Watcher.update()
三. Observer( ): 让一个对象变成响应式
Vue会在初始化数据时调用observe( )方法,每个对象在挂载前,都先会被"响应化
function observe(value){
//如果传入的不是对象或者null,就不做任何处理,直接返回
if(typeof value !== 'object' || value===null) return ;
//如果是,对其进行观察处理,并返回一个Observer实例
return new Observe(value)
}
而Observer的核心逻辑就是对对象的每个属性进行递归处理
//Observer.js
import { defineReactive } from './defineReactive.js';
import { Dep } from './dep.js';
import { arrayMethods, augmentArray } from './arrayMethods.js';
import { observe } from './observe.js';
class Observer {
constructor(value) {
//要被观察的数据对象或数组
this.value = value;
//实例化一个依赖收集器Dep,用于在这个对象本身发生变化时通知依赖更新
this.dep = new Dep();
//标记这个对象已经被响应式处理过,防止重复处理
object.defineProperty(value, '__ob__', {
value: this,
enumerable: false,
});
//如果是数组
if (Array.isArray(value)) {
//重写数组方法实现响应式
this.augmentArray(value);
//递归监听数组元素
this.observeArray(value);
} else {
//如果是普通对象,给对象的所有袁术添加getter.setter
this.walk(value);
}
}
//对象属性递归响应式处理
walk(obj){
Object.keys(obj).forEach(key=>{
defineReactive(obj,key,obj[key]);
})
}
//重写数组原型方法
augmentArray(arr){
arr.__proto__=arrayMethods
}
//递归观察数组元素
observeAarray(items){
items.forEach(item => observe(item));
}
}
四. defineReactive
defineReactive: Vue想要追踪某个属性的读取和修改,就必须在这个属性的getter中收集依赖,在setter中通知更新,而defineReactive就是专门用来包裹一个对象的属性,让它具备这些能力
举个例子
const obj={ }
defineReactive(obj,'msg','hello')
conosle.log(obj.msg); //触发getter, 收集依赖obj.msg='world'; //触发setter,派发更新
//defineReactive.js
import { Dep } from './dep.js';
import { observe } from './observe.js';
function defineReactive(obj, key, val) {
const dep = new Dep();//每个属性都拥有自己的依赖收集器
observe(val); //如果属性值还是对象,递归处理
object.defineProperty(obj, key, {
get() {
//如果现在处于依赖收集阶段
if (Dep.target) {
dep.depend(); //依赖收集
}
return val;
},
set(newVal) {
if (newVal === val) return;
val = newVal;
//对新值进行递归响应式处理,如果它是对象或数组,并赋值给childOb
childOb = observe(newVal);
//通知依赖当前属性的Watcher(计算属性,渲染函数,侦听器等)重新执行
dep.notify();
}
});
}
五. Dep: 依赖收集与派发更新
Dep是一个依赖收集器,它的主要职责是:
(1) 存储观察者: Dep实例内部维护了一个观察者(Watcher)对象的数组,在依赖收集阶段,观察者对象会被添加到Dep实例的数字中,而在派发更新阶段,Dep类则会遍历这个数组,通知所有的观察者
(2)依赖收集: Dep类提供了addSub方法,用于在依赖收集阶段添加新的观察者,当数据的getter函数被调用时,Dep会把当前正在评估的观察者添加到自身的观察者列表中
(3)派发更新: Dep类提供了notify方法,用于在数据发生变更时通知所有的观察者,当数据的setter函数被调用时,Dep会遍历自己观察者列表,并调用它们的update方法
let uid = 0;
class Dep {
//每个Deo实例代表一个"响应式属性"的依赖容器
//每一个被defineReactive()包括的属性都会对应一个Dep实例
//用于存放所有依赖这个属性Watcher(比如组件渲染函数,计算属性,侦听器)
//用于给每个Dep实例生成唯一ID(调试用,无功能性作用)
constructor() {
this.id = uid++;
//用于存放所有依赖这个属性的Watcher
this.subs = [];
}
//把某个Watcher添加到当前Dep的依赖列表中
//这个方法一般在Watcher.addDep(dep)中调用
addSub(sub) {
this.subs.push(sub);
}
//如果Dep.target不为空(代表当前有一个Watcher正在运行),就调用它哦addDep(this)
//换句话说: 这个Dep告诉当前Watcher:"我被你用到了,你得订阅我"
//注意: 不是dep.addSub(watcher),而是watcher.addDep(this)
depend() {
if (Dep.target) {
Dep.target.addDep(this);
}
}
//数据变化时触发通知,让所有依赖的Watcher执行更新逻辑(update())
notify() {
this.subs.forEach(sub => sub.update());
}
}
- subs 数组保存所有依赖(Watcher)
- 依赖收集通过dep.depend( )和全局Dep.target配合完成;
- 数据变化时调用notify( ),触发所有watcher更新
六.观察者 Watcher
Watcher是一个关键部分,它用于在数据变化时执行更新的操作,其主要作用是在依赖收集阶段将自己添加到每个相关数据的Dependent(Dep)对象中,并在数据变化时接收通知,从而出发回调函数
主要职责:
(1) 依赖收集: Watcher在初始化时会调用自己的get方法去读取数据,这会触发数据的getter函数从而进行依赖收集,在getter函数中,当前Watcher实例会被添加到数据对一个的Deo实例中
(2)执行更新: 当数据发生变化,Dep实例调用notify方法时, Watcher实例会接收到通知,然后调用自己的update方法以触发回调
let watcherId = 0;
class Watcher {
constructor(vm, expOrFn, cb) {
//为每个watcher分配唯一id(用于优化)
this.id = watcherId++;
//当前组件实例
this.vm = vm;
this.getter = expOrFn; //表达式函数或渲染函数,比如render或某个计算属性getter
this.cb = cb;//数据更新后调用的回调,比如更新DOM
this.deps = [];//当前Watcher依赖了哪些Dep
this.get(); //初次执行getter,触发依赖收集
}
get() {
Dep.target = this; //当前正在求值的watcher(静态属性)
this.getter.call(this.vm); //执行getter,触发data的getter,从而进行依赖收集
Dep.target = null; //清空target,避免污染其他依赖收集
}
//每个响应式数据(通过defineReactive实现)在getter被触发时,会把Dep.target添加到自己的subs(订阅者列表)中,
//最终完成Dep记录Watcher,也就是Watcher鼎娱乐Dep
//添加依赖
addDep(dep) {
dep.addSub(this); //将当前watcher添加到Dep的订阅者列表中
this.deps.push(dep); //记录依赖了哪个Dep(用于后续取消依赖)
}
update() {
//这里执行视图更新逻辑,调用回调
this.cb();
}
//当某个响应式数据发生变化,它的dep.notify()方法会被调用
//notify() 会遍历所有订阅它的Watcher,执行他们的update()方法
}
- 创建watcher后会立即执行get( )进行依赖收集
- 依赖数据的getter被调用时,收集watcher;
- 数据变化时调用dep.notify( )时,watcher.update()被触发,更新视图
七. 数组响应式的实现(结合Observer和Dep)
- Observer会为数组替换原型,绑定重写后的变异方法
- 这些方法执行时调用dep.notify( ),通知依赖更新;
- 数组内部的对象元素也会被递归观察,实现深度响应式
1.arraymethods 是vue2中通过劫持数组原型方法来模拟数组响应式
// arraymethods.js
//获取原始的Array原型,用于保留原生方法引用
const arrayProto = Array.prototype;
//创建一个新对象,继承自原生数组原型
//我们将在这个对象上"重写"某些变更方法
const arrayMethods = Object.create(arrayProto);
// 需要被重写的 7 个数组变更方法
const methodsToPatch = [
'push', // 尾部插入
'pop', // 尾部删除
'shift', // 头部删除
'unshift', // 头部插入
'splice', // 插入/删除指定位置
'sort', // 排序
'reverse' // 反转
];
//遍历每一个方法,进行重写
methodsToPatch.forEach(function (method) {
//保留原始方法的引用,稍后调用
const original = arrayProto[method];
//在arrayMethods 上定义一个新的同名方法
Object.defineProperty(arrayMethods, method, {
value: function mutator(...args) {
//执行原始方法,拿到其返回值
const result = original.apply(this, args);
//拿到当前数组的Observer实例
const ob = this.__ob__;
//用于存储新插入的元素(如果有)
let inserted;
switch (method) {
case 'push':
case 'unshift':
// 新元素全部在参数中
inserted = args;
break;
case 'splice':
// splice(start, deleteCount, ...inserted)
// 插入的新元素从第三个参数开始
inserted = args.slice(2);
break;
}
//对插入的新元素做响应式处理
if (inserted) ob.observeArray(inserted);
//通知依赖更新,触发视图刷新
ob.dep.notify();
//返回原始方法的执行结果
return result;
},
enumerable: false,
writable: true,
configurable: true
});
});
在Observer中使用
if(Array.isArray(value)){
protoAugment(value,arrayMethods);
this.observeArray(value)
}
八. 工作流程总结
-
Vue初始化数据时,调用observe(data);
-
对象每个属性调用defineReactive,加getter/setter;
-
组件渲染时,会创建对应的Watcher,并执行渲染函数
-
渲染函数访问数据属性,触发getter,Dep.target收集依赖Watcher;
- 数据被修改,setter调用dep.notify( ),触发所有依赖watcher执行update( )
- watcher执行视图更新,组件自动刷新