最近疲于面试,但是总算告一段落了,特地把这篇文章补上~
前言
Vue的MVVM模式响应式原理之observe、Observer和defineReactive ①
Vue的MVVM模式响应式原理——数组的特殊处理之偷梁不换柱 ②
本章内容主要是衔接上面两篇的,感兴趣的可以先看看~
- 老规矩先上结论和图
结论
- new Watcher():是一个入口函数,它需要指定对象、表达式,回调;在表达式变化时触发响应的回调;
- get():在读取【响应式】数据时,收集依赖,这个依赖是一个 Watcher 的实例;
- set():在设置【响应式】数据时,通知依赖,这个通知是 Watcher实例的一个方法 notify
执行时机:
1. 在模板解析阶段,解析 {{obj.a}} 时读取 obj.a 的值触发get();
2.使用 watch API时;
- 流程图
既然所有层次的数据都已经是响应式的了。
那如何在数据发生变化时,通知所有用到该数据的组件呢
【题内话】回忆一下Vue中 watch的用法,它的原理正是本章要阐述的。
watch
类型:{ [key: string]: string | Function | Object | Array }
详细:
一个对象,键是需要观察的表达式,值是对应回调函数。
值也可以是方法名,或者包含选项的对象。
Vue 实例将会在实例化时调用 $watch(),遍历 watch 对象的每一个 property。
{
data: {
a: 1,
},
watch:{
a: function (val, oldVal) {
console.log('a被改动我就打印', val, oldVal)
},
}
/* 模拟操作 */
data.a = 2
/* 控制台 */
'a被改动我就打印', 2, 1
}
观察wacht的运用,思考一下它内部是如何实现的呢?这里先抛出问题引起思考,现在来一步一步解答。
一、Vue中运用了发布订阅模式,在读取数据时收集依赖,在更改数据时通知更新
export default function defineReactive(obj, key, value) {
let childOb = observe(value)
+ let dep = new Dep()
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
+ if (Dep.target) {
+ dep.depend()
+ }
}
console.log('访问数据触发get' + '您试图访问' + key + '属性', value)
return value
},
set(newVal) {
if (newVal === value) {
return
}
console.log('修改数据触发set' + '您试图修改' + key + '属性', newVal)
value = newVal
+ dep.notify()
}
})
}
在get和set中加入这部分的逻辑。并且发现收集依赖的前提是Dep.target存在。
二、Dep和Watcher登场——谁收集依赖,依赖是谁;
Dep依赖收集器
Dep是一个类,由Dep类的实例来收集依赖;可以看到每一个【响应式】的属性,都会对应一个dep
let uid = 0;
/**
* Dep.target 全局的一个标记,在Vue中一次只能处理一个 watcher
* 每次使用的时候 让Dep.target = watcher , 用完了再指向 null
* */
Dep.target = null;
/**
* @this.id 每个Dep实例身上都有一个 id 用于标记自身
* @this.subs 每个Dep实例身上都有一个 subs 数组用于存放 watcher
* */
function Dep() {
this.id = uid++;
this.subs = [];
}
/**
* addSub 将 watcher 添加到 dep 中;
* 由 Dep 的实例调用
* @sub Watcher的实例
* */
Dep.prototype.addSub = function (sub) {
console.log("addSub");
this.subs.push(sub);
};
/**
* 在get() 中接调用
* depend 将 dep 添加到 watcher 中;
* */
Dep.prototype.depend = function () {
console.log("在getter中收集依赖,depend");
Dep.target.addDep(this);
};
/**
* 在set() 中接调用
* 调用subs中每一个watcher的update方法。也就是Component Render Function
* */
Dep.prototype.notify = function () {
console.log("notify");
const subs = this.subs.slice();
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update();
}
};
export { Dep };
Watcher 依赖(订阅者)
Watcher是一个类,它的实例就是一个依赖。为什么又称为订阅者,是因为它会订阅某一个数据,比如{{data.a}},这就是一个订阅。
import { Dep } from "./Dep";
/**
* @uid2 是一个外部变量,用来标识每个 watcher 的id
* */
let uid2 = 0;
function Watcher(vm, exp, cb) {
this.vm = vm; // 🔴🔴🔴🔴🔴🔴🔴🔴 vm 在本文中就指 data 🔴🔴🔴🔴🔴🔴🔴🔴
this.exp = exp; // 🔴🔴🔴🔴🔴🔴🔴🔴 exp 表达式 比如 data.a 🔴🔴🔴🔴🔴🔴🔴🔴
this.cb = cb; // 🔴🔴🔴🔴🔴🔴🔴🔴 cb 回调函数 当exp的值变化时调用🔴🔴🔴🔴🔴🔴🔴🔴
this.id = uid2++;
this.depIdsAnddeps = {};
this.getter = parsePath(this.exp);
this.value = this.get();🔴🔴🔴🔴🔴🔴🔴🔴 每当new Watcher时就会 touch 🔴🔴🔴🔴🔴🔴🔴🔴
}
Watcher.prototype.update = function () {
/**
* update方法会执行视图发生变化时候的回调 cb
* 这个方法会真正的触发视图的改变
* */
let value = this.get();
let oldValue = this.value
this.cb.call(this,value,oldValue);
};
/**
* 将dep添加到watcher中
* */
Watcher.prototype.addDep = function (dep) {
if (!this.depIdsAnddeps.hasOwnProperty(dep.id)) {
console.log("addDep");
this.depIdsAnddeps[dep.id] = dep;
dep.addSub(this);
}
};
Watcher.prototype.get = function () {
let vm = this.vm;
Dep.target = this;
console.log("被触摸 Dep.target=", Dep.target);
let value = this.getter.call(this, vm);
Dep.target = null;
console.log("触摸结束 Dep.target=", Dep.target);
return value;
};
三、Dep.target使每一个watcher精确地对应dep
-
仔细看完上面的代码,可以发现,
如果不进行 new Watcher()那么就意味着没有依赖可以收集。因为Dep.target这个标识会一直为null,这就导致get时无法通过if判断; -
当new Watcher()后会触发
this.get()方法,全局变量Dep.target会先一步被赋值为当前Watcher的实例。此时if判断就会通过,那么dep.depend就会收集到该watcher -
当set()触发的时,内部会再次进行属性值的访问,因此又会触发get(),而有了这个if判断,就可以避免重复收集了。
四、来做一个假设
1、通过 touch 数据来触发get()
-
现在我们所有的数据都已经是
【响应式】的了。 -
此时Vue内部执行到了解析模板的阶段,那么它势必会解析到这样的内容。
{{data.a}}或{{data.b}}等等.. -
此时它的内部就会执行
new Watcher(vm,表达式data.a,cb回调函数..)ornew Watcher(vm,表达式data.b,cb回调函数..) -
从而去读取 data.a 和 data.b 。这样一来就会触发data.a 和 data.b 的
get()从而建立一个相互对应的关系。
- 用一张图来说明这个过程
2、通过 修改 数据来触发set()
-
{{data.a}}的dep和watcher已经建立好了对应关系 -
此时操作 data.a = 2
-
触发 set() ,那么data.a的dep就会调用notify()方法,这个方法会遍历dep中储存的所有watcher,并调用update方法。
-
绑定的回调函数就会被触发,也就是Component Render Function,此时视图就会更新。
(这块内容涉及到Compile,可以手动绑定一个以便理解)
new Watcher(obj, "data.a", function () {
console.log("Component" + "Render Function" + "视图更新的回调");
});
控制台输出 => "Component" + "Render Function" + "视图更新的回调" , 2 , 1
- 用一张图来说明这个过程
五、 现在再回过头来看开头的图
回过头来说 Vue 中的 watch 其实就是手动 new Watcher() ,并传入指定的表达式和回调。和本文示例的用法是一致的。
本文源码在我的GitHub仓库中,欢迎访问。mvvm-webpack-demo