在Vue中,其中最最最核心的一个知识点就是数据响应式原理,数据响应式原理归结起来就包含两大部分:侦测数据变化、依赖收集,了解这两个知识点就了解到了数据响应式原理的精华。
一、侦测数据变化
能够帧听到数据变化是数据响应式原理的前提,因为数据响应式正是基于监听到数据变化后来触发一系列的更新操作。本次介绍数据响应式原理将基于Vue2.x进行,其将数据变为可被侦测数据时主要采用了Object.defineProperty()。
1.1 非数组对象
下面先举一个非数组对象的例子
const obj = {
a: {
m: {
n: 5
}
},
b: 10
};
观察上面的对象,可以发现其是存在包含关系的(即一个对象中可能包含另一个对象),那么自然会想到通过递归的方式实现,在Vue中为了保证代码较高的可读性,引入了三个模块实现该逻辑:observe、Observer、defineReactive,其调用关系如下所示:
1.1.1 observe
这个函数是帧听数据变化的入口文件,通过调用该函数一方面触发了其帧听对象数据变化的能力;另一方面定义了何时递归到最内层的终止条件。
import Observer from './Observer';
export default function (value) {
// 如果value不是对象,什么都不做(表示该递归到的是基本类型,其变化可被帧听的)
if (typeof value !== 'object') {
return;
}
// Observer实例
let ob;
// __ob__是value上的属性,其值就是对应的Observer实例(表示其已经是可帧听的状态)
if (typeof value.__ob__ !== 'undefined') {
ob = value.__ob__;
}
else {
// 是对象且该上属性还是未能够帧听状态的
ob = new Observer(value);
}
return ob;
}
1.1.2 Observer
这个函数的目的主要有两个:一个是将该实例挂载到该对象value的__ob__属性上(observe上用到了该属性,通过判断是否有该属性判断是否已经属于帧听状态);另一个是遍历该对象上的所有属性,然后将该属性均变为可帧听的(通过调用defineReactive实现)。
export default class Observer {
constructor(value) {
// 给实例添加__ob__属性
def(value, '__ob__', this, false);
// 检查是数组还是对象
if (!Array.isArray(value)) {
// 若为对象,则进行遍历,将其上的属性变为响应式的
this.walk(value);
}
}
// 对于对象上的属性进行遍历,将其变为响应式的
walk(value) {
for (let key in value) {
defineReactive(value, key);
}
}
}
1.1.3 defineReactive
这个方法主要是将Object.defineProperty封装到一个函数中,做这一步操作的原因是因为Object.defineProperty设置set属性时需要一个临时变量来存储变化前的值,通过封装利用闭包的思想引入val,这样就不需要在函数外面再设置临时变量了。
export default function defineReactive(data, key, val) {
if (arguments.length === 2) {
val = data[key];
}
// 子元素要进行observe,至此形成了递归
let childOb = observe(val);
Object.defineProperty(data, key, {
// 可枚举
enumerable: true,
// 可配置
configurable: true,
// getter
get() {
console.log(`访问${key}属性`);
return val;
},
// setter
set(newValue) {
console.log(`改变${key}的属性为${newValue}`);
if (val === newValue) {
return;
}
val = newValue;
// 当设置了新值,这个新值也要被observe
childOb = observe(newValue);
}
});
}
1.2 数组
Object.defineProperty不能直接监听数组内部的变化,那么数组内容变化应该怎么操作呢?Vue主要采用的是改装数组方法的方式(push、pop、shift、unshift、splice、sort、reverse),在保留其原有功能的前提下,将其新添加的项变为响应式的。
// array.js文件
// 得到Array的原型
const arrayPrototype = Array.prototype;
// 以Array.prototype为原型创建arrayMethods对象,并暴露
export const arrayMethods = Object.create(arrayPrototype);
// 要被改写的7个数组方法
const methodsNeedChange = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
];
methodsNeedChange.forEach(methodName => {
//备份原来的方法
const original = arrayMethods[methodName];
// 定义新的方法
def(arrayMethods, methodName, function () {
// 恢复原来的功能
const result = original.apply(this, arguments);
// 将类数组对象转换为数组
const args = [...arguments];
// 数组不会是最外层,所以其上已经添加了Observer实例
const ob = this.__ob__;
// push/unshift/splice会插入新项,需要将插入的新项变成observe的
let inserted = [];
switch (methodName) {
case 'push':
case 'unshift': {
inserted = args;
break;
}
case 'splice': {
inserted = args.slice(2);
break;
}
}
// 对于有插入项的,让新项变为响应的
if (inserted.length) {
ob.observeArray(inserted);
}
ob.dep.notify();
return result;
}, false);
});
除了改装其原有数组方法外,Observer函数中也将增加对数组的处理逻辑。
export default class Observer {
constructor(value) {
// 给实例添加__ob__属性
def(value, '__ob__', this, false);
// 检查是数组还是对象
if (Array.isArray(value)) {
// 改变数组的原型为新改装的内容
Object.setPrototypeOf(value, arrayMethods);
// 让这个数组变为observe
this.observeArray(value);
}
else {
// 若为对象,则进行遍历,将其上的属性变为响应式的
this.walk(value);
}
}
// 对于对象上的属性进行遍历,将其变为响应式的
walk(value) {
for (let key in value) {
defineReactive(value, key);
}
}
// 数组的特殊遍历
observeArray(arr) {
for (let i = 0, l = arr.length; i < l; i++) {
// 逐项进行observe
observe(arr[i]);
}
}
}
二、依赖收集
目前对象中所有的属性已经变成可帧听状态,下一步就进入了依赖收集阶段,其整个流程如下所示:
其实看了这张神图后,由于能力有限还不是很理解,经过自己的拆分,认为可以分成两个步骤去理解。
getter中(Object.defineProperty中的get属性)进行收集依赖后的状态
2. 紧接着就是触发依赖,该过程是在setter中进行,当触发依赖时所存储在Dep中的所有Watcher均会被通知并执行,通知其关联的组件更新,例如数据更新的位置是与Dep1所关联的数据,则其上的Watcher1、Watcher2、WatcherN均会被通知并执行。
说了这么多,其中最核心的内容无外乎Dep类、Watcher类、defineReactive函数中的set和get函数。
2.1 Dep类
Dep类用于管理依赖,包含依赖的添加、删除、发送消息,是一个典型的观察者模式。
export default class Dep {
constructor() {
console.log('DEP构造器');
// 数组存储自己的订阅者,这是Watcher实例
this.subs = [];
}
// 添加订阅
addSub(sub) {
this.subs.push(sub);
}
// 添加依赖
depend() {
// Dep.target指定的全局的位置
if (Dep.target) {
this.addSub(Dep.target);
}
}
// 通知更新
notify() {
const subs = this.subs.slice();
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update();
}
}
}
2.2 Watcher类
Watcher类的实例就是依赖,在其实例化阶段会作为依赖存储到Dep中,在对应的数据改变时会更新与该数据相关的Watcher实例,进行对应任务的执行,更新对应组件。
export default class Watcher {
constructor(target, expression, callback) {
console.log('Watcher构造器');
this.target = target;
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;
let 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) {
const segments = str.split('.');
return obj =>{
for (let i = 0; i < segments.length; i++) {
if (!obj) {
return;
}
obj = obj[segments[i]];
}
return obj;
};
}
2.3 defineReactive函数中的set和get函数
Object.defineProperty中的getter阶段进行收集依赖,setter阶段触发依赖。
export default function defineReactive(data, key, val) {
const dep = new Dep();
if (arguments.length === 2) {
val = data[key];
}
// 子元素要进行observe,至此形成了递归
let childOb = observe(val);
Object.defineProperty(data, key, {
// 可枚举
enumerable: true,
// 可配置
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.notify();
}
});
}
参考文献
本文是笔者看了邵山欢老师的视频后做的一次总结,邵老师讲的真心很好,爆赞。
1.如果觉得这篇文章还不错,来个分享、点赞、在看三连吧,让更多的人也看到~
2.关注公众号执鸢者,领取学习资料,定期为你推送原创深度好文
3.扫描下方添加进群,里面大佬多多,一起向他们学习
1. 图解JavaScript——代码实现(Object.create()、flat()等十四种代码原理实现不香吗?)
2. 图解JavaScript——代码实现【2】(重点是Promise、Async、发布/订阅原理实现)
6. 图解浏览器安全(同源策略、XSS、CSRF、跨域、HTTPS、安全沙箱等串成糖葫芦)
8. (2.6w字)网络知识点灵魂拷问(上)——前端面试必问
9. (2.6w字)网络知识点灵魂拷问(下)——前端面试必问
12. 理论与实践相结合彻底理解CORS
13. 三步法解析Express源码
17. 一文彻底搞懂前端监控
18. 前端的葵花宝典——架构
19. canvas从入门到猪头
21. 2021 年前端宝典【超三百篇】
22. 前端也要懂机器学习(上)
23. 前端也要懂机器学习(下)
24. 学架构助力前端起飞