Vue源码详解之数据的响应化
# Vue源码详解之数据的响应化
vue源码构造实例的过程就记住一行:
this._init(options)
执行_init初始化函数,带参数对象
_init函数中先进行了大量的参数初始化操作 this.xxx=xxx
this._data = {}
// call init hook
this._callHook('init')
// initialize data observation and scope inheritance.
this._initState()
// setup event system and option events.
this._initEvents()
// call created hook
this._callHook('created')
// if `el` option is passed, start compilation.
if (options.el) {
this.$mount(options.el)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
基本就是触发init钩子,初始化一些状态,初始化event,然后触发created钩子,最后挂载到具体的元素上面去。_initState()方法中包含了数据的初始化操作,也就是让数据变成响应式的,让Vue能够监听到数据的变动。而this.$mount()方法则承载了绝大部分的代码量,负责模板的嵌入、编译、link、指令和watcher的生成、批处理的执行等等。
# 数据的响应化
Vue是如何解决"getter/setter无法监听属性的添加和删除"的。
对于data对象的几乎任何更改我们都能够监听到。这是MVVM的基础,基本思路就是遍历每一个属性,然后使用Object.defineProperty将这个属性设置为响应式的
遍历一个对象
function touch (obj) {
if (typeof obj === 'object')
if (Array.isArray(obj)) {
for (let i = 0,l = obj.length; i < l; i++) {
touch(obj[i])
}
} else {
let keys = Object.keys(obj)
for (let key of keys) touch(obj[key])
}
console.log(obj)
}
2
3
4
5
6
7
8
9
10
11
12
遇到普通数据属性,直接处理,遇到对象,遍历属性之后递归进去处理属性,遇到数组,递归进去处理数组元素
遍历完就到处理了,也就是Object.defineProperty部分了,对于一个对象,我们可以用这个来改写它属性的getter/setter,这样,当你改属性的值我就有办法监听到。但是对于数组就有问题了。
Vue.prototype._initData = function() {
// 初始化数据,其实一方面把data的内容代理到vm实例上
// 另一方面改造data,变成reactive
// get时 触发依赖收集,将订阅者加入dep实例的subs数组中,set时notify订阅者
var dataFn = this.$options.data
var data = this._data = dataFn ? dataFn() : {}
var props = this._props
// proxy data on instance
var keys = Object.keys(data)
var i, key
i = keys.length
while (i--) {
key = keys[i]
// 将data属性的内容代理到vm上面去, 使得vm访问指定属性即可拿到_data内的同名属性
// 实现 vm.prop === vm._data.prop,
// 这样当前vm的后代实例就能直接通过原型链查找到父代的属性
// 比如v-for指令会为数组的每一个元素创建一个scope,这个scope就继承自vm或上级数组元素的scope,
// 这样就可以在v-for的作用域中访问父级的数据
this._proxy(key)
}
// observe data
observe(data, this)
}
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
开始observe这个data:
export function observe(value, vm) {
if(!value || typeof value !== 'object') {
// 保证只有对象会进入到这个函数
return
}
var ob
if (
// 如果这个数据身上已经有了ob实例了,那observe过了,就直接返回那个ob实例
hasOwn(value, '__ob__') && value.__ob__ instanceof Observer
) {
ob = value.__ob__
} else if (
shouldConvert &&
(isArray(value) || _.isPlainObject(value)) && Object.isExtensible(value) &&
!value._isVue
) {
// 是对象(包括数组)的话就深入进去遍历属性,observe每个属性
ob = new Observer(value)
}
if (ob && vm) {
// 把vm加入到ob的vms数组当中,因为有的时候我们会对数据手动执行$set/$delete操作,
// 那么就要提示vm实例这个行为的发生(让vm代理这个新$set的数据,和更新界面)
}
return ob
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
代码的执行过程一般都是进入到那个else if里,执行new Observer(value),至于shouldConvert和后续的几个判断则是为了防止value不是单纯的对象而是Regexp或者函数之类的,或者是vm实例再或者是不可扩展的,shouldConvert则是某些特殊情况下为false
现在就进入到拿当前的data对象去new Observer(value),现在你可能会疑惑,递归遍历的过程不是应该是纯命令式的、面向过程的吗?怎么代码跑着跑着跑出来一句new一个对象了,嗯先不用管,我们先理清代码执行过程,先带着这个疑问。同时,我们注意到代码最后return了ob,结合代码,我们可以理解为如果return的是undifned,那么说明传进来的value不是对象,反之return除了一个ob,则说明这个value是对象或数组,他可以添加或删除属性
Observer构造函数:
/**
* Observer class that are attached to each observed
* object. Once attached, the observer converts target
* object's property keys into getter/setters that
* collect dependencies and dispatches updates.
*
* @param {Array|Object} value
* @constructor
*/
function Observer (value) {
this.value = value
this.dep = new Dep()
def(value, '__ob__', this) //value的__ob__属性指向这个Ob实例
if (isArray(value)) {
var augment = hasProto
? protoAugment
: copyAugment
augment(value, arrayMethods, arrayKeys)
this.observeArray(value)
} else {
// 如果是对象则使用walk遍历每个属性
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
# observe一个数组
如果遇到数组data中的数组实例增加了一些“变异”的push、pop等方法,这些方法会在数组原本的push、pop方法执行后发出消息,表明发生了改动。听起来这好像可以用继承的方式实现: 继承数组然后在这个子类的原型上附加上变异的方法。
但是你需要知道的是在es5及更低版本的js里,无法完美继承数组,主要原因是Array.call(this)时,Array根本不是像一般的构造函数那样对你传进去this进行改造,而是直接返回一个新的数组。所以一般的继承方式就没法实现了。
但是如果当前浏览器里存在__proto__这个非标准属性的话(大部分都有),那又可以有方法继承,就是创建一个继承自Array.prototype的Object: Object.create(Array.prototype),在这个继承了数组原生方法的对象上添加方法或者覆盖原有方法,然后创建一个数组,把这个数组的__proto__指向这个对象,这样这个数组的响应式的length属性又得以保留,又获得了新的方法,而且无侵入,不会改变本来的数组原型。
Vue就是基于这个思想,先判断__proto__能不能用(hasProto),如果能用,则把那个一个继承自Array.prototype的并且添加了变异方法的Object (arrayMethods),设置为当前数组的__proto__,完成改造,如果__proto__不能用,那么就只能遍历arrayMethods就一个个的把变异方法def到数组实例上面去,这种方法效率不高,所以优先使用改造__proto__的那个方法。
源码里后面那句this.observeArray非常简单,for遍历传进去的value,然后对每个元素执行observe,处理之前说的数组的元素为对象或者数组的情况。
# observe 对象
defineReactive函数
function Dep () {
this.id = uid++
this.subs = []
}
Dep.prototype.depend = function () {
Dep.target.addDep(this)
}
Dep.prototype.notify = function () {
// stablize the subscriber list first
var subs = toArray(this.subs)
for (var i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
function defineReactive (obj, key, val) {
// 生成一个新的Dep实例,这个实例会被闭包到getter和setter中
var dep = new Dep()
var property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
var getter = property && property.get
var setter = property && property.set
// 对属性的值继续执行observe,如果属性的值是一个对象,那么则又递归进去对他的属性执行defineReactive
// 保证遍历到所有层次的属性
var childOb = observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
var value = getter ? getter.call(obj) : val
// 只有在有Dep.target时才说明是Vue内部依赖收集过程触发的getter
// 那么这个时候就需要执行dep.depend(),将watcher(Dep.target的实际值)添加到dep的subs数组中
// 对于其他时候,比如dom事件回调函数中访问这个变量导致触发的getter并不需要执行依赖收集,直接返回value即可
if (Dep.target) {
dep.depend()
if (childOb) {
//如果value是对象,那就让生成的Observer实例当中的dep也收集依赖
childOb.dep.depend()
}
if (isArray(value)) {
for (var e, i = 0, l = value.length; i < l; i++) {
e = value[i]
//如果数组元素也是对象,那么他们observe过程也生成了ob实例,那么就让ob的dep也收集依赖
e && e.__ob__ && e.__ob__.dep.depend()
}
}
}
return value
},
set: function reactiveSetter (newVal) {
var value = getter ? getter.call(obj) : val
if (newVal === value) {
return
}
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
// observe这个新set的值
childOb = observe(newVal)
// 通知订阅我这个dep的watcher们:我更新了
dep.notify()
}
})
}
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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
我们来说说这个Dep,Dep类的定义极其简单,一个id,一个数组,他就是一个很基本的发布者-观察者模式的实现,作为一个发布者,他的subs属性用来存放了订阅他的观察者,也就是后面我们会说到的watcher。
defineProperty是用来将对象的属性转化为响应式的getter/setter的,defineProperty函数执行过程中新建了一个Dep,闭包在了属性的getter和setter中,因此每个属性都有一个唯一的Dep与其对应,我们暂且可以把属性和他对应的Dep理解为一体的。
Dep其实是dependence依赖的缩写,我之前一直没能理解依赖、依赖收集是什么,其实对于我们的一个模板NaN
,我们会说他的依赖有a和b,其实就是依赖了data的a和b属性,更精确的说是依赖了a属性中闭包的dep实例和b属性中闭包的那个dep实例。
详细来说:我们的这个NaN
在dom里最终会被"a+b"表达式的真实值所取代,所以存在一个求出这个“a+b”的表达式的过程,求值的过程就会自然的分别触发a和b的getter,而在getter中,我们看到执行了dep.depend(),这个函数实际上回做dep.addSub(Dep.target),即在dep的订阅者数组中存放了Dep.target,让Dep.target订阅dep。
那Dep.target是什么?他就是我们后面介绍的Watcher实例,为什么要放在Dep.target里呢?是因为getter函数并不能传参,dep可以通过闭包的形式放进去,那watcher可就不行了,watcher内部存放了a+b这个表达式,也是由watcher计算a+b的值,在计算前他会把自己放在一个公开的地方(Dep.target),然后计算a+b,从而触发表达式中所有遇到的依赖的getter,这些getter执行过程中会把Dep.target加到自己的订阅列表中。等整个表达式计算成功,Dep.target又恢复为null.这样就成功的让watcher分发到了对应的依赖的订阅者列表中,订阅到了自己的所有依赖。
我们可以看到这是极其精妙的一笔!在一个表达式的求值过程中隐式的完成依赖订阅。
上面完成的是订阅的过程,而上面setter代码里的dep.notify就负责完成数据变动时通知订阅者的功能。而且数据变化时,后文会说明只有依赖他的那些dom会精确更新,不会出现一些介绍mvvm的文章里虽然实现了订阅更新但是重新计算整个视图的情况。
于是一整个对象订阅、notify的过程就结束了。
# Observer类
getter和setter存在的缺陷:只能监听到属性的更改,不能监听到属性的删除与添加。