VUE - MVVM - part10 - Computed

看這篇以前,若是沒有看過以前的文章,移步拉到文章末尾查看以前的文章。vue

回顧

先捋一下,以前咱們實現的 Vue 類,主要有一下的功能:git

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

對於比與如今的 Vue 中的數據處理,咱們還有一些東西沒有實現:Computedpropsprovied/injectgithub

因爲後二者和子父組件有關,先放一放,咱們先來實現 Computed緩存

Computed

在官方文檔中有這麼一句話:函數

計算屬性的結果會被緩存,除非依賴的響應式屬性變化纔會從新計算。

這也是計算屬性性能比使用方法來的好的緣由所在。oop

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

咱們能夠發現:ui

  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 機制前,咱們須要先實現父子組件,這也是下一步的內容。

點擊查看相關代碼

系列文章地址

  1. VUE - MVVM - part1 - defineProperty
  2. VUE - MVVM - part2 - Dep
  3. VUE - MVVM - part3 - Watcher
  4. VUE - MVVM - part4 - 優化Watcher
  5. VUE - MVVM - part5 - Observe
  6. VUE - MVVM - part6 - Array
  7. VUE - MVVM - part7 - Event
  8. VUE - MVVM - part8 - 優化Event
  9. VUE - MVVM - part9 - Vue
  10. VUE - MVVM - part10 - Computed
  11. VUE - MVVM - part11 - Extend
  12. VUE - MVVM - part12 - props
  13. VUE - MVVM - part13 - inject & 總結
相關文章
相關標籤/搜索