一、Object.defineProperty()方法
彻底弄懂Vue2的数据更新原理,说白了,就是弄清楚MVVM是怎么实现的,手写相关实现代码,让相关知识不再处于“忽悠阶段”。
1.1 从MVVM模式说开去
1.2 侵入式和非侵入式
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AEdTLZgZ-1656422931911)(https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/9114e005f29743dd8a5fbe7b427cf83a~tplv-k3u1fbpfcp-zoom-in-crop-mark:1956:0:0:0.image?)]
1.3 尤雨溪发现了“上帝的钥匙”
Object.defineProperty()是“上帝的钥匙”,可以做 数据劫持/ 数据代理;
Object.defineProperty()其实是利用JavaScript引擎赋予的功能, 检测对象属性变化;
仅有“上帝的钥匙” 不够, 还需要设计一套精密的系统;
1.4 Object.defineProperty()方法———定义及更新对象属性
Object.defineProperty()方法会直接在一个对象上定义一个新属性, 或者修改一个对象的现有属性, 并返回此对象;
思考:为什么要通过Object.defineProperty()来定义,或者,为什么javascript要提供这么一个方法,直接obj.a=3不香吗?因为这样,我们就可以为该对象设置一些额外隐藏的属性。
//可延续使用上一篇【# Vue源码探秘之虚拟DOM和diff算法】,用webpack搭建好的项目环境
// src/index.js
var obj = {};
// 这样就可以给对象定义一个属性了,那么,思考:为什么要这么定义,直接obj.a=3不香吗
// 因为这样定义出来的属性有响应式???
Object.defineProperty(obj, 'a', {
value: 3
});
Object.defineProperty(obj, 'b', {
value: 5
});
console.log(obj);
console.log(obj.a, obj.b);
1.5 Object.defineProperty()方法———设置额外隐藏的属性
Object.defineProperty()方法可以设置一些额外隐藏的属性;
比如:get、set、value、writable、enumerable、configurable
var obj = {};
Object.defineProperty(obj, 'a', {
value: 3,
// 是否可写
writable: false
});
Object.defineProperty(obj, 'b', {
value: 5,
// 是否可以被枚举
enumerable: false
});
console.log(a++);//输出还是3
for(var key in obj) {
//在b中,enumerable: false时,只有a属性被输出,没有b,b会被跳过
console.log(k);
}
1.6 大名术语读的时候getter/setter,写的时候是get/set
getter get() 数据劫持,一旦某属性被访问了,就会无条件的执行其对应getter函数
setter set() 数据代理,一旦某属性的值被改变/设置了,就会无条件的执行其对应setter函数
var obj = {};
Object.defineProperty(obj, 'a', {
// getter 数据劫持,一旦a属性被访问了,就会无条件的执行getter函数
get() {
console.log('你试图访问obj的a属性');
},
// setter 数据代理,一旦a的值被改变/设置了,就会无条件的执行setter函数
set() {
console.log('你试图改变obj的a属性');
}
});
obj.a = 10;
console.log(obj.a);//此时还是undefined,因为你在set()里面什么也没没干
二、defineReactive函数
为什么要定义一个defineReactive函数???
2.1 getter/setter需要变量周转才能工作
var obj = {};
var temp;
Object.defineProperty(obj, 'a', {
// getter
get() {
console.log('你试图访问obj的a属性');
return temp;
},
// setter
set(newValue) {
console.log('你试图改变obj的a属性', newValue);
temp = newValue;
}
});
console.log(obj.a);
obj.a++;
console.log(obj.a);
2.2 使用defineReactive函数不需要设置临时变量了, 而是用闭包
function defineReactive(data, key, val) {
Object.defineProperty(data, key, {
// 可枚举
enumerable: true,
// 可以被配置, 比如可以被delete
configurable: true,
// getter
get() {
console.log('你试图访问'+ data + '的' + key + '属性');
return val;
},
// setter
set(newValue) {
console.log('你试图改变'+ data + '的' + key + '属性', newValue);
if (val === newValue) {
return;
}
val = newValue;
}
});
};
defineReactive(obj, 'a', 666)
console.log(obj.a);//666
obj.a = 10;
obj.a++;
console.log(obj.a);//11
三、递归侦测对象全部属性
// src/index.js
import observe from './observe.js';
import Watcher from './Watcher.js';
var obj = {
a: {
m: {
n: 5
}
},
b: 10,
c: {
d: {
e: {
f: 6666
}
}
},
g: [22, 33, 44, 55]
};
observe(obj);
new Watcher(obj, 'a.m.n', (val) => {
console.log('★我是watcher,我在监控a.m.n', val);
});
obj.a.m.n = 88;
obj.g.push(66);//此时可得知g被访问,但是66这个新增的值这件事没有被检测到,即此时
//的数组不是响应式的
console.log(obj);
// src/observe.js
import Observer from './Observer.js';
export default function (value) {
// 如果value不是对象,什么都不做
if (typeof value != 'object') return;
// 定义ob
var ob;
if (typeof value.__ob__ !== 'undefined') {
ob = value.__ob__;
} else {
ob = new Observer(value);
}
return ob;
}
// src/Observer.js
import { def } from './utils.js';
import defineReactive from './defineReactive.js';
import { arrayMethods } from './array.js';
import observe from './observe.js';
import Dep from './Dep.js';
export default class Observer {
constructor(value) {
// 每一个Observer的实例身上,都有一个dep
this.dep = new Dep();
// 给实例(this,一定要注意,构造函数中的this不是表示类本身,而是表示实例)添加了__ob__属性,值是这次new的实例
def(value, '__ob__', this, false);
// console.log('我是Observer构造器', value);
// 不要忘记初心,Observer类的目的是:将一个正常的object转换为每个层级的属性都是响应式(可以被侦测的)的object
// 检查它是数组还是对象
if (Array.isArray(value)) {
// 如果是数组,要非常强行的蛮干:将这个数组的原型,指向arrayMethods
Object.setPrototypeOf(value, arrayMethods);
// 让这个数组变的observe
this.observeArray(value);
} else {
this.walk(value);
}
}
// 遍历
walk(value) {
for (let k in value) {
defineReactive(value, k);
}
}
// 大白话,不仅仅给数组里的七大方法添加响应式,还要给数组里的每个值添加响应式
// 数组的特殊遍历
observeArray(arr) {
for (let i = 0, l = arr.length; i < l; i++) {
// 逐项进行observe
observe(arr[i]);
}
}
};
// src/utils.js
export const def = function (obj, key, value, enumerable) {
Object.defineProperty(obj, key, {
value,
enumerable,
writable: true,
configurable: true
});
};
// src/defineReactive.js
import observe from './observe.js';
import Dep from './Dep.js';
export default function defineReactive(data, key, val) {
const dep = new Dep();
// console.log('我是defineReactive', key);
if (arguments.length == 2) {
val = data[key];
}
// 子元素要进行observe,至此形成了递归。这个递归不是函数自己调用自己,而是多个函数、类循环调用
let childOb = observe(val);
Object.defineProperty(data, key, {
// 可枚举
enumerable: true,
// 可以被配置,比如可以被delete
configurable: true,
// getter
get() {
console.log('你试图访问' + key + '属性');
// 如果现在处于依赖收集阶段
if (Dep.target) {
dep.depend();
if (childOb) {
childOb.dep.depend();
}
}
return val;
},
// setter
set(newValue) {
console.log('你试图改变' + key + '属性', newValue);
if (val === newValue) {
return;
}
val = newValue;
// 当设置了新值,这个新值也要被observe
childOb = observe(newValue);
// 发布订阅模式,通知dep
dep.notify();
}
});
};
四、数组的响应式处理
4.1 setPrototypeOf 与 Object.create区别
设置原型的目的是进行对象间的委托,可以让一个对象获得另一个对象的一些属性或者方法。关于设置一个对象的原型,JS提供了俩种方式。一种是通过setPrototypeOf,另一种是Object.create,通过了解二者的区别可以让我们能够根据情况去选择适合的方式。
用法
若存在A和B俩个函数,让A的原型指向B
setPrototypeOf
Object.setPrototypeOf(A.prototype,B.prototype)
Create
A.prototype = Object.create(B.prototype)
区别
使用Object.create,A.prototype将会指向一个空对象,空对象的原型属性指向B的prototytpe。所以我们不能再访问A的原有prototypoe中的属性。Object.create的使用方式也凸显了直接重新赋值。
使用Object.setPrototypeOf则会将A.prototype将会指向A原有的prototype,然后这个prototype的prototype再指向B的prototytpe。所以我们优先访问的A,然后再是B。
在进行俩个原型之间的委托时使用setPrototypeOf更好,Object.create更适和直接对一个无原生原型的对象快速进行委托。
4.2 改写数组的七个方法
到目前为止,以上的代码,访问数组时可以被检测到,但是改了数组,却没有得到检测,那么,如何实现对于数组的响应式,实际上,vue底层改写了数组的七大方法以实现响应式,七大方法为:push pop shift unshift splice sort reverse
这七个方法都是Array.prototype上的方法,现在我们就是要备份这个七个方法,在此基础上再添加一些新方法、或者重写。
在Observer.js里使用Object.setPrototypeOf(value,arrayMethods)
在array.js里使用arrayMethods = Object.create(Array.prototype)
// src/array.js
import { def } from './utils.js';
// 得到Array.prototype
const arrayPrototype = Array.prototype;
// 以Array.prototype为原型创建arrayMethods对象,并暴露
export const arrayMethods = Object.create(arrayPrototype);
// 要被改写的7个数组方法,其中push,unshift,splice很特别
const methodsNeedChange = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
];
methodsNeedChange.forEach(methodName => {
// 备份原来的方法,因为push、pop等7个函数的功能不能被剥夺
const original = arrayPrototype[methodName];
// 定义新的方法
def(arrayMethods, methodName, function () {
// 恢复原来的功能
const result = original.apply(this, arguments);
// 把类数组对象变为数组
const args = [...arguments];
// 把这个数组身上的__ob__取出来,__ob__已经被添加了,为什么已经被添加了?因为数组肯定不是最高层,比如obj.g属性是数组,obj不能是数组,第一次遍历obj这个对象的第一层的时候,已经给g属性(就是这个数组)添加了__ob__属性。
const ob = this.__ob__;
// 有三种方法push\unshift\splice能够插入新项,现在要把插入的新项也要变为observe的
let inserted = [];
switch (methodName) {
case 'push':
case 'unshift':
inserted = args;
break;
case 'splice':
// splice格式是splice(下标, 数量, 插入的新项)
inserted = args.slice(2);
break;
}
// 判断有没有要插入的新项,让新项也变为响应的
if (inserted) {
// 原型上有observeArray这个方法
ob.observeArray(inserted);
}
console.log('啦啦啦');
ob.dep.notify();
return result;
}, false);
});
五、依赖收集
5.1 什么是依赖?
需要用到数据的地方, 称为依赖
Vue1.x, 细粒度依赖, 用到数据的DOM都是依赖;
Vue2.x, 中等粒度依赖, 用到数据的组件是依赖;
在getter中收集依赖, 在setter中触发依赖
5.2 Dep类和Watcher类
把依赖收集的代码封装成一个Dep类, 它专门用来管理依赖, 每个Observer的实例,成员中都有一个Dep的实例;
Watcher是一个中介, 数据发生变化时通过Watcher中转, 通知组件;
依赖就是Watcher。 只有Watcher触发的getter才会收集依赖, 哪个Watcher触发了getter, 就把哪个Watcher收集到Dep中。
Dep使用发布订阅模式, 当数据发生变化时, 会循环依赖列表, 把所有的Watcher都通知一遍。
代码实现的巧妙之处: Watcher把自己设置到全局的一个指定位置,然后读取数据, 因为读取了数据, 所以会触发这个数据的getter。 在getter中就能得到当前正在读取数据的Watcher, 并把这个Watcher收集到Dep中。
// src/Dep.js
var uid = 0;
export default class Dep {
constructor() {
console.log('我是DEP类的构造器');
this.id = uid++;
// 用数组存储自己的订阅者。subs是英语subscribes订阅者的意思。
// 这个数组里面放的是Watcher的实例
this.subs = [];
}
// 添加订阅
addSub(sub) {
this.subs.push(sub);
}
// 添加依赖
depend() {
// Dep.target就是一个我们自己指定的全局的位置,你用window.target也行,只要是全剧唯一,没有歧义就行
if (Dep.target) {
this.addSub(Dep.target);
}
}
// 通知更新
notify() {
console.log('我是notify');
// 浅克隆一份
const subs = this.subs.slice();
// 遍历
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update();
}
}
};
// src/Watcher.js
import Dep from "./Dep";
var uid = 0;
export default class Watcher {
constructor(target, expression, callback) {
console.log('我是Watcher类的构造器');
this.id = uid++;
this.target = target;
// 实现表达式a.b.c按点拆分
this.getter = parsePath(expression);
this.callback = callback;
this.value = this.get();
}
update() {
this.run();
}
get() {
// 进入依赖收集阶段。让全局的Dep.target设置为Watcher本身,那么就是进入依赖收集阶段
Dep.target = this;
const obj = this.target;
var value;
// 只要能找,就一直找
try {
value = this.getter(obj);
} finally {
Dep.target = null;
}
return value;
}
run() {
this.getAndInvoke(this.callback);
}
getAndInvoke(cb) {
const value = this.get();
if (value !== this.value || typeof value == 'object') {
const oldValue = this.value;
this.value = value;
cb.call(this.target, value, oldValue);
}
}
};
function parsePath(str) {
var segments = str.split('.');
return (obj) => {
for (let i = 0; i < segments.length; i++) {
if (!obj) return;
obj = obj[segments[i]]
}
return obj;
};
}