回顧
在 step4
中,我們大致實現了一個 MVVM
的框架,由3個部分組成:
defineReactive
控制了物件屬性,使變為可監聽結構Dep
收集管理依賴Watcher
一個抽象的依賴
defineReactive
和 Dep
改造了物件下的某個屬性,將目標變成了觀察者模式中的目標,當目標發生變化時,會呼叫觀察者;
Watcher
就是一個具體的觀察者,會註冊到目標中。
之前的程式碼實現了觀察者模式,使得資料的變化得以響應,但是還是有兩個需要優化的地方:
- 如果我們想讓物件的屬性都得以響應,那我們必須對物件下的所有屬性進行遍歷,依次呼叫
defineReactive
這不是很方便 - 程式碼都在一個檔案中,不利於管理
解決
問題2
先解決第二個問題,我們僅僅需要把程式碼進行劃分即可,然後用 webpack/babel
打包即可,當然這裡就不說如何去配置 webpack
,使用 webpack/babel
我們就可以使用 ES6
的語法和模組系統了。
但是為了偷懶,我把程式碼直接放在 node
環境中執行了,但是 import
語法需要特定的 node
版本,我這裡使用的是 8.11.1
(版本網上都應該是支援的),同時需要特定的檔案字尾(.mjs)和命令 node --experimental-modules xxx.mjs
。
執行方式進入到 step5
的目錄下,命令列執行 node --experimental-modules test.mjs
即可。
當然你也可以用 webpack/babel
進行打包和轉碼,然後放到瀏覽器上執行即可。
問題1
對於問題1,我們需要做的僅僅是實現一個方法進行遍歷物件屬性即可。我們把這個過程抽象成一個物件 Observe
。至於為什麼要把這個過程抽象成一個物件,後面會說。
注: 由於是在 node
環境下執行程式碼,這裡就直接用 ES6
的語法了。同樣的我把別的模組也用 ES6
語法寫了一遍。
export class Observer {
constructor(value) {
this.value = value
this.walk(value)
// 標誌這個物件已經被遍歷過,同時儲存 Observer
Object.defineProperty(value, '__ob__', {
value: this,
enumerable: false,
writable: true,
configurable: true
})
}
walk(obj) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i], obj[keys[i]])
}
}
}
複製程式碼
從程式碼可以看出,這個類在例項化的時候自動遍歷了傳入引數下的所有屬性(value
),並把每個屬性都應用了 defineReactive
。
為了確保傳入的值為物件,我們再寫一個方法來判斷。
export function observe (value) {
// 確保 observe 為一個物件
if (typeof value !== 'object') {
return
}
let ob
// 如果物件下有 Observer 則不需要再次生成 Observer
if (value.hasOwnProperty('__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else if (Object.isExtensible(value)) {
ob = new Observer(value)
}
return ob
}
複製程式碼
函式返回該物件的 Observer
例項,這裡判斷了如果該物件下已經有 Observer
例項,則直接返回,不再去生產 Observer
例項。這就確保了一個物件下的 Observer
例項僅被例項化一次。
上面程式碼實現了對某個物件下所有屬性的轉化,但是如果物件下的某個屬性是物件呢?
所以我們還需改造一下 defineReactive
具體程式碼為:
export function defineReactive(object, key, value) {
let dep = new Dep()
// 遍歷 value 下的屬性,由於在 observe 中已經判斷是否為物件,這裡就不判斷了
observe(value)
Object.defineProperty(object, key, {
configurable: true,
enumerable: true,
get: function () {
if (Dep.target) {
dep.addSub(Dep.target)
Dep.target.addDep(dep)
}
return value
},
set: function (newValue) {
if (newValue !== value) {
value = newValue
dep.notify()
}
}
})
}
複製程式碼
ok 我們來測試下
import Watcher from './Watcher'
import {observe} from "./Observe"
let object = {
num1: 1,
num2: 1,
objectTest: {
num3: 1
}
}
observe(object)
let watcher = new Watcher(object, function () {
return this.num1 + this.num2 + this.objectTest.num3
}, function (newValue, oldValue) {
console.log(`監聽函式,${object.num1} + ${object.num2} + ${object.objectTest.num3} = ${newValue}`)
})
object.num1 = 2
// 監聽函式,2 + 1 + 1 = 4
object.objectTest.num3 = 2
// 監聽函式,2 + 1 + 2 = 5
複製程式碼
當然為了更好的瞭解這個過程,最好把 step5
目錄中的程式碼拉下來一起看。至於之前實現的功能這裡就不專門寫測試了。
最後
最後解釋下為什麼要把遍歷物件屬性這個過程抽象成一個物件
-
物件在
js
下存放是是引用,也就是說有可能幾個物件下的某個屬性是同一個物件下的引用,如下let obj1 = {num1: 1} let obj2 = {obj: obj1} let obj3 = {obj: obj1} 複製程式碼
如果我們抽象成物件,而僅僅是函式呼叫的話,那麼
obj1
這個物件就會遍歷兩次,而抽象成一個物件的話,我們可以把這個物件儲存在obj1
下(ob 屬性),遍歷的時候判斷一下就好。 -
當然解決上面問題我們也可以在
obj1
下設定一個標誌位即可,但是這個物件在之後會有特殊的用途,先這樣寫吧。(與陣列和Vue.set
有關)
在程式碼中我為 Dep
和 Watch
新增了 id
這個暫時用不到,先加上。