在文章 原始碼學習VUE之響應式原理我們大概描述了響應式的實現流程,主要寫了observe,dep和wather的簡易實現,以及推導思路。但相應程式碼邏輯並不完善,今天我們再來填之前的一些坑。
Observe
之前實現的observe函式只能處理一個物件的單個屬性,但我們更多的資料是儲存在物件中,為了抽象話,我們也封裝一個物件Observe,只要傳進一個引數,就可以把這個物件進行監聽。
對現有所有屬性進行監聽
var obj = {
a: 1,
b: 2
}
比如一個物件有兩個屬性 a,b。我們可以嘗試寫出下面的實現類
class Observe{
constructor(value){
this.value = value //要監聽的值。
this.walk();
}
walk(){ //通過walk函式,依次處理
const keys = Object.keys(obj);
let self = this;
for (let i = 0; i < keys.length; i++) {
self.defineReactive(obj, keys[i])
}
}
defineReactive (data, key, val) {
var dep = new Dep();
Object.defineProperty(obj, a, {
enumerable: true,
configurable: true,
get: function(){
if(Dep.target){
dep.addSub(Dep.target); // Dep.target是Watcher的例項
}
},
set: function(newVal){
if(val === newVal) return
val = newVal;
dep.notify();
}
})
}
}
當然,為了防止重複監聽,我們可以給原object設定一個識別符號以作辨別。
class Obsever(){
construct(){
this.value = value //要監聽的值。
Object.defineProperty(value, "__ob__", {
value: this,
enumerable: false,
writable: true,
configurable: true
})
this.walk();
}
}
監聽陣列
雖然陣列也是一個物件,但是我們隊陣列的操作卻不會觸發set,get方法。因此必須對陣列特殊處理。
首先需要對運算元組的方法進行改寫,如push
,pop
,shift
等
//首先拿到Array的原生原型鏈
const arrayProto = Arrary.prototype;
//為了保證修改不會影響原生方法,我們建立一個新物件
const arrayMethods = Object.create(arrayProto);
//要改寫的方法
const methodsToPatch = [`push`,`pop`,`shift`,`unshift`,`splice`,`sort`,`reverse`]
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
break
case `splice`:
inserted = args.slice(2)
break
}
//Observe插入的值
if (inserted) ob.observeArray(inserted)
// notify change
ob.dep.notify()
return result
})
})
其實邏輯很簡單。對於可以改變array的方法我們都改寫一下。只要呼叫了這些方法,除了返回正確的值,我們都通知觀察物件,資料改變了,觸發觀察者update操作。同時,陣列裡面可能是個物件,我們不改變陣列本身,但是改變陣列裡面的某個值,這也算是一種改變,因此,除了監聽陣列本身的改變,也要對陣列每個值進行observe。
這涉及到兩點,一是observe Array的時候,就要對每個值進行Observe。另外,插入陣列的每個值也要observe.第二點就是上面程式碼中特別關注push
,unshift
,splice
這三個可以插值方法的原因。
class Obsever(){
construct(){
this.value = value //要監聽的值。
Object.defineProperty(value, "__ob__", {
value: this,
enumerable: false,
writable: true,
configurable: true
})
if(Array.isArray(value)){
this.observeArray();
}else{
this.walk();
}
}
observeArray(items){
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
},
function observe (value) {
let ob
if (hasOwn(value, `__ob__`) && value.__ob__ instanceof Observer) {
// 如果已經observe的物件就不再進行重複的observe操作
ob = value.__ob__
} else {
ob = new Observer(value)
}
return ob
}
優化
實際開發中我們經常會遇到一個很大的資料。如渲染tables時,table的資料很可能很大(一個多多維陣列)。如果都進行observe無意會是很大的開銷。關鍵是我們只是需要拿這些資料來渲染,並不關心資料內部的變化。因此可能就存在這種需求,可以不對array或object深層遍歷observe。我們可以使用Object.freeze()將這個資料凍結起來。
因此對於凍結的資料我們就不再進行observe。上面的程式碼可以這麼優化
function observe (value) {
let ob
if (hasOwn(value, `__ob__`) && value.__ob__ instanceof Observer) {
// 如果已經observe的物件就不再進行重複的observe操作
ob = value.__ob__
} else if(Object.isExtensible(value)){// 如果資料被凍結,或者不可擴充套件,則不進行observe操作
ob = new Observer(value)
}
return ob
}
defineReactive (data, key, val) {
var dep = new Dep();
var property = Object.getOwnPropertyDescriptor(obj, key)
// 如果資料被凍結,或者不可擴充套件,則改寫set,get方法
if (property && property.configurable === false) {
return
}
//傳進來的物件可能之前已經被定義了set,get方法,因此我們不能直接拿value
var getter = property && property.get
var setter = property && property.set
Object.defineProperty(obj, a, {
enumerable: true,
configurable: true,
get: function(){
var value = getter ? getter.call(obj) : val;
if(Dep.target){
dep.addSub(Dep.target); // Dep.target是Watcher的例項
}
return value
},
set: function(newVal){
if(val === newVal) return
val = newVal;
dep.notify();
}
})
}