为什么需要依赖收集?
1、在 Vue 中,我们可能更新了不用更新视图的数据,如果没有依赖收集,则也会调用更新视图的 cb 函数,显然这是不合理的
2、Vue 页面中可能多处引用同一个 Vue 组件对象,更新响应式数据时,则应当更新多处视图,这些都涉及依赖收集
首先的订阅者 Dep 类
/**
* 依赖收集类
*/
class Dep {
constructor() {
/* 用来存放Watcher对象的数组 */
this.subs = [];
}
/* 向subs数组中插入新的Watcher对象 */
addSub(sub) {
this.subs.push(sub);
}
/* 通知subs数组中所有Watcher对象更新视图 */
/* 参数val是我为了模拟更新页面中数据设定的,本来没有,删掉即可 */
notify(val) {
this.subs.forEach(sub => sub.update(val));
}
}
在订阅者Dep对象中
- 用 addSub 方法可以在目前的 Dep 对象中增加一个 Watcher 的订阅操作;
- 用 notify 方法通知目前 Dep 对象的 subs 中的所有 Watcher 对象触发更新操作
然后的观察者Watcher
/**
* 所有依赖观察者类
*/
class Watcher {
/* id是我为了模拟更新页面数据设定的,删掉即可 */
constructor(id) {
/* 在new一个Watcher对象时将该对象赋值给Dep.target,在get中会用到 */
Dep.target = this;
this.id = id;
console.log(Dep.target);
}
/* 视图更新方法 */
/* val是我为了模拟更新页面数据设定的,删掉即可 */
update(val) {
console.log(this.id);
document.querySelector(this.id).innerHTML = val;
console.log("视图更新啦~");
}
}
/* 用来指定当前创建的Watcher将其添加至依赖收集对象中 */
Dep.target = null;
注意:响应式数据对象(data对象)中每个属性都有一个Dep对象用来收集依赖,而每个调用该属性的Vue组件都会创建一个新的Watcher对象,即都是一个新的依赖
最后的依赖收集
接下来我们更新一下上一篇博客中写到的 defineReactive 以及 Vue 的构造函数,来完成依赖收集
/**
* 将属性响应式化函数
*
* @param {object} obj 响应式化对象
* @param {} key 响应式对象属性
* @returns {} val 旧属性值
*
*/
function defineReactive(obj, key, val) {
/* 每个属性都有一个dep对象 */
const dep = new Dep();
Object.defineProperty(obj, key, {
enumerable: true,
/* 可枚举 */
configurable: true,
/* 可配置修改或删除 */
get: function reactiveGetter() {
/* 依赖采集 */
dep.addSub(Dep.target);
return val;
},
set: function reactiveSetter(newVal) {
if (newVal === val) {
return;
}
val = newVal;
/* 更新属性值时通知所有观察者更新视图 */
/* 参数是我为了模拟更新页面数据设定的,删掉即可 */
dep.notify(val);
}
})
}
/**
* Vue构造类
*/
class Vue {
constructor(options) {
this._data = options.data;
observer(this._data);
/* 新建一个Watcher观察者对象,这时候Dep.target会指向这个Watcher对象 */
/* 参数是我为了模拟更新页面数据设定的,删掉即可 */
new Watcher('.Test');
/* 在这里模拟render的过程,为了触发test属性的get函数 */
console.log('render~', this._data.test);
/* 这里页面中存在两处调用该属性的Vue组件,则创建两个Watcher */
new Watcher('.Vue');
console.log('render~', this._data.test);
}
}
完整代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Vue源码解析</title>
</head>
<body>
<div class="Test">I am test Vue.</div>
<div class="Vue">I am test Vue.</div>
</body>
<script>
/**
* 依赖收集类
*/
class Dep {
constructor() {
/* 用来存放Watcher对象的数组 */
this.subs = [];
}
/* 向subs数组中插入新的Watcher对象 */
addSub(sub) {
this.subs.push(sub);
}
/* 通知subs数组中所有Watcher对象更新视图 */
notify(val) {
this.subs.forEach(sub => sub.update(val));
}
}
/**
* 所有依赖观察者类
*/
class Watcher {
/* id是我为了模拟更新页面数据设定的,删掉即可 */
constructor(id) {
/* 在new一个Watcher对象时将该对象赋值给Dep.target,在get中会用到 */
Dep.target = this;
this.id = id;
console.log(Dep.target);
}
/* 视图更新方法 */
update(val) {
console.log(this.id);
document.querySelector(this.id).innerHTML = val;
console.log("视图更新啦~");
}
}
/**
* 将属性响应式化函数
*
* @param {object} obj 响应式化对象
* @param {} key 响应式对象属性
* @returns {} val 旧属性值
*
*/
function defineReactive(obj, key, val) {
/* 每个属性都有一个dep对象 */
const dep = new Dep();
Object.defineProperty(obj, key, {
enumerable: true,
/* 可枚举 */
configurable: true,
/* 可配置修改或删除 */
get: function reactiveGetter() {
/* 依赖采集 */
dep.addSub(Dep.target);
return val;
},
set: function reactiveSetter(newVal) {
if (newVal === val) {
return;
}
val = newVal;
/* 更新属性值时通知所有观察者更新视图 */
/* 参数是我为了模拟更新页面数据设定的,删掉即可 */
dep.notify(val);
}
})
}
/**
* 将对象所有属性响应式化函数
*
* @param {object} value 响应式化对象
*
*/
function observer(value) {
if (!value || (typeof value !== 'object')) {
return;
}
/* 遍历value中属性定制set和get */
Object.keys(value).forEach(key => defineReactive(value, key, value[key]));
}
/**
* Vue构造类
*/
class Vue {
constructor(options) {
this._data = options.data;
observer(this._data);
/* 新建一个Watcher观察者对象,这时候Dep.target会指向这个Watcher对象 */
/* 参数是我为了模拟更新页面数据设定的,删掉即可 */
new Watcher('.Test');
/* 在这里模拟render的过程,为了触发test属性的get函数 */
console.log('render~', this._data.test);
/* 这里页面中存在两处调用该属性的Vue组件,则创建两个Watcher */
new Watcher('.Vue');
console.log('render~', this._data.test);
}
}
var test = new Vue({
data: {
test: 'I am test.'
}
});
Dep.target = null;
</script>
</html>
测试结果
总结
在遍历 data 属性设定 get 时,就会设定依赖收集方法, Dep对象即是依赖收集对象 ,所有的 Watcher 则是被收集依赖的观察者
在数据变化时, set 会调用 Dep 对象的 notify 方法通知它内部所有的 Watcher 对象进行视图更新。
图来源《Vue的内部运行机制》
参考文章
代码参考《响应式系统的依赖收集追踪原理》