實現 VUE 中 MVVM - step5 - Observe
在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
這個暫時用不到,先加上。