实现 VUE 中 MVVM - step10 - Computed
看這篇之前,如果沒有看過之前的文章,移步查看:
回顧
先捋一下,之前我們實現的 Vue 類,主要有一下的功能:
對于比與現在的 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我們可以發現:
解決
第一點很好解決,使用 Object.defineProperty 代理一下就 ok。
接下來看第二點和第三點,當依賴發生改變時,值就會變化,這點和我們之前實現 Watcher 很像,計算屬性的值就是 get 函數的返回值,在 Watcher 中我們同樣保存了監聽的值(watcher.value),而這個值是會根據依賴的變化而變化的(如果沒看過 Watcher 實現的同學,去看下 step3 和 step4),所以計算屬性的 get 就是 Watcher 的 getter。
那么 Watcher 的 callback 是啥?其實這里根本不需要 callback ,計算屬性僅僅需要當依賴發生變化時,保存的值發生變化。
ok 了解之后我們來實現它,同樣的為了方便理解我寫成了一個類:
function noop() { }let uid = 0export default class Computed {constructor(key, option, ctx) {// 這里的 ctx 一般是 Vue 的實例this.uid = uid++this.key = keythis.option = optionthis.ctx = ctxthis._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}})} }我們實現了代理屬性 Object.defineProperty 和更新計算屬性的值,同時依賴沒變化時,也是不會觸發 Watcher 的更新,解決了以上的 3 個問題。
但是,試想一下,計算屬性真的需要實時去更新對應的值嗎?
首先我們知道,依賴的屬性發生了變化會導致計算屬性的變化,換句話說就是,當計算屬性發生變化了,data 下的屬性一定有一部分發生了變化,而 data 下屬性發生變化,會導致視圖的改變,所以計算屬性發生變化在去觸發視圖的變化是不必要的。
其次,我們不能確保計算屬性一定會用到。
而基于第一點,計算屬性是不必要去觸發視圖的變化的,所以計算屬性其實只要在獲取的時候更新對應的值即可。
Watcher 的臟檢查機制
根據我們上面的分析,而 Computed 是 Watcher 的一種實現,所以我們要實現一個不實時更新的 Watcher。
在 Watcher 中我們實現值的更新是通過下面這段代碼:
update() {const value = this.getter.call(this.obj)const oldValue = this.valuethis.value = valuethis.cb.call(this.obj, value, oldValue) }當依賴更新的時候,會去觸發這個函數,這個函數變更了 Watcher 實例保存的 value ,所以我們需要在這里做出改變,先看下偽代碼:
update() {if(/* 判斷這個 Watcher 需不需要實時更新 */){// doSomething// 跳出 updatereturn}const value = this.getter.call(this.obj)const oldValue = this.valuethis.value = valuethis.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 Watcherif (this.lazy) {// 需要進行臟檢查this.dirty = truereturn}const value = this.getter.call(this.obj)const oldValue = this.valuethis.value = valuethis.cb.call(this.obj, value, oldValue) }如果代碼走到 update 也就說明這個 Watcher 的依賴發生了變化,同時這是個 lazy Watcher ,那這個 Watcher 就需要進行臟檢查。
但是,上面代碼雖然標志了這個 Watcher ,但是 value 并沒有發生變化,我們需要專門寫一個函數去觸發變化。
/*** 臟檢查機制手動觸發更新函數*/ evaluate() {this.value = this.getter.call(this.obj)// 臟檢查機制觸發后,重置 dirtythis.dirty = false }查看完整的 Watcher 代碼
ok 接著我們來修改 Computed 的實現:
class Computed {constructor(ctx, key, option,) {this.uid = uid++this.key = keythis.option = optionthis.ctx = ctxthis._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 Yangtest.computedSet = 'accco Yang' console.log(test.computedValue) // 測試緩存 (通過 set 使得依賴發生了變化) // accco Yang到目前為止,單個 Vue 下的數據相關的內容就差不多了,在實現 props、provied/inject 機制前,我們需要先實現父子組件,這也是下一步的內容。
點擊查看相關代碼
更多內容,可以訪問http://blog.acohome.cn
總結
以上是生活随笔為你收集整理的实现 VUE 中 MVVM - step10 - Computed的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: docker里images是什么意思
- 下一篇: 基于vue自动化表单实践