在(zai)開發 vue 的(de)(de)時(shi)候(hou),我(wo)們可(ke)以使用 watch 和 computed 很(hen)方(fang)便的(de)(de)檢(jian)測數據的(de)(de)變化,從而做出相應的(de)(de)改變,但(dan)是在(zai)小程(cheng)序里,只(zhi)能在(zai)數據改變時(shi)手動(dong)觸發 this.setData(),那么(me)如(ru)何給(gei)小程(cheng)序也加上這兩個功能呢?
我們(men)知道在 vue 里是(shi)通過 Object.defineProperty 來實現(xian)數據(ju)變(bian)化(hua)檢測的,給(gei)該(gai)變(bian)量的 setter 里注入所有(you)的綁定(ding)操作,就(jiu)可以在該(gai)變(bian)量變(bian)化(hua)時帶動其它數據(ju)的變(bian)化(hua)。那(nei)么(me)是(shi)不是(shi)可以把這種方法運用在小程序(xu)上(shang)呢(ni)?
實際上(shang),在小程序(xu)里(li)實現要比 vue 里(li)簡單,應(ying)(ying)為對于(yu)(yu) data 里(li)對象(xiang)來(lai)說,vue 要遞歸(gui)的(de)(de)(de)(de)綁定對象(xiang)里(li)的(de)(de)(de)(de)每(mei)一個變量,使之響應(ying)(ying)式化(hua)。但是在微(wei)信小程序(xu)里(li),不管是對于(yu)(yu)對象(xiang)還是基本類型,只(zhi)能通(tong)過 this.setData() 來(lai)改(gai)變,這樣我們只(zhi)需檢測 data 里(li)面(mian)的(de)(de)(de)(de) key 值的(de)(de)(de)(de)變化(hua),而不用檢測 key 值里(li)面(mian)的(de)(de)(de)(de) 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) } })
},
})
現在我們要實現 watch 和(he) computed 方法,使得(de) test 變(bian)化時,test1、test2、test3 也變(bian)化,為此,我們增加了一個按(an)鈕,當(dang)點擊這個按(an)鈕時,test 會改(gai)變(bian)。
watch 方法相對簡單點(dian),首(shou)先我們定(ding)義一(yi)個函數(shu)來檢測變(bian)化:
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
},
})
}
然后遍歷(li) watch 函數(shu)傳入的(de)對(dui)象,給每個鍵調用該方法
function watch(ctx, obj) {
Object.keys(obj).forEach(key => {
defineReactive(ctx.data, key, ctx.data[key], function(value) {
obj[key].call(ctx, value)
})
})
}
這里有參數是 fn ,即上面 watch 方法里 test 的(de)值,這里把該(gai)方法包一層(ceng),綁定 context。
接著來(lai)看 computed,這個稍微復雜(za),因為我(wo)們無法得知 computed 里(li)依賴的(de)是 data 里(li)面的(de)哪個變量,因此只能(neng)遍(bian)歷 data 里(li)的(de)每一(yi)個變量。
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 里的(de)每(mei)個屬性(xing)調用 defineReactive 方法。接著計算 computed 里面每(mei)個屬性(xing)第一(yi)次(ci)的(de)值(zhi),也(ye)就是上例中的(de) test2、test3。
computed(this, {
test2: function() {
return this.data.test.a + '2222222'
},
test3: function() {
return this.data.test.a + '3333333'
}
})
這(zhe)里分別調用 test2 和(he) test3 的值(zhi),將返回值(zhi)與對(dui)(dui)應的 key 值(zhi)組合成一個(ge)對(dui)(dui)象,然(ran)后再(zai)調用 setData() ,這(zhe)樣就(jiu)會(hui)(hui)第一次計算這(zhe)兩(liang)(liang)個(ge)值(zhi),這(zhe)里使(shi)用了(le) reduce 方法。但(dan)是你可(ke)能會(hui)(hui)發現其中這(zhe)兩(liang)(liang)行(xing)代碼,它們好(hao)像(xiang)都沒有被提(ti)到是干嘛用的。
ctx.data.$target = function() {
ctx.setData({ [next]: obj[next].call(ctx) })
}
ctx.data.$target = null
可以看到(dao),test2 和 test3 都(dou)是依賴 test 的,這樣必須在 test 改變的時候在其的 setter 函數中調(diao)用 test2 和 test3 中對應的函數,并通過 setData 來(lai)設置這兩個變量(liang)。為此,需要(yao)將 defineReactive 改動一下(xia)。
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
},
})
}
相較(jiao)于之(zhi)前(qian),增加了幾行(xing)(xing)代(dai)碼,我(wo)們聲(sheng)明了一(yi)(yi)(yi)個變(bian)量來保存所有(you)在(zai)(zai)變(bian)化(hua)時(shi)需(xu)(xu)要執行(xing)(xing)的(de)(de)函(han)數(shu),在(zai)(zai) set 時(shi)執行(xing)(xing)每一(yi)(yi)(yi)個函(han)數(shu),因為此時(shi) this.data.test 的(de)(de)值(zhi)還(huan)未改變(bian),使用 setTimeout 在(zai)(zai)下一(yi)(yi)(yi)輪再執行(xing)(xing)。現在(zai)(zai)就(jiu)(jiu)(jiu)有(you)一(yi)(yi)(yi)個問題,怎么將函(han)數(shu)添(tian)加到(dao) subs 中。不(bu)知道各位還(huan)是(shi)(shi)(shi)否記得上(shang)面我(wo)們說(shuo)到(dao)的(de)(de)在(zai)(zai) reduce 里的(de)(de)那兩行(xing)(xing)代(dai)碼。因為在(zai)(zai)執行(xing)(xing)計算 test1 和 test2 第一(yi)(yi)(yi)次 computed 值(zhi)的(de)(de)時(shi)候(hou),會調用 test 的(de)(de) getter 方法,此刻就(jiu)(jiu)(jiu)是(shi)(shi)(shi)一(yi)(yi)(yi)個好機會將函(han)數(shu)注(zhu)入到(dao) subs 中,在(zai)(zai) data 上(shang)聲(sheng)明一(yi)(yi)(yi)個 $target 變(bian)量,并將需(xu)(xu)要執行(xing)(xing)的(de)(de)函(han)數(shu)賦值(zhi)給該(gai)變(bian)量,這(zhe)(zhe)樣在(zai)(zai) getter 中就(jiu)(jiu)(jiu)可(ke)以判斷 data 上(shang)有(you)無 target 值(zhi),從而就(jiu)(jiu)(jiu)可(ke)以 push 進(jin) subs,要注(zhu)意的(de)(de)是(shi)(shi)(shi)需(xu)(xu)要馬上(shang)將 target 設為 null,這(zhe)(zhe)就(jiu)(jiu)(jiu)是(shi)(shi)(shi)第二句的(de)(de)用途,這(zhe)(zhe)樣就(jiu)(jiu)(jiu)達(da)到(dao)了一(yi)(yi)(yi)石二鳥(niao)的(de)(de)作用。當然,這(zhe)(zhe)其實(shi)就(jiu)(jiu)(jiu)是(shi)(shi)(shi) vue 里的(de)(de)原理,只不(bu)過(guo)這(zhe)(zhe)里沒那么復雜。
到此為(wei)止(zhi)已經實現(xian)了(le) watch 和 computed,但是還沒完,有個問題。當(dang)同時使用這(zhe)兩(liang)者的(de)(de)時候,watch 里(li)的(de)(de)對象的(de)(de)鍵也同時存(cun)在(zai)(zai)于 data 中,這(zhe)樣(yang)就(jiu)會(hui)重復(fu)在(zai)(zai)該變量上調用 Object.defineProperty ,后(hou)面(mian)會(hui)覆蓋前(qian)面(mian)。因為(wei)這(zhe)里(li)不像 vue 里(li)可以決定兩(liang)者的(de)(de)調用順序,因此我們推薦先寫 computed 再寫 watch,這(zhe)樣(yang)可以 watch computed 里(li)的(de)(de)值(zhi)。這(zhe)樣(yang)就(jiu)有一(yi)個問題,computed 會(hui)因覆蓋而無效。
思考一下為什么?
很(hen)明顯(xian),這(zhe)(zhe)時(shi)因(yin)為(wei)(wei)之(zhi)前的(de)(de)(de) subs 被重新聲(sheng)明為(wei)(wei)空數組(zu)了。這(zhe)(zhe)時(shi),我們想一(yi)(yi)個簡單(dan)的(de)(de)(de)方(fang)法就是(shi)把之(zhi)前 computed 里(li)的(de)(de)(de) subs 存在一(yi)(yi)個地方(fang),下一(yi)(yi)次調用 defineReactive 的(de)(de)(de)時(shi)候看(kan)對應的(de)(de)(de) key 是(shi)否已經(jing)有了 subs,這(zhe)(zhe)樣就可以解(jie)決問題。修改(gai)一(yi)(yi)下代(dai)碼。
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
},
})
}
這樣,我們就一步一步的實現了所需的功能。完整的代碼和例子