我的博客地址vue
像Vue官網上面說的,vue是經過Object.defineProperty
來偵測對象屬性值的變化。數組
function defineReactive (obj, key, val) {
let dep = new Dep()
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get () {
return val
},
set (newVal) {
if (val === newVal) return
val = newVal
}
})
}
複製代碼
函數 defineReactive 是對 Object.defineProperty 的封裝,做用是定義一個響應式的數據。瀏覽器
不過若是隻是這樣是沒有什麼用的,真正有用的是收集依賴。在getter中收集依賴,在setter觸發依賴。緩存
Dep (收集依賴)閉包
// 還有幾個方法沒寫,好比怎麼移除依賴。
class Dep {
constructor () {
// 依賴數組
this.subs = []
}
addSub (sub) {
this.subs.push(sub)
}
depend (target) {
if (Dep.target) {
// 這時的Dep.target是Watcher實例
Dep.target.addDep(this)
}
}
notity () {
this.subs.forEach(val => {
val.update()
})
}
Dep.target = null
}
複製代碼
Watcher (依賴)app
// 原本在Watcher中也要記錄Dep,可是偷懶沒寫了,記錄了Dep後能夠通知收集了Watcher的Dep移除依賴。
class Watcher {
constructor (vm, expOrFn, cb) {
// vm: vue實例
// expOrFn: 字符串或函數
// cb: callback回調函數
this.vm = vm
this.cb = cb
// 執行this.getter就能夠讀取expOrFn的數據,就會收集依賴
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
// parsePath是讀取字符串keypath的函數,具體的能夠去瀏覽Vue的源碼
this.getter = parsePath(expOrFn)
}
this.value = this.get()
}
get () {
Dep.target = this
// 在這裏執行this.getter
let value = this.getter(this.vm, this.vm)
Dep.target = null
return value
}
addDep (dep) {
dep.addSub(this)
}
// 更新依賴
update () {
const oldValue = this.value
this.value = this.get()
this.cb.call(this.vm, this.value, oldValue)
}
}
複製代碼
接下來再改一下剛開始定義的 defineReactive 函數函數
function defineReactive (obj, key, val) {
let dep = new Dep() // 閉包
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get () {
// 觸發getter時,收集依賴
dep.addDep()
return val
},
set (newVal) {
if (val === newVal) return
val = newVal
// 觸發setter時,觸發Dep的notify,便利依賴
dep.notity()
}
})
}
複製代碼
這個時候已經能夠偵測數據的單獨一個屬性,最後再封裝一下:ui
class Observer {
constructor (value) {
this.value = value
// 偵測數據的變化和偵測對象的變化是有區別的
if (!Array.isArray(value)) {
this.walk(value)
}
}
walk (value) {
const keys = Object.keys(value)
keys.forEach(key => {
this.defineReactive(value, key, value[key])
})
}
}
複製代碼
最後總結一下:this
實例化 Watcher 時經過 get 方法把 Dep.target 賦值爲當前的 Wathcer 實例,並把 Watcher 實例添加在 Dep 中,當設置數據時,觸發 defineReactive 的 set 運行 Dep.notify() 遍歷 Dep 中收集的依賴 Watcher 實例,而後觸發 Watcher 實例的 update 方法。spa
Object 能夠經過 getter/setter 來偵測變化,可是數組是經過方法來變化,好比 push 。這樣就不能和對象同樣,只能經過攔截器來實現偵測變化。
定義一個攔截器來覆蓋 Array.prototype,每當使用數組原型上面的方法操做數組的時候,實際上執行的是攔截器上面的方法,而後再攔截器裏面使用 Array 的原型方法。
const arrayProto = Array.prototype
const arrayMethods = Object.create(arrayProto)
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
methodsToPatch.forEach(function (method) {
// 緩存原始方法
const original = arrayProto[method]
Object.defineProperty(arrayMethods, method, {
enumerable: false,
configurable: true,
writable: true,
value: function mutator (...args) {
return original.apply(this, args)
}
})
複製代碼
而後就要覆蓋 Array 的原型:
// 看是否支持__proto__, 若是不支持__proto__,則直接把攔截器的方法直接掛載到value上。
const hasProto = "__proto__" in {}
const arrayKeys = Object.getOwnPropertyNames(arrayMethods)
class Observer {
constructor (value) {
this.value = value
if (!Array.isArray(value)) {
this.walk(value)
} else {
const augment = hasProto ? protoAugment : copyAugment
augment(value, arrayMethods, arraykeys)
}
}
walk (value) {
const keys = Object.keys(value)
keys.forEach(key => {
this.defineReactive(value, key, value[key])
})
}
}
function protoAugment (target, src: Object) {
target.__proto__ = src
}
function copyAugment (target: Object, src: Object, keys: Array<string>) {
for (let i = 0, l = keys.length; i < l; i++) {
const key = keys[i]
def(target, key, src[key])
}
}
複製代碼
Array 也是在 getter 中收集依賴,不過依賴存的地方有了變化。Vue.js 把依賴存在 Observer 中:
class Observer {
constructor (value) {
this.value = value
this.dep = new Dep // 新增Dep
if (!Array.isArray(value)) {
this.walk(value)
} else {
const augment = hasProto ? protoAugment : copyAugment
augment(value, arrayMethods, arraykeys)
}
}
walk (value) {
const keys = Object.keys(value)
keys.forEach(key => {
this.defineReactive(value, key, value[key])
})
}
}
複製代碼
至於爲何把 Dep 存在 Observer 是由於必須在 getter 和 攔截器中都能訪問到。
function defineReactive (data, key, val) {
let childOb = observer(val) // 新增
let dep = new Dep()
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get () {
dep.addDep()
if (childOb) {
// 在這裏收集數組依賴
childOb.dep.depend()
}
return val
},
set (newVal) {
if (val === newVal) return
val = newVal
dep.notity()
}
})
}
// 若是value已是響應式數據,即有了__ob__屬性,則直接返回已經建立的Observer實例
// 若是不是響應式數據,則建立一個Observer實例
function observer (value, asRootData) {
if (!isObject(value)) {
return
}
let ob
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observe) {
ob = value.__ob__
} else {
ob = new Observer(value)
}
return ob
}
複製代碼
由於攔截器是對 Array 原型的封裝,因此能夠在攔截器中訪問到this(當前正在被操做的數組),
dep保存在 Observer 實例中,因此須要在this上訪問到 Observer 實例:
function def (obj, key, val, enumerable) {
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable,
writable: true,
configerable: true
})
}
class Observer {
constructor (value) {
this.value = value
this.dep = new Dep
// 把value上新增一個不可枚舉的屬性__ob__,值爲當前的Observer實例
// 這樣就能夠經過數組的__ob__屬性拿到Observer實例,而後就能夠拿到Observer的depp
// __ob__不止是爲了拿到Observer實例,還能夠標記是不是響應式數據
def(value, '__ob__', this) // 新增
if (!Array.isArray(value)) {
this.walk(value)
} else {
const augment = hasProto ? protoAugment : copyAugment
augment(value, arrayMethods, arraykeys)
}
}
...
}
複製代碼
在攔截器中:
methodsToPatch.forEach(function (method) {
const original = arrayProto[method]
def(arrayMethods, method, function mutator (...args) {
const result = original.apply(this, args)
const ob = this.__ob__ // 新增
ob.dep.notify() // 新增 向依賴發送信息
return resullt
})
})
複製代碼
到這裏還只是偵測了數組的變化,還要偵測數組元素的變化:
class Observer {
constructor (value) {
this.value = value
this.dep = new Dep
def(value, '__ob__', this)
if (!Array.isArray(value)) {
this.walk(value)
} else {
const augment = hasProto ? protoAugment : copyAugment
augment(value, arrayMethods, arraykeys)
// 偵測數組中的每一項
this.observeArray(value) // 新增
}
}
observeArray (items) {
items.forEach(item => {
observe(item)
})
}
...
}
複製代碼
而後還要偵測數組中的新增元素的變化:
methodsToPatch.forEach(function (method) {
const original = arrayProto[method]
def(arrayMethods, method, function mutator (...args) {
const result = original.apply(this, args)
const ob = this.__ob__
// 新增開始
let inserted
switch (method) {
case 'push'
case 'unshift'
inserted = args
breaak
case 'splice'
inserted = args.slice(2)
break
}
if (inserted) ob.observeArray(inserted)
// 新增結束
ob.dep.notify()
return resullt
})
})
複製代碼
總結一下:
Array 追蹤變化的方式和 Object 不同,是經過攔截器去覆蓋數組原型的方法來追蹤變化。
爲了避免污染全局的 Array.prototype ,因此只針對那些須要偵測變化的數組,對於不支持 __proto__
的瀏覽器則直接把攔截器佈置到數組自己上。
在 Observer 中,對每一個偵測了變化的數據都加了 __ob__
屬性,而且把this(Observer實例)
保存在__ob__
上,主要有兩個做用:
__ob__
,進一步拿到 Observer 實例。因此把數組的依賴存放在 Observer 中,當攔截到數組發生變化時,向依賴發送通知。
最後還要經過observeArray
偵測數組子元素和數組新增元素的變化。