在微信小程序里使用watch和computed的方法
在開發(fā) vue 的時候,我們可以使用 watch 和 computed 很方便的檢測數(shù)據(jù)的變化,從而做出相應的改變,但是在小程序里,只能在數(shù)據(jù)改變時手動觸發(fā) this.setData()
,那么如何給小程序也加上這兩個功能呢?
我們知道在 vue 里是通過 Object.defineProperty
來實現(xiàn)數(shù)據(jù)變化檢測的,給該變量的 setter 里注入所有的綁定操作,就可以在該變量變化時帶動其它數(shù)據(jù)的變化。那么是不是可以把這種方法運用在小程序上呢?
實際上,在小程序里實現(xiàn)要比 vue 里簡單,應為對于 data 里對象來說,vue 要遞歸的綁定對象里的每一個變量,使之響應式化。但是在微信小程序里,不管是對于對象還是基本類型,只能通過 this.setData()
來改變,這樣我們只需檢測 data 里面的 key 值的變化,而不用檢測 key 值里面的 key 。
先上測試代碼
<view>{{ test.a }}</view> <view>{{ test1 }}</view> <view>{{ test2 }}</view> <view>{{ test3 }}</view> <button bindtap="changeTest">change</button>
const { watch, computed } = require('./vuefy.js') Page({ data: { test: { a: 123 }, test1: 'test1', }, onLoad() { computed(this, { test2: function() { return this.data.test.a + '2222222' }, test3: function() { return this.data.test.a + '3333333' } }) watch(this, { test: function(newVal) { console.log('invoke watch') this.setData({ test1: newVal.a + '11111111' }) } }) }, changeTest() { this.setData({ test: { a: Math.random().toFixed(5) } }) }, })
現(xiàn)在我們要實現(xiàn) watch 和 computed 方法,使得 test 變化時,test1、test2、test3 也變化,為此,我們增加了一個按鈕,當點擊這個按鈕時,test 會改變。
watch 方法相對簡單點,首先我們定義一個函數(shù)來檢測變化:
function defineReactive(data, key, val, fn) { Object.defineProperty(data, key, { configurable: true, enumerable: true, get: function() { return val }, set: function(newVal) { if (newVal === val) return fn && fn(newVal) val = newVal }, }) }
然后遍歷 watch 函數(shù)傳入的對象,給每個鍵調(diào)用該方法
function watch(ctx, obj) { Object.keys(obj).forEach(key => { defineReactive(ctx.data, key, ctx.data[key], function(value) { obj[key].call(ctx, value) }) }) }
這里有參數(shù)是 fn ,即上面 watch 方法里 test 的值,這里把該方法包一層,綁定 context。
接著來看 computed,這個稍微復雜,因為我們無法得知 computed 里依賴的是 data 里面的哪個變量,因此只能遍歷 data 里的每一個變量。
function computed(ctx, obj) { let keys = Object.keys(obj) let dataKeys = Object.keys(ctx.data) dataKeys.forEach(dataKey => { defineReactive(ctx.data, dataKey, ctx.data[dataKey]) }) let firstComputedObj = keys.reduce((prev, next) => { ctx.data.$target = function() { ctx.setData({ [next]: obj[next].call(ctx) }) } prev[next] = obj[next].call(ctx) ctx.data.$target = null return prev }, {}) ctx.setData(firstComputedObj) }
詳細解釋下這段代碼,首先給 data 里的每個屬性調(diào)用 defineReactive
方法。接著計算 computed 里面每個屬性第一次的值,也就是上例中的 test2、test3。
computed(this, { test2: function() { return this.data.test.a + '2222222' }, test3: function() { return this.data.test.a + '3333333' } })
這里分別調(diào)用 test2 和 test3 的值,將返回值與對應的 key 值組合成一個對象,然后再調(diào)用 setData()
,這樣就會第一次計算這兩個值,這里使用了 reduce
方法。但是你可能會發(fā)現(xiàn)其中這兩行代碼,它們好像都沒有被提到是干嘛用的。
ctx.data.$target = function() { ctx.setData({ [next]: obj[next].call(ctx) }) } ctx.data.$target = null
可以看到,test2 和 test3 都是依賴 test 的,這樣必須在 test 改變的時候在其的 setter 函數(shù)中調(diào)用 test2 和 test3 中對應的函數(shù),并通過 setData 來設(shè)置這兩個變量。為此,需要將 defineReactive 改動一下。
function defineReactive(data, key, val, fn) { let subs = [] // 新增 Object.defineProperty(data, key, { configurable: true, enumerable: true, get: function() { // 新增 if (data.$target) { subs.push(data.$target) } return val }, set: function(newVal) { if (newVal === val) return fn && fn(newVal) // 新增 if (subs.length) { // 用 setTimeout 因為此時 this.data 還沒更新 setTimeout(() => { subs.forEach(sub => sub()) }, 0) } val = newVal }, }) }
相較于之前,增加了幾行代碼,我們聲明了一個變量來保存所有在變化時需要執(zhí)行的函數(shù),在 set 時執(zhí)行每一個函數(shù),因為此時 this.data.test
的值還未改變,使用 setTimeout 在下一輪再執(zhí)行?,F(xiàn)在就有一個問題,怎么將函數(shù)添加到 subs 中。不知道各位還是否記得上面我們說到的在 reduce 里的那兩行代碼。因為在執(zhí)行計算 test1 和 test2 第一次 computed 值的時候,會調(diào)用 test 的 getter 方法,此刻就是一個好機會將函數(shù)注入到 subs 中,在 data 上聲明一個 $target 變量,并將需要執(zhí)行的函數(shù)賦值給該變量,這樣在 getter 中就可以判斷 data 上有無 target 值,從而就可以 push 進 subs,要注意的是需要馬上將 target 設(shè)為 null,這就是第二句的用途,這樣就達到了一石二鳥的作用。當然,這其實就是 vue 里的原理,只不過這里沒那么復雜。
到此為止已經(jīng)實現(xiàn)了 watch 和 computed,但是還沒完,有個問題。當同時使用這兩者的時候,watch 里的對象的鍵也同時存在于 data 中,這樣就會重復在該變量上調(diào)用 Object.defineProperty
,后面會覆蓋前面。因為這里不像 vue 里可以決定兩者的調(diào)用順序,因此我們推薦先寫 computed 再寫 watch,這樣可以 watch computed 里的值。這樣就有一個問題,computed 會因覆蓋而無效。
思考一下為什么?
很明顯,這時因為之前的 subs 被重新聲明為空數(shù)組了。這時,我們想一個簡單的方法就是把之前 computed 里的 subs 存在一個地方,下一次調(diào)用 defineReactive
的時候看對應的 key 是否已經(jīng)有了 subs,這樣就可以解決問題。修改一下代碼。
function defineReactive(data, key, val, fn) { let subs = data['$' + key] || [] // 新增 Object.defineProperty(data, key, { configurable: true, enumerable: true, get: function() { if (data.$target) { subs.push(data.$target) data['$' + key] = subs // 新增 } return val }, set: function(newVal) { if (newVal === val) return fn && fn(newVal) if (subs.length) { // 用 setTimeout 因為此時 this.data 還沒更新 setTimeout(() => { subs.forEach(sub => sub()) }, 0) } val = newVal }, }) }
這樣,我們就一步一步的實現(xiàn)了所需的功能。完整的代碼和例子請戳。
雖然經(jīng)過了一些測試,但不保證沒有其它未知錯誤,歡迎提出問題。
以上就是本文的全部內(nèi)容,希望對大家的學習有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
對 lightbox JS 圖片控件進行了一下改造, 使其他支持復雜的圖片說明
如果要為圖片添加詳細的圖片說明,并為圖片的說明設(shè)置一些格式,如字體的大小、顏色等,那么使用 title 這個屬性來設(shè)置這些說明信息是沒辦法實現(xiàn)的。2010-03-03JavaScript實現(xiàn)棧結(jié)構(gòu)Stack過程詳解
這篇文章主要介紹了JavaScript實現(xiàn)棧結(jié)構(gòu)Stack過程詳解,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下2020-03-03TypeScript中交叉類型和聯(lián)合類型的區(qū)別詳解
聯(lián)合類型(Union Types)和交叉類型(Intersection Types)是 TypeScript 中的兩種高級類型,它們都用于組合多個類型并生成新的類型,但它們兩者之間的用法不一樣,本文小編就給大家講講TypeScript中交叉類型和聯(lián)合類型的區(qū)別,需要的朋友可以參考下2023-09-09innerHTML屬性,outerHTML屬性,textContent屬性,innerText屬性區(qū)別詳解
這篇文章主要介紹了javascript中的innerHTML屬性,outerHTML屬性,textContent屬性,innerText屬性區(qū)別詳解,都是個人經(jīng)驗的總結(jié),分享給大家,希望大家能夠喜歡。2015-03-03