實現 VUE 中 MVVM - step10 - Computed

undefined_er發表於2019-04-16

回顧

先捋一下,之前我們實現的 Vue 類,主要有一下的功能:

  1. 屬性和方法的代理 proxy
  2. 監聽屬性 watcher
  3. 事件

對於比與現在的 Vue 中的資料處理,我們還有一些東西沒有實現:Computedpropsprovied/inject

由於後兩者和子父元件有關,先放一放,我們先來實現 Computed

Computed

在官方文件中有這麼一句話:

計算屬性的結果會被快取,除非依賴的響應式屬性變化才會重新計算。

這也是計算屬性效能比使用方法來的好的原因所在。

ok 現在我們來實現它,我們先規定一下一個計算屬性的形式:

{
    get: Function,
    set: Function
}
複製程式碼

官方給了我們兩種形式來寫 Computed ,看了一眼原始碼,發現最終是處理成這種形式,所以我們先直接使用這種形式,之後再做統一化處理。

慣例我們通過測試程式碼來看我們要實現什麼功能:

let test = new Vue({
    data() {
        return {
            firstName: 'aco',
            lastName: 'Yang'
        }
    },
    computed: {
        computedValue: {
            get() {
                console.log('測試快取')
                return this.firstName + ' ' + this.lastName
            }
        },
        computedSet: {
            get() {
                return this.firstName + ' ' + this.lastName
            },
            set(value) {
                let names = value.split(' ')
                this.firstName = names[0]
                this.lastName = names[1]
            }
        }
    }
})

console.log(test.computedValue)
// 測試快取
// aco Yang
console.log(test.computedValue)
// acoYang (快取成功,並沒有呼叫 get 函式)
test.computedSet = 'accco Yang'
console.log(test.computedValue)
// 測試快取 (通過 set 使得依賴發生了變化)
// accco Yang
複製程式碼

我們可以發現:

  1. 計算屬性是代理到 Vue 例項上的一個屬性
  2. 第一次呼叫時,呼叫了 get 方法(有 ‘測試快取’ 輸出),而第二次沒有輸出
  3. 當依賴發生改變時,再次呼叫了 get 方法

解決

第一點很好解決,使用 Object.defineProperty 代理一下就 ok。 接下來看第二點和第三點,當依賴發生改變時,值就會變化,這點和我們之前實現 Watcher 很像,計算屬性的值就是 get 函式的返回值,在 Watcher 中我們同樣儲存了監聽的值(watcher.value),而這個值是會根據依賴的變化而變化的(如果沒看過 Watcher 實現的同學,去看下 step3step4),所以計算屬性的 get 就是 Watchergetter

那麼 Watchercallback 是啥?其實這裡根本不需要 callback ,計算屬性僅僅需要當依賴發生變化時,儲存的值發生變化。

ok 瞭解之後我們來實現它,同樣的為了方便理解我寫成了一個類:

function noop() {
}

let uid = 0

export default class Computed {
    constructor(key, option, ctx) {
        // 這裡的 ctx 一般是 Vue 的例項
        this.uid = uid++
        this.key = key
        this.option = option
        this.ctx = ctx
        this._init()
    }

    _init() {
        let watcher = new Watcher(
            this.ctx,
            this.option.get || noop,
            noop
        )

        // 將屬性代理到 Vue 例項下
        Object.defineProperty(this.ctx, this.key, {
            enumerable: true,
            configurable: true,
            set: this.option.set || noop,
            get() {
                return watcher.value
            }
        })
    }
}

// Vue 的建構函式
export class Vue extends Event {
    constructor(options) {
        super()
        this.uid = uid++
        this._init(options)
    }

    _init(options) {
        let vm = this
        ...
        for (let key in options.computed) {
            new Computed(vm, key, options.computed[key])
        }

    }
}
複製程式碼

我們實現了代理屬性 Object.defineProperty 和更新計算屬性的值,同時依賴沒變化時,也是不會觸發 Watcher 的更新,解決了以上的 3 個問題。

但是,試想一下,計算屬性真的需要實時去更新對應的值嗎?

首先我們知道,依賴的屬性發生了變化會導致計算屬性的變化,換句話說就是,當計算屬性發生變化了,data 下的屬性一定有一部分發生了變化,而 data 下屬性發生變化,會導致檢視的改變,所以計算屬性發生變化在去觸發檢視的變化是不必要的。

其次,我們不能確保計算屬性一定會用到。

而基於第一點,計算屬性是不必要去觸發檢視的變化的,所以計算屬性其實只要在獲取的時候更新對應的值即可。

