先从最简单的一段 Vue 代码开始:
<template>
<div>
{{ message }}
</div>
</template>
<script>
new Vue({
data() {
return {
message: "hello world",
};
},
});
</script>
- 这段代码很简单,最终会在页面上打印一个 hello world,它是如何实现的呢?
- 我们从源头:new Vue 的地方开始分析。
// 执行 new Vue 时会依次执行以下方法
// 1. Vue.prototype._init(option)
// 2. initState(vm)
// 3. observe(vm._data)
// 4. new Observer(data)
// 5. 调用 walk 方法,遍历 data 中的每一个属性,监听数据的变化。
function walk(obj) {
const keys = Object.keys(obj);
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i]);
}
}
// 6. 执行 defineProperty 监听数据读取和设置。
function defineReactive(obj, key, val) {
// 为每个属性创建 Dep(依赖搜集的容器,后文会讲)
const dep = new Dep();
// 绑定 get、set
Object.defineProperty(obj, key, { // vue3换成了proxy
get() {
const value = val;
// 如果有 target 标识,则进行依赖搜集
if (Dep.target) {
dep.depend();
}
return value;
},
set(newVal) {
val = newVal;
// 修改数据时,通知页面重新渲染
dep.notify();
},
});
}
数据描述符绑定完成后,我们就能得到以下的流程图:
- 图中我们可以看到,Vue 初始化时,进行了数据的 get、set 绑定,并创建了一个 Dep 对象。
- 对于数据的 get、set 绑定我们并不陌生,但是 Dep 对象什么呢?
- Dep 对象用于依赖收集,它实现了一个发布订阅模式,完成了数据 Data 和渲染视图 Watcher 的订阅,我们一起来剖析一下。
class Dep {
// 根据 ts 类型提示,我们可以得出 Dep.target 是一个 Watcher 类型。
static target: ?Watcher;
// subs 存放搜集到的 Watcher 对象集合
subs: Array<Watcher>;
constructor() {
this.subs = [];
}
addSub(sub: Watcher) {
// 搜集所有使用到这个 data 的 Watcher 对象。
this.subs.push(sub);
}
depend() {
if (Dep.target) {
// 搜集依赖,最终会调用上面的 addSub 方法
Dep.target.addDep(this);
}
}
notify() {
const subs = this.subs.slice();
for (let i = 0, l = subs.length; i < l; i++) {
// 调用对应的 Watcher,更新视图
subs[i].update();
}
}
}
根据对 Dep 的源码分析,我们得到了下面这张逻辑图:
了解 Data 和 Dep 之后,我们来继续揭开 Watcher 的面纱。
class Watcher {
constructor(vm: Component, expOrFn: string | Function) {
// 将 vm._render 方法赋值给 getter。
// 这里的 expOrFn 其实就是 vm._render,后文会讲到。
this.getter = expOrFn;
this.value = this.get();
}
get() {
// 给 Dep.target 赋值为当前 Watcher 对象
Dep.target = this;
// this.getter 其实就是 vm._render
// vm._render 用来生成虚拟 dom、执行 dom-diff、更新真实 dom。
const value = this.getter.call(this.vm, this.vm);
return value;
}
addDep(dep: Dep) {
// 将当前的 Watcher 添加到 Dep 收集池中
dep.addSub(this);
}
update() {
// 开启异步队列,批量更新 Watcher
queueWatcher(this);
}
run() {
// 和初始化一样,会调用 get 方法,更新视图
const value = this.get();
}
}
源码中我们看到,Watcher 实现了渲染方法 _render 和 Dep 的关联, 初始化 Watcher 的时候,打上 Dep.target 标识,然后调用 get 方法进行页面渲染。加上上文的 Data,目前 Data、Dep、Watcher 三者的关系如下:
我们再拉通串一下整个流程:Vue 通过 defineProperty 完成了 Data 中所有数据的代理,当数据触发 get 查询时,会将当前的 Watcher 对象加入到依赖收集池 Dep 中,当数据 Data 变化时,会触发 set 通知所有使用到这个 Data 的 Watcher 对象去 update 视图。
目前的整体流程如下:
发布订阅者模式
Dep依赖收集的实现方式就是发布订阅模式
class Publisher {
constructor() {
this._subsMap = {}
}
/* 消息订阅 */
subscribe(type, cb) {
if (this._subsMap[type]) {
if (!this._subsMap[type].includes(cb))
this._subsMap[type].push(cb)
} else this._subsMap[type] = [cb]
}
/* 消息退订 */
unsubscribe(type, cb) {
if (!this._subsMap[type] ||
!this._subsMap[type].includes(cb)) return
const idx = this._subsMap[type].indexOf(cb)
this._subsMap[type].splice(idx, 1)
}
/* 消息发布 */
notify(type, ...payload) {
if (!this._subsMap[type]) return
this._subsMap[type].forEach(cb => cb(...payload))
}
}
const adadis = new Publisher()
adadis.subscribe('运动鞋', message => console.log('152xxx' + message)) // 订阅运动鞋
adadis.subscribe('运动鞋', message => console.log('138yyy' + message))
adadis.subscribe('帆布鞋', message => console.log('139zzz' + message)) // 订阅帆布鞋
adadis.notify('运动鞋', ' 运动鞋到货了 ~') // 打电话通知买家运动鞋消息
adadis.notify('帆布鞋', ' 帆布鞋售罄了 T.T') // 打电话通知买家帆布鞋消息
// 输出: 152xxx 运动鞋到货了 ~
// 输出: 138yyy 运动鞋到货了 ~
// 输出: 139zzz 帆布鞋售罄了 T.T