Object.defineProperty()方法
Vue2数据响应式的核心是通过Object.defineProperty实现的,该方法可以对指定对象中的一个属性设置get和set方法,进行数据劫持。
当属性被访问时会触发set方法,当属性修改时会触发set方法,通过set方法VUE就能知道哪些数据发生变化了。
const obj={
name:'数据响应式'
}
Object.defineProperty(obj, "name", {
get() {
console.log('访问了name属性');
},
set(newVal) {
//newVal接收新值
console.log('修改了name属性');
}
})
console.log(obj.name);//触发get方法
obj.name = '响应式原理'; //触发set方法
自定义defineReactive函数
通过自定义defineReactive函数对Object.defineProperty进行封装,并进行导出
import observe from "./observe";
export default function defineReactive(obj, key, val) {
if (arguments.length === 2) {
//传的值等于对象本身的值
val = obj[key];
}
observe(val);
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
console.log('访问了' + key + '属性');
return val;
},
set(newVal) {
console.log('修改了' + key + '属性');
if (val === newVal) return;
val = newVal;
observe(val);
}
})
}
observe函数会在下文中进行讲解
递归监听对象全部属性
由于Object.defineProperty()方法一次只能劫持对象中的一个属性,所以需要使用递归遍历所有属性进行数据劫持。
首先,需要定义个Observer类,该类的功能是将需要被侦测Object对象中每一层级的属性都设置为响应式的。一个Observer类代表一个对象。
import { def } from "./utils"; //封装的defineProperty方法
import defineReactive from "./defineReactive";
//将obj对象中的属性都转成响应式发
export default class Observer {
constructor(value) {
console.log("Observer构造器: ", value);
def(value, '__ob__', this, false);
this.walk(value);
}
walk(value) {
for (let key in value) { // 遍历当前层属性设置数据劫持
defineReactive(value, key);
}
}
}
然后创建一个Observe函数,对Observer类进行实例化
import Observer from "./Observer";
export default function observe(value) {
if (typeof value !== 'object') return;
let ob;
if (typeof value.__ob__ !== 'undefined')
ob = value.__ob__;
else
ob = new Observer(value);
return ob;
}
value对象中的__ob__属性用来存储Observer实例。
整体流程是:调用Observe函数,查看对象中有没有__ob__属性,若没有则new Observer,将产生的实例添加到__ob__上,每个Observer实例都会对它的每个属性进行defindeReactive数据劫持
在defindeReactive函数中对属性进行数据劫持的同时,通过调用Observe函数对该属性再次进行上述操作,若该属性是一个对象,则继续对其子元素进行数据劫持,从而形成递归实现。
针对数组的响应式处理
对于数组,通过方法去改变其属性值时,只会被get方法获取到,但不会被set方法劫持
import observe from './responsive/observe';
const obj = {
a: 1,
b: 2,
c: {
c1: 333
},
d: [1, 2, 3, 4, 5]
}
observe(obj);
obj.b++;
obj.c.c1++;
obj.d.push(6);
可以看到通过push方法添加了一个值,只有get方法获取到了
Vue针对 这一现象对Array.prototype中的七个方法进行了重写,并在这些方法里进行判断数据是否被更改。
分别是:push、pop、shift、unshift、splice、sort、reverse
现在我们就来实现一下这七个重写的方法
实现思路是:
1. 创建一个arrayMethods对象,令对象以Array.prototype为原型
2. 将需要重写的方法写在该对象中进行重写
3. 将需要被劫持的对象的原型指向arrayMethods
这样被劫持对象就可以使用Array中被重写过以及没重写过的方法了,也不会改变Array中原有的方法
import { def } from "./utils";
const arrayPrototype = Array.prototype;
//以array.prototype为原型创建一个对象
export const arrayMethods = Object.create(arrayPrototype);
//改写的数组
const methodsNeedChange = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
methodsNeedChange.forEach((method) => {
//备份原始方法
const original = arrayMethods[method];
//重写,进行数据劫持
def(arrayMethods, method, function () {
console.log('重写成功!');
console.log(arguments);
//执行原有方法功能
original.call(this, arguments);
}, false);
})
在Observer类中对每个属性进行判断,若是数组则将其原型指向arrayMethods
import { def } from "./utils"; //封装的defineProperty方法
import defineReactive from "./defineReactive";
import { arrayMethods } from "./arrary";
//将obj对象中的属性都转成响应式
export default class Observer {
constructor(value) {
console.log("Observer构造器: ", value);
def(value, '__ob__', this, false);
if (Array.isArray(value)) {
//改变数组原型
Object.setPrototypeOf(value, arrayMethods);
this.observeArray(value);
} else
this.walk(value);
}
//对象
walk(value) {
for (let key in value) { // 遍历当前层属性设置数据劫持
defineReactive(value, key);
}
}
//数组
observeArray(value) {
for (let key of value) {
//递归遍历
observe(key);
}
}
}
这样最基本的重写功能就写好了。
需要注意的是,push、shift、splice方法会新增数组元素,所以还需要对新增元素进行数据劫持
//重写,进行数据劫持
def(arrayMethods, method, function () {
let inserted = [];
let args = [...arguments];
//执行原有方法功能
const result = original.apply(this, arguments);
//处理新增元素
switch (method) {
case 'push':
case 'shift':
inserted = args;
break;
case 'splice':
inserted = args.slice(2);
break;
}
//对新添加的函数设置数据劫持
if (inserted) {
this.__ob__.observeArray(inserted);
}
//判断数组被更改
console.log('响应');
//返回结果
return result;
}, false);
依赖收集
什么是依赖收集?
这里的依赖指的是需要用到数据的地方,比如在vue2中,用到数据的组件就是依赖
收集就是将这些依赖全部存储到一个集合中,然后当数据发送变化时,通知这个集合中的所有组件,组件最终通过diff算法进行视图更新。
我们会在getter中收集依赖,在setter中触发依赖
依赖收集主要通过Dep类和Watcher类实现
Dep类和Watcher类
- 将依赖收集的代码封装到Dep类中,该类是专门管理依赖的,每个Observer实例中都会有一个Dep实例
- Watcher类是一个中介,数据发生改变时通过Watcher中转,通知组件
深入响应式原理 — Vue.js (vuejs.org)
借助VUE官网中的流程图进行分析:
Touch指的是使用到了某个被劫持的数据,然后就会在getter中收集依赖(需要用到Dep类中的depend方法进行收集依赖),当该数据被修改时,会在setter中触发依赖(需要用到Dep类中的notify方法进行触发依赖),通过Watcher通知视图更新,然后组件进行最终的更新。
Dep类成员
成员属性:
- subs:用来存储Watcher的数组。当数据更新时,Dep会通知所有存储的Watcher进行视图更新。
- target(静态属性):指向当前Watcher实例,保证在同一时刻只有一个Watcher。
成员函数:
-
depend:添加依赖。使dep与当前watcher建立依赖关系
- notify:通知更新。当依赖属性发生变化时(即set触发),通过该函数遍历存储在subs中的所有Watcher进行更新视图。
- addSub:添加watcher至subs中。
- removeSub:删除subs中的watcher。
let uid = 0;
export default class Dep {
constructor() {
this.id = uid++;
//当前属性的依赖集合
this.subs = [];
}
addSub(sub) {
this.subs.push(sub);
}
//收集
depend() {
if (Dep.target) {
this.addSub(Dep.target);
}
}
//通知更新
notify() {
//浅拷贝
const subs = this.subs.slice();
for (let i = 0; i < subs.length; i++) {
console.log("依赖通知");
subs[i].update();
}
}
}
说明:Dep类的作用就是用来管理所有Watcher,确保数据变化时能够更新数组。通过subs数组存储Watcher,通过notify函数通知所有Watcher更新,因为Dep类是唯一的,所以其target属性确保了同一时刻全局只有一个Watcher
Dep类使用场合:
- Observer类中:每个Observer实例中都有一个有一个Dep实例
import Dep from './Dep'; export default class Observer { constructor(value) { console.log("Observer构造器: ", value); //对象及其子对象都添加一个dep实例 this.dep = new Dep(); //...... } }
- defineReactive函数中:当属性被访问时,进行依赖收集,当属性被修改时,通知组件更新
import observe from "./observe"; import Dep from './Dep'; export default function defineReactive(obj, key, val) { const dep = new Dep(); if (arguments.length === 2) { //传的是对象或空 val = obj[key]; } const childOb = observe(val); Object.defineProperty(obj, key, { enumerable: true, configurable: true, get() { console.log('访问了' + key + '属性'); //收集依赖 if (Dep.target) { dep.depend(); if(childOb) childOb.dep.depend(); } return val; }, set(newVal) { console.log('修改了' + key + '属性'); if (val === newVal) return; val = newVal; //通知依赖:当数据发生改变时,通知组件更新 dep.notify(); //当设置新值时也进行数据劫持 observe(val); } }) }
- Watcher类中:将当前Watcher实例指向Dep.target
Watcher类
成员属性:
- target:被数据劫持的对象
-
getter:深度优先方法,返回指定的最内层属性值,
-
callback:其他操作
-
value:获取的值
成员函数:
- update:进行一系列更新操作
-
get:返回最新的属性值,会在初始化实例和视图更新时被调用
import Dep from './Dep';
let uid = 0;
export default class Watcher {
constructor(target, expression, callback) {
this.id = uid++;
this.target = target;
this.getter = parsePath(expression);
this.callback = callback;
this.value = this.get();
}
//当属性首次被使用和视图更新时触发
get() {
//进入依赖收集
Dep.target = this;
const obj = this.target;
let value;
try {
value = this.getter(obj); //获取
} finally {
//当前Watcher退出依赖收集,让下个Watcher进入
obj.target = null;
}
return value; //获取最内层属性
}
update() {
this.run();
}
run() {
this.getAndInvoke(this.callback);
}
getAndInvoke(cb) {
const value = this.get();
const oldValue = this.value;
if (value !== this.value || typeof value === 'object') {
//获取最新更新
this.value = value;
cb.call(this.target, value, oldValue);
}
}
}
//深度优先 返回一个查找最内层的属性的方法
function parsePath(exp) {
const segments = exp.split('.')
return (obj) => {
for (const i of segments) {
obj = obj[i];
}
return obj;
}
}
说明:
为方便理解,可以认为每个组件实例都对应一个Watcher实例,它会监听当前组件中被数据劫持的数据,当数据发送变化时,会触发dep.notify方法,该方法会调用所有依赖了该数据的Watcher实例的update方法实现更新。
视图更新总结:
每个组件都对应一个Watcher实例,当数据被某一组件实例使用时,该实例会在getter中被dep进行依赖收集至subs数组中。当数据发生变化时,会被setter劫持,触发subs数组中的所有Watcher实例的update方法完成更新。