Watcher 的髒檢查機制

根據我們上面的分析,而 ComputedWatcher 的一種實現,所以我們要實現一個不實時更新的 Watcher

Watcher 中我們實現值的更新是通過下面這段程式碼:

update() {
    const value = this.getter.call(this.obj)
    const oldValue = this.value
    this.value = value
    this.cb.call(this.obj, value, oldValue)
}
複製程式碼

當依賴更新的時候,會去觸發這個函式,這個函式變更了 Watcher 例項儲存的 value ,所以我們需要在這裡做出改變,先看下虛擬碼:

update() {
    if(/* 判斷這個 Watcher 需不需要實時更新 */){
        // doSomething
        // 跳出 update
        return
    }
    const value = this.getter.call(this.obj)
    const oldValue = this.value
    this.value = value
    this.cb.call(this.obj, value, oldValue)
}
複製程式碼

這裡的判斷是需要我們一開始就告訴 Watcher 的,所以同樣的我們需要修改 Watcher 的建構函式

constructor(object, getter, callback, options) {
    ···
    if (options) {
        this.lazy = !!options.lazy
    } else {
        this.lazy = false
    }
    this.dirty = this.lazy
}
複製程式碼

我們給 Watcher 多傳遞一個 options 來傳遞一些配置資訊。這裡我們把不需要實時更新的 Watcher 叫做 lazy Watcher。同時設定一個標誌(dirty)來標誌這個 Watcher 是否需要更新,換個專業點的名稱是否需要進行髒檢查。

ok 接下來我們把上面的虛擬碼實現下:

update() {
    // 如果是 lazy Watcher
    if (this.lazy) {
        // 需要進行髒檢查
        this.dirty = true
        return
    }
    const value = this.getter.call(this.obj)
    const oldValue = this.value
    this.value = value
    this.cb.call(this.obj, value, oldValue)
}
複製程式碼

如果程式碼走到 update 也就說明這個 Watcher 的依賴發生了變化,同時這是個 lazy Watcher ,那這個 Watcher 就需要進行髒檢查。

但是,上面程式碼雖然標誌了這個 Watcher ,但是 value 並沒有發生變化,我們需要專門寫一個函式去觸發變化。

/**
 * 髒檢查機制手動觸發更新函式
 */
evaluate() {
    this.value = this.getter.call(this.obj)
    // 髒檢查機制觸發後,重置 dirty
    this.dirty = false
}
複製程式碼

檢視完整的 Watcher 程式碼

ok 接著我們來修改 Computed 的實現:

class Computed {
    constructor(ctx, key, option,) {
        this.uid = uid++
        this.key = key
        this.option = option
        this.ctx = ctx
        this._init()
    }

    _init() {
        let watcher = new Watcher(
            this.ctx,
            this.option.get || noop,
            noop,
            // 告訴 Wather 來一個 lazy Watcher
            {lazy: true}
        )

        Object.defineProperty(this.ctx, this.key, {
            enumerable: true,
            configurable: true,
            set: this.option.set || noop,
            get() {
                // 如果是 dirty watch 那就觸發髒檢查機制,更新值
                if (watcher.dirty) {
                    watcher.evaluate()
                }
                return watcher.value
            }
        })
    }
}
複製程式碼

ok 測試一下

let test = new Vue({
    data() {
        return {
            firstName: 'aco',
            lastName: 'Yang'
        }
    },
    computed: {
        computedValue: {
            get() {
                console.log('測試快取')
                return this.firstName + ' ' + this.lastName
            }
        },
        computedSet: {
            get() {
                return this.firstName + ' ' + this.lastName
            },
            set(value) {
                let names = value.split(' ')
                this.firstName = names[0]
                this.lastName = names[1]
            }
        }
    }
})
// 測試快取 (剛繫結 watcher 時會呼叫一次 get 進行依賴繫結)
console.log('-------------')
console.log(test.computedValue)
// 測試快取
// aco Yang
console.log(test.computedValue)
// acoYang (快取成功,並沒有呼叫 get 函式)

test.firstName = 'acco'
console.log(test.computedValue)
// 測試快取 (當依賴發生變化時,就會呼叫 get 函式)
// acco Yang

test.computedSet = 'accco Yang'
console.log(test.computedValue)
// 測試快取 (通過 set 使得依賴發生了變化)
// accco Yang
複製程式碼

到目前為止,單個 Vue 下的資料相關的內容就差不多了,在實現 propsprovied/inject 機制前,我們需要先實現父子元件,這也是下一步的內容。

點選檢視相關程式碼

相關文章