理解 Vue 2 的响应式原理:数据劫持与依赖收集的背后
在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;//对新值进行递归响应式处理,如果它是对象或数组,并赋值给childObchildOb = 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++;//用于存放所有依赖这个属性的Watcherthis.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执行视图更新,组件自动刷新