實現 VUE 中 MVVM - step5 - Observe

undefined_er發表於2019-04-12

回顧

step4 中,我們大致實現了一個 MVVM 的框架,由3個部分組成:

  1. defineReactive 控制了物件屬性,使變為可監聽結構
  2. Dep 收集管理依賴
  3. Watcher 一個抽象的依賴

defineReactiveDep 改造了物件下的某個屬性,將目標變成了觀察者模式中的目標,當目標發生變化時,會呼叫觀察者;

Watcher 就是一個具體的觀察者,會註冊到目標中。

之前的程式碼實現了觀察者模式,使得資料的變化得以響應,但是還是有兩個需要優化的地方:

  1. 如果我們想讓物件的屬性都得以響應,那我們必須對物件下的所有屬性進行遍歷,依次呼叫 defineReactive 這不是很方便
  2. 程式碼都在一個檔案中,不利於管理

解決

問題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 有關)

在程式碼中我為 DepWatch 新增了 id 這個暫時用不到,先加上。

點選檢視相關程式碼

相關文章