上篇介绍了创立 vue
实例时大略做了一些什么事件,其中有一项是初始化数据,本篇来看一下数据察看具体是怎么做的。
_initData
就是数据察看的终点了:
exports._initData = function () {
// 代理 data 到实例
var data = this._data
var keys = Object.keys(data)
var i = keys.length
var key
while (i--) {key = keys[i]
if (!_.isReserved(key)) {this._proxy(key)
}
}
// 察看 data 数据
Observer.create(data).addVm(this)
}
_proxy
办法上一篇曾经说过了,就是把 data
数据代理到 vue
实例上,能够通过 this.xx
拜访到 this.data.xx
的数据,要害是Observer
。
create
是 Observer
类的静态方法,用来给一个数组或对象创立察看对象:
Observer.create = function (value) {
if (
value &&
value.hasOwnProperty('__ob__') &&
value.__ob__ instanceof Observer
) {return value.__ob__} else if (_.isArray(value)) {return new Observer(value, ARRAY)
} else if (_.isPlainObject(value) &&
!value._isVue
) {return new Observer(value, OBJECT)
}
}
从这里能够晓得 vue
只会对数组和纯正的对象进行察看,其余比方函数什么的是不会察看的,其次要逻辑是判断该属性是否曾经察看过了,是的话就返回观察者对象,否则别离对数组和对象应用不同的标记来实例化察看对象。
来看 Observer
类:
function Observer (value, type) {
this.id = ++uid
this.value = value
this.deps = []
// 将该察看实例设置到该对象或数组的一个属性,不便前面检查和应用
_.define(value, '__ob__', this)
if (type === ARRAY) {// 数组分支
var augment = _.hasProto
? protoAugment
: copyAugment
augment(value, arrayMethods, arrayKeys)
this.observeArray(value)
} else if (type === OBJECT) {// 对象分支
this.walk(value)
}
}
初始化了一些属性,先看一下比较简单的对象分支:
p.walk = function (obj) {var keys = Object.keys(obj)
var i = keys.length
var key, prefix
while (i--) {key = keys[i]
prefix = key.charCodeAt(0)
if (prefix !== 0x24 && prefix !== 0x5F) { // 跳过 $ or _结尾的公有属性
this.convert(key, obj[key])
}
}
}
walk
办法对对象的每个子属性遍历调用 convert
办法:
p.convert = function (key, val) {
var ob = this
// 如果该属性的值也是个数组或对象,那么也须要进行察看,observe 办法最终调用的也是 Object.create 办法
var childOb = ob.observe(val)
// 每个属性都会创立一个依赖收集实例,利用闭包来保留
var dep = new Dep()
// 该属性的察看实例增加到属性值的察看对象里
if (childOb) {childOb.deps.push(dep)
}
Object.defineProperty(ob.value, key, {
enumerable: true,
configurable: true,
get: function () {
// 这里进行收集依赖,Observer.target 是一个全局属性,是一个 watcher 实例,后续再细说,当援用该属性前把 watcher 实例赋值给这个全局属性,此处就能援用到,而后收集到该属性的 dep 实例列表里
if (Observer.target) {Observer.target.addDep(dep)
}
return val
},
set: function (newVal) {if (newVal === val) return
// 如果旧的值是对象或数组那么必定也有对应的察看实例,所以须要从对应的察看实例里移除该属性的 dep
var oldChildOb = val && val.__ob__
if (oldChildOb) {
var oldDeps = oldChildOb.deps
oldDeps.splice(oldDeps.indexOf(dep), 1)
}
val = newVal
// 查看新值,新赋的值是对象或数组又须要进行递归创立察看实例
var newChildOb = ob.observe(newVal)
if (newChildOb) {newChildOb.deps.push(dep)
}
// 告诉该属性的依赖进行更新
dep.notify()}
})
}
接下来看一下数组的分支:
if (type === ARRAY) {
var augment = _.hasProto
? protoAugment
: copyAugment
augment(value, arrayMethods, arrayKeys)
this.observeArray(value)
}
vue
批改了数组原型上的一些办法,比方:push
、shift
等等,起因是应用这些办法操作数组不会触发该属性的 setter
,所以vue
就无奈检测到变动进行更新,所以须要拦挡这些办法进行批改。
这里应用了两种办法,如果浏览器反对 __proto__
,间接通过批改数组的__proto__
来设置新的原型对象,如果不反对,则应用 Object.defineProperty
来笼罩增加批改后的数组办法。
var arrayProto = Array.prototype
// 创立一个以数组原型对象为原型的新对象
var arrayMethods = Object.create(arrayProto)
;[
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
.forEach(function (method) {
// 缓存数组的原始办法
var original = arrayProto[method]
_.define(arrayMethods, method, function mutator () {
// 这里将 arguments 拷贝了一份,防止将该对象间接传递给其余函数应用,可能对性能不利
var i = arguments.length
var args = new Array(i)
while (i--) {args[i] = arguments[i]
}
// 调用原始办法
var result = original.apply(this, args)
// 获取该数组的察看实例
var ob = this.__ob__
// 获取新插入数组的值
var inserted
switch (method) {
case 'push':
inserted = args
break
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
// 如果有新插入的值,那么对它递归进行察看
if (inserted) ob.observeArray(inserted)
// 告诉依赖更新
ob.notify()
return result
})
})
逻辑很简略,就是当调用了这些办法更新数组后察看新插入的数据,以及告诉更新,这里是调用察看对象 ob
的更新办法notify
:
p.notify = function () {
var deps = this.deps
for (var i = 0, l = deps.length; i < l; i++) {deps[i].notify()}
}
通过下面的 convert
办法咱们晓得这个 deps
数组里收集的是该属性值对应的属性的依赖收集实例dep
,有点绕:
{
data: {a: [1, 2, 3],
b: {c: [4, 5, 6]
}
}
}
比方这个例子,疏忽 b
的话,一共存在两个 Observer
实例,一个是属性 data
的值的,另一个是 [1, 2, 3]
的,[1, 2, 3]
的 Observer
实例的 deps
数组收集了 a
的dep
,咱们应用上述数组的办法更新了这个数组,会告诉 a
的dep
进行更新告诉,这很容易了解,如果咱们给 a
设置了新值,比方:data.a = 2
是会触发 a
的setter
的,外面会调用 a
的dep
的 notify
办法,只是当初这个 a
的值变成了数组,数组变动了就相当于 a
变动了,但问题是数组变动并不会触发 a
的setter
,所以就只能手动去调用 a
的dep
的更新办法去告诉 a
的依赖也去更新,然而,比方 c
的数组变动了,会告诉 c
的依赖更新,然而不会向上再去告诉 b
的依赖更新。
数组的原型办法批改完后就须要去遍历该数组的元素进行察看:
p.observeArray = function (items) {
var i = items.length
while (i--) {this.observe(items[i])
}
}
很简略,遍历数组调用 observe
办法。
到这里,就实现了对 data
上所有数据的察看了,总结一下,从 data
对象开始,给该对象创立一个察看实例,而后遍历它的子属性,值是数组或对象的话又创立对应的察看实例,而后再持续遍历它们的子属性,持续递归,直到把每个属性都转换成 getter
和setter
。
在第一次渲染的时候会援用用到的值,也就是会触发对应属性的 getter
,援用前会把对应的watcher
赋值到 Observer.target
属性,JavaScript
代码执行是单线程的,所以同一时刻只会有一个 Observer.target
,所以只有某个属性的getter
里获取到了此刻的 Observer.target
,那肯定代表该watcher
是依赖该属性的,那么就增加到该属性的依赖收集对象 dep
里,这里奇妙的应用闭包来保留每个属性的 dep
实例,后续如果该属性值变动了,那么会触发setter
,如果新赋值是对象或数组又会递归进行察看,最初再告诉该属性的所有依赖进行更新。
下面始终都提到了这个dep
,当初来看一下:
function Dep () {
this.id = ++uid
this.subs = []}
var p = Dep.prototype
p.addSub = function (sub) {this.subs.push(sub)
}
p.removeSub = function (sub) {if (this.subs.length) {var i = this.subs.indexOf(sub)
if (i > -1) this.subs.splice(i, 1)
}
}
p.notify = function () {var subs = _.toArray(this.subs)
for (var i = 0, l = subs.length; i < l; i++) {subs[i].update()}
}
这个类很简略,这就是全副代码,性能是收集订阅者、删除订阅者以及遍历调用订阅者的 update
办法。
最初看一下批改数组和对象的辅助办法,如:$set
、$remove
等。
对于数组,间接应用索引设置数组项 vue
是不能检测到的,所以提供了 $set
办法:
_.define(
arrayProto,
'$set',
function $set (index, val) {if (index >= this.length) {this.length = index + 1}
return this.splice(index, 1, val)[0]
}
)
给数组的原型上增加了 $set
办法,调用 splice
办法来设置值,这个办法因为曾经被重写过了,所以能够触发更新,咱们齐全能够间接应用 splice
办法。
对于对象,在 data
初始化后在增加新属性也是不能检测到的,在 0.11
版本提供各了 $add
办法:
_.define(
objProto,
'$add',
function $add (key, val) {if (this.hasOwnProperty(key)) return
var ob = this.__ob__
if (!ob || _.isReserved(key)) {this[key] = val
return
}
ob.convert(key, val)
if (ob.vms) {
var i = ob.vms.length
while (i--) {var vm = ob.vms[i]
vm._proxy(key)
vm._digest()}
} else {ob.notify()
}
}
)
间接调用 convert
办法就能够了,设置完得告诉更新,这里分了两种状况,如果设置的是 data
的根属性,那么须要把该属性代理到 vue
实例上,另外须要告诉该实例及其所有子实例的 watcher
进行强制更新。如果不是根属性,那么调用所在对象的观察者实例的 notify
办法,告诉对象对应的属性的订阅者进行更新。
数据察看到这里就完结了,然而当初还不晓得,依赖到底是什么时候才进行收集的,Observer.target
到底什么时候才会被赋值,如果数据更新了,watcher
是什么,watcher
又是怎么触发 DOM
更新以及怎么更新,问题还有很多,咱们下回再见。