实现 VUE 中 MVVM - step10 - Computed
看這篇之前,如果沒有看過之前的文章,移步查看:
回顧
先捋一下,之前我們實(shí)現(xiàn)的 Vue 類,主要有一下的功能:
對(duì)于比與現(xiàn)在的 Vue 中的數(shù)據(jù)處理,我們還有一些東西沒有實(shí)現(xiàn):Computed、props、provied/inject。
由于后兩者和子父組件有關(guān),先放一放,我們先來(lái)實(shí)現(xiàn) Computed 。
Computed
在官方文檔中有這么一句話:
計(jì)算屬性的結(jié)果會(huì)被緩存,除非依賴的響應(yīng)式屬性變化才會(huì)重新計(jì)算。這也是計(jì)算屬性性能比使用方法來(lái)的好的原因所在。
ok 現(xiàn)在我們來(lái)實(shí)現(xiàn)它,我們先規(guī)定一下一個(gè)計(jì)算屬性的形式:
{get: Function,set: Function }官方給了我們兩種形式來(lái)寫 Computed ,看了一眼源碼,發(fā)現(xiàn)最終是處理成這種形式,所以我們先直接使用這種形式,之后再做統(tǒng)一化處理。
慣例我們通過測(cè)試代碼來(lái)看我們要實(shí)現(xiàn)什么功能:
let test = new Vue({data() {return {firstName: 'aco',lastName: 'Yang'}},computed: {computedValue: {get() {console.log('測(cè)試緩存')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) // 測(cè)試緩存 // aco Yang console.log(test.computedValue) // acoYang (緩存成功,并沒有調(diào)用 get 函數(shù)) test.computedSet = 'accco Yang' console.log(test.computedValue) // 測(cè)試緩存 (通過 set 使得依賴發(fā)生了變化) // accco Yang我們可以發(fā)現(xiàn):
解決
第一點(diǎn)很好解決,使用 Object.defineProperty 代理一下就 ok。
接下來(lái)看第二點(diǎn)和第三點(diǎn),當(dāng)依賴發(fā)生改變時(shí),值就會(huì)變化,這點(diǎn)和我們之前實(shí)現(xiàn) Watcher 很像,計(jì)算屬性的值就是 get 函數(shù)的返回值,在 Watcher 中我們同樣保存了監(jiān)聽的值(watcher.value),而這個(gè)值是會(huì)根據(jù)依賴的變化而變化的(如果沒看過 Watcher 實(shí)現(xiàn)的同學(xué),去看下 step3 和 step4),所以計(jì)算屬性的 get 就是 Watcher 的 getter。
那么 Watcher 的 callback 是啥?其實(shí)這里根本不需要 callback ,計(jì)算屬性僅僅需要當(dāng)依賴發(fā)生變化時(shí),保存的值發(fā)生變化。
ok 了解之后我們來(lái)實(shí)現(xiàn)它,同樣的為了方便理解我寫成了一個(gè)類:
function noop() { }let uid = 0export default class Computed {constructor(key, option, ctx) {// 這里的 ctx 一般是 Vue 的實(shí)例this.uid = uid++this.key = keythis.option = optionthis.ctx = ctxthis._init()}_init() {let watcher = new Watcher(this.ctx,this.option.get || noop,noop)// 將屬性代理到 Vue 實(shí)例下Object.defineProperty(this.ctx, this.key, {enumerable: true,configurable: true,set: this.option.set || noop,get() {return watcher.value}})} }我們實(shí)現(xiàn)了代理屬性 Object.defineProperty 和更新計(jì)算屬性的值,同時(shí)依賴沒變化時(shí),也是不會(huì)觸發(fā) Watcher 的更新,解決了以上的 3 個(gè)問題。
但是,試想一下,計(jì)算屬性真的需要實(shí)時(shí)去更新對(duì)應(yīng)的值嗎?
首先我們知道,依賴的屬性發(fā)生了變化會(huì)導(dǎo)致計(jì)算屬性的變化,換句話說(shuō)就是,當(dāng)計(jì)算屬性發(fā)生變化了,data 下的屬性一定有一部分發(fā)生了變化,而 data 下屬性發(fā)生變化,會(huì)導(dǎo)致視圖的改變,所以計(jì)算屬性發(fā)生變化在去觸發(fā)視圖的變化是不必要的。
其次,我們不能確保計(jì)算屬性一定會(huì)用到。
而基于第一點(diǎn),計(jì)算屬性是不必要去觸發(fā)視圖的變化的,所以計(jì)算屬性其實(shí)只要在獲取的時(shí)候更新對(duì)應(yīng)的值即可。
Watcher 的臟檢查機(jī)制
根據(jù)我們上面的分析,而 Computed 是 Watcher 的一種實(shí)現(xiàn),所以我們要實(shí)現(xiàn)一個(gè)不實(shí)時(shí)更新的 Watcher。
在 Watcher 中我們實(shí)現(xiàn)值的更新是通過下面這段代碼:
update() {const value = this.getter.call(this.obj)const oldValue = this.valuethis.value = valuethis.cb.call(this.obj, value, oldValue) }當(dāng)依賴更新的時(shí)候,會(huì)去觸發(fā)這個(gè)函數(shù),這個(gè)函數(shù)變更了 Watcher 實(shí)例保存的 value ,所以我們需要在這里做出改變,先看下偽代碼:
update() {if(/* 判斷這個(gè) Watcher 需不需要實(shí)時(shí)更新 */){// doSomething// 跳出 updatereturn}const value = this.getter.call(this.obj)const oldValue = this.valuethis.value = valuethis.cb.call(this.obj, value, oldValue) }這里的判斷是需要我們一開始就告訴 Watcher 的,所以同樣的我們需要修改 Watcher 的構(gòu)造函數(shù)
constructor(object, getter, callback, options) {···if (options) {this.lazy = !!options.lazy} else {this.lazy = false}this.dirty = this.lazy }我們給 Watcher 多傳遞一個(gè) options 來(lái)傳遞一些配置信息。這里我們把不需要實(shí)時(shí)更新的 Watcher 叫做 lazy Watcher。同時(shí)設(shè)置一個(gè)標(biāo)志(dirty)來(lái)標(biāo)志這個(gè) Watcher 是否需要更新,換個(gè)專業(yè)點(diǎn)的名稱是否需要進(jìn)行臟檢查。
ok 接下來(lái)我們把上面的偽代碼實(shí)現(xiàn)下:
update() {// 如果是 lazy Watcherif (this.lazy) {// 需要進(jìn)行臟檢查this.dirty = truereturn}const value = this.getter.call(this.obj)const oldValue = this.valuethis.value = valuethis.cb.call(this.obj, value, oldValue) }如果代碼走到 update 也就說(shuō)明這個(gè) Watcher 的依賴發(fā)生了變化,同時(shí)這是個(gè) lazy Watcher ,那這個(gè) Watcher 就需要進(jìn)行臟檢查。
但是,上面代碼雖然標(biāo)志了這個(gè) Watcher ,但是 value 并沒有發(fā)生變化,我們需要專門寫一個(gè)函數(shù)去觸發(fā)變化。
/*** 臟檢查機(jī)制手動(dòng)觸發(fā)更新函數(shù)*/ evaluate() {this.value = this.getter.call(this.obj)// 臟檢查機(jī)制觸發(fā)后,重置 dirtythis.dirty = false }查看完整的 Watcher 代碼
ok 接著我們來(lái)修改 Computed 的實(shí)現(xiàn):
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 來(lái)一個(gè) lazy Watcher{lazy: true})Object.defineProperty(this.ctx, this.key, {enumerable: true,configurable: true,set: this.option.set || noop,get() {// 如果是 dirty watch 那就觸發(fā)臟檢查機(jī)制,更新值if (watcher.dirty) {watcher.evaluate()}return watcher.value}})} }ok 測(cè)試一下
let test = new Vue({data() {return {firstName: 'aco',lastName: 'Yang'}},computed: {computedValue: {get() {console.log('測(cè)試緩存')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]}}} }) // 測(cè)試緩存 (剛綁定 watcher 時(shí)會(huì)調(diào)用一次 get 進(jìn)行依賴綁定) console.log('-------------') console.log(test.computedValue) // 測(cè)試緩存 // aco Yang console.log(test.computedValue) // acoYang (緩存成功,并沒有調(diào)用 get 函數(shù))test.firstName = 'acco' console.log(test.computedValue) // 測(cè)試緩存 (當(dāng)依賴發(fā)生變化時(shí),就會(huì)調(diào)用 get 函數(shù)) // acco Yangtest.computedSet = 'accco Yang' console.log(test.computedValue) // 測(cè)試緩存 (通過 set 使得依賴發(fā)生了變化) // accco Yang到目前為止,單個(gè) Vue 下的數(shù)據(jù)相關(guān)的內(nèi)容就差不多了,在實(shí)現(xiàn) props、provied/inject 機(jī)制前,我們需要先實(shí)現(xiàn)父子組件,這也是下一步的內(nèi)容。
點(diǎn)擊查看相關(guān)代碼
更多內(nèi)容,可以訪問http://blog.acohome.cn
總結(jié)
以上是生活随笔為你收集整理的实现 VUE 中 MVVM - step10 - Computed的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: docker里images是什么意思
- 下一篇: numpy 读取txt为array 一行