在微信小程序里使用watch和computed的方法
在開發(fā) vue 的時候,我們可以使用 watch 和 computed 很方便的檢測數(shù)據(jù)的變化,從而做出相應(yīng)的改變,但是在小程序里,只能在數(shù)據(jù)改變時手動觸發(fā) this.setData()
,那么如何給小程序也加上這兩個功能呢?
我們知道在 vue 里是通過 Object.defineProperty
來實(shí)現(xiàn)數(shù)據(jù)變化檢測的,給該變量的 setter 里注入所有的綁定操作,就可以在該變量變化時帶動其它數(shù)據(jù)的變化。那么是不是可以把這種方法運(yùn)用在小程序上呢?
實(shí)際上,在小程序里實(shí)現(xiàn)要比 vue 里簡單,應(yīng)為對于 data 里對象來說,vue 要遞歸的綁定對象里的每一個變量,使之響應(yīng)式化。但是在微信小程序里,不管是對于對象還是基本類型,只能通過 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)在我們要實(shí)現(xiàn) watch 和 computed 方法,使得 test 變化時,test1、test2、test3 也變化,為此,我們增加了一個按鈕,當(dāng)點(diǎn)擊這個按鈕時,test 會改變。
watch 方法相對簡單點(diǎn),首先我們定義一個函數(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,這個稍微復(fù)雜,因?yàn)槲覀儫o法得知 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) }
詳細(xì)解釋下這段代碼,首先給 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 的值,將返回值與對應(yīng)的 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 中對應(yīng)的函數(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 因?yàn)榇藭r this.data 還沒更新 setTimeout(() => { subs.forEach(sub => sub()) }, 0) } val = newVal }, }) }
相較于之前,增加了幾行代碼,我們聲明了一個變量來保存所有在變化時需要執(zhí)行的函數(shù),在 set 時執(zhí)行每一個函數(shù),因?yàn)榇藭r this.data.test
的值還未改變,使用 setTimeout 在下一輪再執(zhí)行。現(xiàn)在就有一個問題,怎么將函數(shù)添加到 subs 中。不知道各位還是否記得上面我們說到的在 reduce 里的那兩行代碼。因?yàn)樵趫?zhí)行計算 test1 和 test2 第一次 computed 值的時候,會調(diào)用 test 的 getter 方法,此刻就是一個好機(jī)會將函數(shù)注入到 subs 中,在 data 上聲明一個 $target 變量,并將需要執(zhí)行的函數(shù)賦值給該變量,這樣在 getter 中就可以判斷 data 上有無 target 值,從而就可以 push 進(jìn) subs,要注意的是需要馬上將 target 設(shè)為 null,這就是第二句的用途,這樣就達(dá)到了一石二鳥的作用。當(dāng)然,這其實(shí)就是 vue 里的原理,只不過這里沒那么復(fù)雜。
到此為止已經(jīng)實(shí)現(xiàn)了 watch 和 computed,但是還沒完,有個問題。當(dāng)同時使用這兩者的時候,watch 里的對象的鍵也同時存在于 data 中,這樣就會重復(fù)在該變量上調(diào)用 Object.defineProperty
,后面會覆蓋前面。因?yàn)檫@里不像 vue 里可以決定兩者的調(diào)用順序,因此我們推薦先寫 computed 再寫 watch,這樣可以 watch computed 里的值。這樣就有一個問題,computed 會因覆蓋而無效。
思考一下為什么?
很明顯,這時因?yàn)橹暗?subs 被重新聲明為空數(shù)組了。這時,我們想一個簡單的方法就是把之前 computed 里的 subs 存在一個地方,下一次調(diào)用 defineReactive
的時候看對應(yīng)的 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 因?yàn)榇藭r this.data 還沒更新 setTimeout(() => { subs.forEach(sub => sub()) }, 0) } val = newVal }, }) }
這樣,我們就一步一步的實(shí)現(xiàn)了所需的功能。完整的代碼和例子請戳。
雖然經(jīng)過了一些測試,但不保證沒有其它未知錯誤,歡迎提出問題。
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
對 lightbox JS 圖片控件進(jìn)行了一下改造, 使其他支持復(fù)雜的圖片說明
如果要為圖片添加詳細(xì)的圖片說明,并為圖片的說明設(shè)置一些格式,如字體的大小、顏色等,那么使用 title 這個屬性來設(shè)置這些說明信息是沒辦法實(shí)現(xiàn)的。2010-03-03JavaScript實(shí)現(xiàn)棧結(jié)構(gòu)Stack過程詳解
這篇文章主要介紹了JavaScript實(shí)現(xiàn)棧結(jié)構(gòu)Stack過程詳解,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下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)驗(yàn)的總結(jié),分享給大家,希望大家能夠喜歡。2015-03-03js仿京東輪播效果 選項(xiàng)卡套選項(xiàng)卡使用
這篇文章主要為大家詳細(xì)介紹了js仿京東輪播效果,選項(xiàng)卡里套選項(xiàng)卡,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-01-01