简要的描述加复杂的代码表达核心思想
本文源码来自vue/src/core/observer/*.js
前言
我们实际开发中发现,在data
中定义的所有数据,后续无论是在template
中使用,还是在methods
中使用,都能随着数据的变化而变化。为了理解这其中的原理,研究源码后整理出这篇文章,欢迎大家及时指正。
第一步:数据注册监听
vue 2.x
版本使用的是 Object.defineProperty
详细API文档见Object.defineProperty
用于绑定Object
类型数据,比如定义一个person
:
let person = {
name: 'usm',
age: 12
}
现在希望person
的name
和age
发生改变时,可以触发一些操作,就可以通过 Object.defineProperty
实现:
Object.defineProperty(person, 'name', {
enumerable: true,
configurable: true,
get() {
console.log('get name's value');
},
set(val) {
console.log(`set value ${val}`);
}
});
person.name // get name's value
person.name = 'new' // set value new
其中enumerable
属性表示此属性设置为可枚举,configurable
表示此属性可被修改/删除。
至此,person
对象中的name
属性发生读/写操作时,都可以被监听到,并执行对应的代码。
回到源码,vue
中实现了一个Observer
对象,用来对vue
实例中的每个数据添加监听。
class Observer {
constructor (value) {
this.value = value
def(value, '__ob__', this)
if (Array.isArray(value)) {
this.observeArray(value)
} else {
this.walk(value)
}
}
// 遍历Object中每个属性,添加监听
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
// 监听数组类型数据
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
其中def(value, '__ob__', this)
用于给当前对象添加一个__ob__
属性,值就是当前的Observer
,目的时用来标记为已添加监听。
可以看到针对Object
类型的对象,遍历后对每个属性调用了defineReactive
方法。
// defineReactive方法部分内容
function defineReactive (obj, key, val) {
......
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = val;
...
return value
},
set: function reactiveSetter (newVal) {
const value = val
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
if (getter && !setter) return
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
...
}
})
}
get
和set
中省略部分就是数据发生改变后所做的操作。
其中set
时做了优化,判断数据是否变化,无变化或无set
函数时不做操作;当已存在set
函数时直接执行,避免重复监听。
第二步:绑定监听器
第一步中实现了数据的监听,第二步就要根据数据的变化,来通知对应的dom
进行更新。所以我们要先知道通知谁,也就是谁依赖了这个数据,由于获取数据时会触发get
函数,因此我们就在get
函数中收集依赖。
vue
中实现了一个Dep
类,用来管理当前数据的依赖,只需要对每个添加监听的数据创建一个Dep
类,再当“谁”调用了当前数据,就把“谁”添加到Dep
中,触发set
时再通知Dep
中存放的依赖们。
首先实现Dep
类:
class Dep {
constructor () {
this.id = uid++
this.subs = []
}
addSub (sub) {
this.subs.push(sub)
}
removeSub (sub) {
remove(this.subs, sub)
}
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
notify () {
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
Dep
类比较简单,定义了subs
用于存放依赖数组,收集依赖时,触发addSub
方法,派发通知时调用notify
方法,对数组中每个依赖调用update
方法。
第三步:Watcher
类
在Dep
中,我们发现每个方法都是在处理一个依赖,而这个依赖从何而来,查看源码后发现,vue
还定义了一个Watcher
类,也就是我们说的依赖。
class Watcher {
constructor (vm, cb) {
this.vm = vm
this.cb = cb
this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()
this.value = this.get()
}
get () {
pushTarget(this)
let value
const vm = this.vm
value = this.getter.call(vm, vm)
popTarget()
return value
}
addDep (dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
dep.addSub(this)
}
}
}
update () {
...
this.run()
}
depend () {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
}
其中在实例化时,调用了get
方法获取value
,这里调用了一个pushTarget
方法,和一个对应的popTarget
方法,位于源码中dep.js
文件中
function pushTarget (target) {
targetStack.push(target)
Dep.target = target
}
function popTarget () {
targetStack.pop()
Dep.target = targetStack[targetStack.length - 1]
}
我们看到调用pushTarget
方法时,将Dep
的静态属性target
设置为当前的Watcher
对象,同时推入一个target
栈中,调用popTarget
时再弹栈。
回到Observer
类的源码中
// 截取Object.defineProperty部分
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = val
if (Dep.target) {
dep.depend()
}
return value
},
set: function reactiveSetter (newVal) {
const value = val
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
if (getter && !setter) return
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
dep.notify()
}
})
得出整体流程如下,初始化Watcher
类,调用get
方法,在get
方法中,调用数据的getter
,而在getter
中,调用了dep.depend
,depend
方法中调用了Watcher
类的addDep
方法,addDep
方法最终调用addSub
方法添加依赖并注册监听。
派发通知时,触发update
方法,从而更新DOM
。
流程图如下:

总结
本文分析了vue
源码中对Object
类型数据的绑定过程。
- 针对
vue
中定义的各项数据,收集使用到该数据的依赖并注册监听 - 当数据发生变化时,通知所有依赖进行更新。
因此当data
中定义的数据发生变化时,所有用到该数据的地方都能发生变化,也就实现了vue
中数据绑定的功能。