Vue 2
深入理解 Vue 2 ,附源码分析
Vue 有三个与响应式相关的核心概念:
Observer
:将数据处理为响应式数据,负责对数据进行监听,数据变化时触发通知Dep
:负责依赖管理与分发,每一个响应式数据需要依赖其他哪些数据,全部记录在 Dep 中Watcher
:Observer 与 Dep 的中间人,负责订阅与触发更新,连接数据与指令。在编辑阶段,由 Watcher 来告诉 Dep 数据有何依赖,Dep 进行收集;数据变化后 Dep 需要找到相应的 Watcher,由具体的 Watcher 触发模版的更新与渲染。
发布订阅模式 + 观察者模式
# Observer
Observer 的构造器是这样定义的:
src/core/observer/index.js
class Observer {
constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
// 增加响应式数据标识
def(value, '__ob__', this)
if (Array.isArray(value)) {
// 是否支持隐式原型 __proto__
if (hasProto) {
// 将数组的隐式原型改写为一个代理对象,见下文「处理响应式数组」分析
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
// 最后再监听该数组
this.observeArray(value)
} else {
// value 是对象时,为每个元素 defineReactive
this.walk(value)
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
可以看到,Vue 对数组、对象的响应式处理有所区别。
# 处理响应式对象
对于对象,遍历对象 obj 的每一个 key,分别调用 defineReactive()
。
src/core/observer/index.js
class Observer {
// 承接上面代码, 此处省略...
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
}
function defineReactive(
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
// 此处省略...
// 如果属性值是对象 / 数组会继续递归去监听,然后返回子 observer
let childOb = !shallow && observe(val)
Object.defineProperty(obj, key, {
get: reactiveGetter() {
// TODO: dep.depend() 收集依赖
// if (childOb) childOb.dep.depend()
},
set: reactiveSetter() {
// TODO: dep.notify(),分发,触发所有依赖对应的 Watcher
}
})
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# 处理响应式数组
数组相对于对象有一些特殊,它的元素是有序的,以下标作为标识。如果我们像处理对象一样处理数组,并调用了一些可能引起元素顺序变化的 API(如 shift、unshift、splice),就有可能触发多个 setter 与 getter。例如使用 unshift 向数组的前面追加一个元素,导致原来所有元素逐一向后移位,这时候就触发所有原有元素的 getter 与 setter。可以尝试查看 codepen 这个例子 (opens new window)。
Vue 为了解决这个问题,对数组的原型方法进行了拦截与重写。
src/core/observer/array.js
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto) // 新建一继承 Array 原型的空对象
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
/**
* 重写、拦截数组的方法
*/
methodsToPatch.forEach(function (method) {
const original = arrayProto[method] // 数组原型上的方法
// 将 arrayMethods 设置为一个可对操作数组方法进行拦截的响应式对象
def(arrayMethods, method, function mutator (...args) {
const result = original.apply(this, args) // 调用原生方法获取结果
const ob = this.__ob__
let inserted // 获取新插入的值
switch (method) {
// push、unshift,所有参数均是新插值
case 'push':
case 'unshift':
inserted = args
break
// splice 的第三个参数是新值
case 'splice':
inserted = args.slice(2)
break
}
if (inserted) ob.observeArray(inserted)
ob.dep.notify() // 此时只需要触发一次更新通知
return result
})
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
对原型的拦截与重写后,调用数组方法会先经过代理对象 arrayMethods
,如果恰好是 methodsToPatch
中声明的方法,实际上调用代理对象的同名方法,也就是对数组操作一次只触发一次 dep.notify(),避免上面 codepen 例子 中 getter、setter 触发多次。
# Dep
Observer
的构造器中实例化了一个 dep
(this.dep = new Dep()
),也就是,每一个 Observer 都对应一个 dep。
export default class Dep {
static target: ?Watcher;
id: number;
subs: Array<Watcher>;
constructor () {
this.id = uid++
this.subs = []
}
addSub (sub: Watcher) {
this.subs.push(sub)
}
removeSub (sub: Watcher) {
remove(this.subs, sub)
}
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
notify () {
const subs = this.subs.slice()
if (process.env.NODE_ENV !== 'production' && !config.async) {
subs.sort((a, b) => a.id - b.id)
}
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
如前面所说,Dep
是负责依赖管理与分发的,它记录了数据之间的依赖关系,以及每条数据发生变化后需要通知哪些 Watcher 。在 defineReactive 中我用注释省略了具体代码,实际上在 getter
与 setter
中分别有调用 Dep 实例的 depend
与 notify。
。
depend
:方法用于添加 Watcher 的依赖addSub
、removeSub
:对订阅者的增删管理notify
:发布函数,数据发生变化时通知对应的 Watcher 执行相应操作
小结
Observer
在 defineReactive
后实现了:
- 获取数据时触发
getter
,getter
会调用Dep
作依赖收集,Dep
维护「数据间的依赖关系」和「数据与 Watcher 的依赖关系」 - 设置数据时触发
setter
,setter
会调用Dep
作消息分发,通知依赖该数据的数据及 Watcher 进行更新
# Watcher
Watcher 是订阅者模式中的订阅者。Vue 中,Watcher 分为数据渲染 Watcher(Render Watcher) 与选项 Watcher。其中渲染 Watcher 是组件级别的,每一个组件都有一个渲染 Watcher,而数据 Watcher 是根据开发者编写的 data
/ watch
/ computed
选项、调用 Vue 原型上的 $watch
生成的 Watcher。
疑问 🤔️
Vue 收集依赖的入口是哪?何时触发?Watcher 什么时候实例化?
我们从 Vue 生命周期的顺序理一下。
Vue Compiler 会将 <template></template>
中的内容编译为 render 函数,比如:
<template>
<div class="example">{{ msg }}</div>
</template>
2
3
编译为一个包含渲染函数的对象:
{
render: function () {
with (this) {
return _c('div',{ staticClass: "example" },[ _v(_s(msg)) ])
}
},
...
}
2
3
4
5
6
7
8
9
你可以尝试使用 Runtime + Compiler 版本的 Vue 并调用 Vue.compile() 查看结果。这是 Vue 在线编译生成的 render 函数,本地开发中 .vue 单文件被打包后也类似,对 Compiler 暂不深究
当 Vue 需要渲染时,自然触发了 msg 的 getter,此时,实例化 watcher, 并将 Dep.target
指向当前实例化的 watcher,开始依赖收集,收集完毕后重置 Dep.target。也就是说所有 watcher 都是按序实例化、按序收集依赖,因为 JS 单线程执行,Dep.target 即使在全局中是唯一的,当前 watcher 也不会影响到其他 watcher 的依赖收集过程(Dep.target 相当于是一个瞬时 watcher 指针)。
// src/core/observer/watcher.js
class Watcher {
constructor() {
// 省略若干代码...
this.value = this.lazy
? undefined
: this.get()
}
get () {
pushTarget(this)
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm)
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
} finally {
if (this.deep) {
// deep watch
traverse(value)
}
popTarget()
this.cleanupDeps()
}
return value
}
}
// src/core/observer/dep.js
export function pushTarget (_target: ?Watcher) {
if (Dep.target) targetStack.push(Dep.target)
Dep.target = _target
}
export function popTarget () {
Dep.target = targetStack.pop()
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# computed
一句话总结
computed 的本质是 lazy watcher
回过头来看 Watcher 的构造器中最后一句:
return this.value = this.lazy
? undefined
: this.get()
2
3
这个 lazy 正是专门为 computed 设计的标识。在 src/core/instance/state.js
中这行代码说明一切:
const computedWatcherOptions = { lazy: true }
前面说过,依赖收集是在触发 getter 时进行的,而 computed 的数据,只在它依赖的数据发生变化后才需要初始化,因此对于 computed 来说,实例化 watcher 时并不需要马上调用 this.get()
。
我们从生命周期的角度理一遍这里面的逻辑。
- Vue 初始化,调用
initState(vm)
src / core / instance / init.js
// ...
initInjections(vm)
initState(vm)
initProvide(vm)
// ...
2
3
4
5
initState
时初始化props
、methods
、data
,监听data
。
src / core / instance / state.js
export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
if (opts.props) initProps(vm, opts.props)
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) {
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
调用 observe 时数据时创建一个 Observer
实例,然后对 data
执行前面所述的依赖收集过程。在 initComputed
之前
已经将 data 处理为响应式数据。
initComputed
,为每一个 computed 属性调用new Watcher
并存储到_computedWatchers
对象中,此时因为 lazy 为true
,_computedWatchers
中所有 key 对应的值都是 undefined,但defineComputed
为他们分别定义了专属于 computed 属性的 getter 和 setter。getter 在进行依赖收集前将 watcher 的 lazy 置为false
,watcher.value 就能正确返回了。之后 computed 属性依赖的数据发生了变化,会通知到computedWatcher
修改它的 value。
function initComputed (vm: Component, computed: Object) {
const watchers = vm._computedWatchers = Object.create(null)
const isSSR = isServerRendering()
for (const key in computed) {
const userDef = computed[key]
const getter = typeof userDef === 'function' ? userDef : userDef.get
if (!isSSR) {
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
)
}
if (!(key in vm)) {
defineComputed(vm, key, userDef)
}
}
}
// 读者自行查阅 defineComputed,如果前面都能理解,相信很容易看明白
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
TO BE CONTINUED