實現 VUE 中 MVVM - step10 - Computed
先捋一下,之前我們實現的Vue
類,主要有一下的功能:
proxy watcher
對於比與現在的Vue
中的資料處理,我們還有一些東西沒有實現:Computed
、props
、provied/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 複製程式碼
我們可以發現:
Vue get get
解決
第一點很好解決,使用Object.defineProperty
代理一下就 ok。
接下來看第二點和第三點,當依賴發生改變時,值就會變化
,這點和我們之前實現Watcher
很像,計算屬性的值就是get
函式的返回值,在Watcher
中我們同樣儲存了監聽的值(watcher.value
),而這個值是會根據依賴的變化而變化的(如果沒看過Watcher
實現的同學,去看下step3
和step4
),所以計算屬性的get
就是Watcher
的getter
。
那麼Watcher
的callback
是啥?其實這裡根本不需要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 的髒檢查機制
根據我們上面的分析,而Computed
是Watcher
的一種實現,所以我們要實現一個不實時更新的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 } 複製程式碼
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
下的資料相關的內容就差不多了,在實現props
、provied/inject
機制前,我們需要先實現父子元件,這也是下一步的內容。