上篇介紹了創(chuàng)建vue實(shí)例時(shí)大概做了一些什么事情,其中有一項(xiàng)是初始化數(shù)據(jù),本篇來看一下數(shù)據(jù)觀察具體是怎么做的。
_initData就是數(shù)據(jù)觀察的起點(diǎn)了:
exports._initData = function () {
// 代理data到實(shí)例
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數(shù)據(jù)
Observer.create(data).addVm(this)
}
_proxy方法上一篇已經(jīng)說過了,就是把data數(shù)據(jù)代理到vue實(shí)例上,可以通過this.xx訪問到this.data.xx的數(shù)據(jù),關(guān)鍵是Observer。
create是Observer類的靜態(tài)方法,用來給一個數(shù)組或?qū)ο髣?chuàng)建觀察對象:
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只會對數(shù)組和純粹的對象進(jìn)行觀察,其他比如函數(shù)什么的是不會觀察的,其主要邏輯是判斷該屬性是否已經(jīng)觀察過了,是的話就返回觀察者對象,否則分別對數(shù)組和對象使用不同的標(biāo)志來實(shí)例化觀察對象。
來看Observer類:
function Observer (value, type) {
this.id = ++uid
this.value = value
this.deps = []
// 將該觀察實(shí)例設(shè)置到該對象或數(shù)組的一個屬性,方便后面檢查和使用
_.define(value, '__ob__', this)
if (type === ARRAY) {// 數(shù)組分支
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方法對對象的每個子屬性遍歷調(diào)用convert方法:
p.convert = function (key, val) {
var ob = this
// 如果該屬性的值也是個數(shù)組或?qū)ο?,那么也需要進(jìn)行觀察,observe方法最終調(diào)用的也是Object.create方法
var childOb = ob.observe(val)
// 每個屬性都會創(chuàng)建一個依賴收集實(shí)例,利用閉包來保存
var dep = new Dep()
// 該屬性的觀察實(shí)例添加到屬性值的觀察對象里
if (childOb) {
childOb.deps.push(dep)
}
Object.defineProperty(ob.value, key, {
enumerable: true,
configurable: true,
get: function () {
// 這里進(jìn)行收集依賴,Observer.target是一個全局屬性,是一個watcher實(shí)例,后續(xù)再細(xì)說,當(dāng)引用該屬性前把watcher實(shí)例賦值給這個全局屬性,此處就能引用到,然后收集到該屬性的dep實(shí)例列表里
if (Observer.target) {
Observer.target.addDep(dep)
}
return val
},
set: function (newVal) {
if (newVal === val) return
// 如果舊的值是對象或數(shù)組那么肯定也有對應(yīng)的觀察實(shí)例,所以需要從對應(yīng)的觀察實(shí)例里移除該屬性的dep
var oldChildOb = val && val.__ob__
if (oldChildOb) {
var oldDeps = oldChildOb.deps
oldDeps.splice(oldDeps.indexOf(dep), 1)
}
val = newVal
// 檢查新值,新賦的值是對象或數(shù)組又需要進(jìn)行遞歸創(chuàng)建觀察實(shí)例
var newChildOb = ob.observe(newVal)
if (newChildOb) {
newChildOb.deps.push(dep)
}
// 通知該屬性的依賴進(jìn)行更新
dep.notify()
}
})
}
接下來看一下數(shù)組的分支:
if (type === ARRAY) {
var augment = _.hasProto
? protoAugment
: copyAugment
augment(value, arrayMethods, arrayKeys)
this.observeArray(value)
}
vue修改了數(shù)組原型上的一些方法,比如:push、shift等等,原因是使用這些方法操作數(shù)組不會觸發(fā)該屬性的setter,所以vue就無法檢測到變化進(jìn)行更新,所以需要攔截這些方法進(jìn)行修改。
這里使用了兩種方法,如果瀏覽器支持__proto__,直接通過修改數(shù)組的__proto__來設(shè)置新的原型對象,如果不支持,則使用Object.defineProperty來覆蓋添加修改后的數(shù)組方法。
var arrayProto = Array.prototype
// 創(chuàng)建一個以數(shù)組原型對象為原型的新對象
var arrayMethods = Object.create(arrayProto)
;[
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
.forEach(function (method) {
// 緩存數(shù)組的原始方法
var original = arrayProto[method]
_.define(arrayMethods, method, function mutator () {
// 這里將arguments拷貝了一份,避免將該對象直接傳遞給其他函數(shù)使用,可能對性能不利
var i = arguments.length
var args = new Array(i)
while (i--) {
args[i] = arguments[i]
}
// 調(diào)用原始方法
var result = original.apply(this, args)
// 獲取該數(shù)組的觀察實(shí)例
var ob = this.__ob__
// 獲取新插入數(shù)組的值
var inserted
switch (method) {
case 'push':
inserted = args
break
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
// 如果有新插入的值,那么對它遞歸進(jìn)行觀察
if (inserted) ob.observeArray(inserted)
// 通知依賴更新
ob.notify()
return result
})
})
邏輯很簡單,就是當(dāng)調(diào)用了這些方法更新數(shù)組后觀察新插入的數(shù)據(jù),以及通知更新,這里是調(diào)用觀察對象ob的更新方法notify:
p.notify = function () {
var deps = this.deps
for (var i = 0, l = deps.length; i < l; i++) {
deps[i].notify()
}
}
通過上面的convert方法我們知道這個deps數(shù)組里收集的是該屬性值對應(yīng)的屬性的依賴收集實(shí)例dep,有點(diǎn)繞:
{
data: {
a: [1, 2, 3],
b: {
c: [4, 5, 6]
}
}
}
比如這個例子,忽略b的話,一共存在兩個Observer實(shí)例,一個是屬性data的值的,另一個是 [1, 2, 3]的, [1, 2, 3]的Observer實(shí)例的deps數(shù)組收集了a的dep,我們使用上述數(shù)組的方法更新了這個數(shù)組,會通知a的dep進(jìn)行更新通知,這很容易理解,如果我們給a設(shè)置了新值,比如:data.a = 2是會觸發(fā)a的setter的,里面會調(diào)用a的dep的notify方法,只是現(xiàn)在這個a的值變成了數(shù)組,數(shù)組變化了就相當(dāng)于a變化了,但問題是數(shù)組變化并不會觸發(fā)a的setter,所以就只能手動去調(diào)用a的dep的更新方法去通知a的依賴也去更新,但是,比如c的數(shù)組變化了,會通知c的依賴更新,但是不會向上再去通知b的依賴更新。
數(shù)組的原型方法修改完后就需要去遍歷該數(shù)組的元素進(jìn)行觀察:
p.observeArray = function (items) {
var i = items.length
while (i--) {
this.observe(items[i])
}
}
很簡單,遍歷數(shù)組調(diào)用observe方法。
到這里,就完成了對data上所有數(shù)據(jù)的觀察了,總結(jié)一下,從data對象開始,給該對象創(chuàng)建一個觀察實(shí)例,然后遍歷它的子屬性,值是數(shù)組或?qū)ο蟮脑捰謩?chuàng)建對應(yīng)的觀察實(shí)例,然后再繼續(xù)遍歷它們的子屬性,繼續(xù)遞歸,直到把每個屬性都轉(zhuǎn)換成getter和setter。
在第一次渲染的時(shí)候會引用用到的值,也就是會觸發(fā)對應(yīng)屬性的getter,引用前會把對應(yīng)的watcher賦值到Observer.target屬性,JavaScript代碼執(zhí)行是單線程的,所以同一時(shí)刻只會有一個Observer.target,所以只要某個屬性的getter里獲取到了此刻的Observer.target,那一定代表該watcher是依賴該屬性的,那么就添加到該屬性的依賴收集對象dep里,這里巧妙的使用閉包來保存每個屬性的dep實(shí)例,后續(xù)如果該屬性值變化了,那么會觸發(fā)setter,如果新賦值是對象或數(shù)組又會遞歸進(jìn)行觀察,最后再通知該屬性的所有依賴進(jìn)行更新。
上面一直都提到了這個dep,現(xiàn)在來看一下:
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()
}
}
這個類很簡單,這就是全部代碼,功能是收集訂閱者、刪除訂閱者以及遍歷調(diào)用訂閱者的update方法。
最后看一下修改數(shù)組和對象的輔助方法,如:$set、$remove等。
對于數(shù)組,直接使用索引設(shè)置數(shù)組項(xiàng)vue是不能檢測到的,所以提供了$set方法:
_.define(
arrayProto,
'$set',
function $set (index, val) {
if (index >= this.length) {
this.length = index + 1
}
return this.splice(index, 1, val)[0]
}
)
給數(shù)組的原型上添加了$set方法,調(diào)用splice方法來設(shè)置值,這個方法由于已經(jīng)被重寫過了,所以可以觸發(fā)更新,我們完全可以直接使用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()
}
}
)
直接調(diào)用convert方法就可以了,設(shè)置完得通知更新,這里分了兩種情況,如果設(shè)置的是data的根屬性,那么需要把該屬性代理到vue實(shí)例上,另外需要通知該實(shí)例及其所有子實(shí)例的watcher進(jìn)行強(qiáng)制更新。如果不是根屬性,那么調(diào)用所在對象的觀察者實(shí)例的notify方法,通知對象對應(yīng)的屬性的訂閱者進(jìn)行更新。
數(shù)據(jù)觀察到這里就結(jié)束了,但是現(xiàn)在還不知道,依賴到底是什么時(shí)候才進(jìn)行收集的,Observer.target到底什么時(shí)候才會被賦值,如果數(shù)據(jù)更新了,watcher是什么,watcher又是怎么觸發(fā)DOM更新以及怎么更新,問題還有很多,咱們下回再見。