150行代碼帶你實(shí)現(xiàn)微信小程序中的數(shù)據(jù)偵聽(tīng)
在小程序項(xiàng)目中, 我們的通常會(huì)使用到使用到一個(gè)全局對(duì)象作為各個(gè)頁(yè)面通用的數(shù)據(jù)存儲(chǔ)容器, 將它綁定到app對(duì)象后, 就能在每一個(gè)頁(yè)面都自由的操縱這個(gè)對(duì)象. 然而在實(shí)踐中, 由于這個(gè)對(duì)象及其屬性不具備響應(yīng)式條件, 它不能直接參與業(yè)務(wù)邏輯的編寫, 能力僅僅局限于數(shù)據(jù)儲(chǔ)存. 若是在VueJS項(xiàng)目中, 我們可能經(jīng)常使用到 Vue.$watch 去偵聽(tīng)某個(gè)數(shù)據(jù)是否發(fā)生變化, 小程序卻缺乏這種能力.
在這篇文章中, 我將用150行代碼, 手把手帶你打造一個(gè)小程序也可以使用的偵聽(tīng)器(下簡(jiǎn)稱VX):
// 一個(gè)快速賦值的語(yǔ)法糖函數(shù), 可以創(chuàng)建結(jié)構(gòu)為 { value: a { b: { val: ''} } } 的對(duì)象
vx.set('value.a.d', { val: '' })
// 對(duì)某個(gè)屬性進(jìn)行偵聽(tīng), 如果發(fā)生改變, 則執(zhí)行相應(yīng)函數(shù)(可多次watch以執(zhí)行多個(gè)函數(shù))
vx.watch('value.a.d.val', newVal => {
console.log(`val改變?yōu)?: `, newVal)
})
value.a.d.val = 3 // val改編為 : 3
使用VX偵聽(tīng)器, 我們可以更加方便的管理各個(gè)頁(yè)面的狀態(tài). 同時(shí), 我們憑借 watch 語(yǔ)法, 可以更優(yōu)雅地編寫業(yè)務(wù)邏輯.
坐穩(wěn)了, 三輪車準(zhǔn)備啟動(dòng)了~ 各位評(píng)論見(jiàn)~ :yum:
稍微理一理思路
在全局對(duì)象中, 我們不一定要對(duì)每一個(gè)屬性都進(jìn)行偵聽(tīng), 所以VX主要的功能就是通過(guò)set去設(shè)置某個(gè)具體屬性的setter/getter, 同時(shí)通過(guò)watch向添加該屬性添加需要訂閱的回調(diào)函數(shù).
依賴對(duì)象的實(shí)現(xiàn)
首先我們需要造一個(gè)通用的 依賴對(duì)象 , 依賴對(duì)象攜帶一個(gè)訂閱數(shù)組用于存放一組回調(diào)函數(shù), 同時(shí)它還應(yīng)該包括一些操作訂閱數(shù)組能力(如添加訂閱, 清空訂閱)的函數(shù)
class Dep {
constructor () {
this.subs = []
}
// 將回調(diào)添加到數(shù)組中
addSub (fn) { /*...*/ }
delSub (fn) { /*...*/ }
// 執(zhí)行數(shù)組中每一項(xiàng)函數(shù)
notify (newVal, oldVal) {
this.subs.forEach(func => func(newVal, oldVal))
}
}
全局對(duì)象中每一個(gè)響應(yīng)式屬性(及其每一個(gè)子屬性), 都應(yīng)該和一個(gè)新的Dep實(shí)例保持一一對(duì)應(yīng)的關(guān)系, 這樣我們進(jìn)行偵聽(tīng)變化, 執(zhí)行訂閱的回調(diào)函數(shù)時(shí), 只需要找到對(duì)應(yīng)的實(shí)例執(zhí)行 notify 通知更新即可.
設(shè)置響應(yīng)式屬性
defineProperty
可能是因?yàn)榻佑|DefineProperty要比接觸Proxy早一些的緣故, 代碼使用了前者進(jìn)行響應(yīng)式的實(shí)現(xiàn), Object.defineProperty方法會(huì)直接在一個(gè)對(duì)象上定義一個(gè)新屬性, 這里快速過(guò)一遍 defineProperty 具體配置:
// @param obj 要在其上定義屬性的對(duì)象
// @param key 要定義或修改的屬性的名稱
Object.defineProperty(obj, key, {
// 該屬性是否能被枚舉
enumerable: true,
// 該屬性能否被刪除
configurable: true,
// 訪問(wèn)該屬性則會(huì)執(zhí)行此方法
get: () => {
return val
},
// 修改該屬性時(shí)會(huì)執(zhí)行此方法
set: newVal => {
val = newVal
},
// value & writeble 不能和 getter/setter 同時(shí)出現(xiàn)
})
通過(guò)對(duì)defineProperty進(jìn)行上層封裝, 我們可以輕松的實(shí)現(xiàn)在全局對(duì)象上設(shè)置響應(yīng)式屬性功能, 在此, 我們結(jié)合剛才定義的Dep對(duì)象, 將一個(gè)新的dep實(shí)例綁定到新增屬性的setter中:
set (key, val, options = {}, obj = this.store) {
const dep = new Dep()
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: () => {
return val
},
set: newVal => {
if (newVal === val) {
return
}
dep.notify(newVal, val)
val = newVal
}
})
}
每當(dāng)對(duì)應(yīng)屬性被賦值, 就會(huì)執(zhí)行依賴數(shù)組中的回調(diào)函數(shù).
不過(guò)這樣還不夠, 我們還得想辦法獲取到這個(gè)dep實(shí)例, 才能給它的依賴數(shù)組填充函數(shù).
這邊提供一個(gè)很簡(jiǎn)單的思路, 并不推薦實(shí)踐中這么做:
set (key, val, options = {}, obj = this.store) {
const dep = new Dep()
Object.defineProperty(obj, key, {})
+ return dep
}
const valueDep = set('value', b, {})
valueDep.addSub(() => { console.log('value changed!') })
雖然代碼能使用了, 就是是看起來(lái)怪怪的~ :yum: 我們的三輪車開(kāi)進(jìn)了岔路~
通過(guò)watch添加訂閱
喝口水我們繼續(xù)
<黑客與畫家>一書中曾經(jīng)提到這樣一個(gè)觀點(diǎn), 我深有體會(huì):
如果你覺(jué)得你的代碼奇怪, 那么往往它是錯(cuò)的
上面的那一串代碼僅僅是能跑通的水平, 我們需要加入更多的細(xì)節(jié)和思考, 有時(shí)候只需要坐下來(lái)稍微看一下代碼, 就會(huì)有各種想法蹦出來(lái):
構(gòu)思這種東西有一個(gè)特點(diǎn),那就是它會(huì)導(dǎo)致更多的構(gòu)思。你有沒(méi)有注意過(guò),坐下來(lái)寫東西的時(shí)候,一半的構(gòu)思是寫作時(shí)產(chǎn)生的?
隱藏Dep
這些內(nèi)容應(yīng)和外部是解耦的. 首先一點(diǎn), 我們創(chuàng)建一個(gè)偵聽(tīng)器類, 用于封裝我們偵聽(tīng)所用到的所有方法, 它包含了我們想要的全局對(duì)象以及操作它的方法(如watch,set):
class VX {
constructor () {
this.store = Object.create(null)
}
watch (key, fn, obj = this.store) {}
set (key, val, options = {}, obj = this.store) {}
}
const vx = new VX()
我們可以在watch中給對(duì)象某個(gè)屬性添加回調(diào), 就不用去直接操作Dep依賴數(shù)組了. 只是, 我們?cè)跇I(yè)務(wù)代碼中調(diào)用watch, 要怎么去獲取obj.key對(duì)應(yīng)的dep呢?
我們?cè)O(shè)置一個(gè)全局的depHandler, 在obj.key的getter中主動(dòng)將depHandler設(shè)置為當(dāng)前obj.key的dep實(shí)例, 那么我們?cè)趙atch函數(shù)里, 只要用任意操作觸發(fā)obj.key的getter, 就能通過(guò)depHandler得到它的dep實(shí)例了, 代碼形如:
+ // 一開(kāi)始沒(méi)有持有dep實(shí)例
+ let handleDep = null
class VX {
watch (key, fn, obj = this.store) {
+ console.log(obj.key) // 使用任意操作觸發(fā)obj.key的getter, 那么handleDep將自動(dòng)引用obj.key的dep實(shí)例
+ handleDep.addSub(fn)
}
set (key, val, options = {}, obj = this.store) {
const dep = new Dep()
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: () => {
+ handleDep = dep
return val
},
set: newVal => {}
})
}
}
主動(dòng)收集依賴
我們?cè)黾?handleDep.addSub(fn) 添加回調(diào)函數(shù)的邏輯, 其實(shí)可以直接放到getter中, 首先在Dep類中封裝一個(gè)'主動(dòng)'收集依賴的 collect 方法, 他會(huì)將全局 handleFn 存放到訂閱數(shù)組中, 這樣一來(lái), 在watch函數(shù)中, 我們只要觸發(fā)obj.key的getter, 就可以主動(dòng)收集依賴了:
let handleFn = null
class Dep {
addSub (fn) {}
delSub (fn) {}
clear () {}
collect (fn = handleFn) {
if (fn && !this.subs.find(x => x === fn)) {
this.addSub(fn)
}
}
notify (newVal, oldVal) {}
}
let handleDep = null
class VX {
watch (key, fn, obj = this.store) {
handleFn = fn
console.log(obj.key)
}
set (key, val, options = {}, obj = this.store) {
const dep = new Dep()
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: () => {
handleDep = dep
handleDep.collect()
return val
},
set: newVal => {}
})
}
}
處理key值為對(duì)象鏈的情況
在先前的watch函數(shù)中, 我們使用console.log(obj.key)去觸發(fā)對(duì)應(yīng)屬性的getter, 如果我們調(diào)用方式是 watch('a.b.c') 就無(wú)能為力了. 這里我們封裝一個(gè)通用方法, 用于處理對(duì)象鏈字符串的形式:
// 通過(guò)將字符串'a.b'分割為['a', 'b'], 再使用一個(gè)while循環(huán)就可以走完這個(gè)對(duì)象鏈
function walkChains (key, obj) {
const segments = key.split('.')
let deepObj = obj
while (segments.length) {
deepObj = deepObj[segments.shift()]
}
}
class VX {
watch (key, fn, obj = this.store) {
handleFn = fn
walkChains(key, obj)
}
}
在set方法中處理對(duì)象鏈字符串稍微有些不同, 因?yàn)槿绻?set('a.b') 時(shí), 沒(méi)有在我們?nèi)謱?duì)象中找到a屬性, 這里應(yīng)該拋錯(cuò).
實(shí)際的處理中, 需要推斷'obj.a'以及'obj.a.b'是否存在. 假設(shè)沒(méi)有'obj.a', 那么我們應(yīng)該創(chuàng)建一個(gè)新的對(duì)象, 并且給新的對(duì)象添加屬性'b', 所以代碼類似 walkChains 函數(shù), 只是稍作一層判斷:
set (key, val, obj) {
const segments = key.split('.')
// 這里需要注意, 我們只處理到倒數(shù)第二個(gè)屬性
while (segments.length > 1) {
const handleKey = segments.shift()
const handleVal = obj[handleKey]
// 存在'obj.a'的情況
if (typeof handleVal === 'object') {
obj = handleVal
// 不存在'obj.a'則給a屬性賦一個(gè)非響應(yīng)式的對(duì)象
} else if (!handleVal) {
obj = (
key = handleKey,
obj[handleKey] = {},
obj[handleKey]
)
} else {
console.trace('already has val')
}
}
// 最后一個(gè)屬性要手動(dòng)賦值
key = segments[0]
}
業(yè)務(wù)場(chǎng)景應(yīng)用
小程序跨頁(yè)面刷新數(shù)據(jù)
我們經(jīng)常碰到在小程序中由A頁(yè)面跳轉(zhuǎn)到B頁(yè)面, 如果B頁(yè)面進(jìn)行了一些操作, 希望A頁(yè)面自動(dòng)刷新數(shù)據(jù)的情況. 但是由于A頁(yè)面跳轉(zhuǎn)到B頁(yè)面不同(也許是redirect,也許是navigate), 處理方法也不盡相同.
使用navigate方式跳轉(zhuǎn)后, A頁(yè)面不會(huì)被注銷, 所以我們一般會(huì)通過(guò)全局對(duì)象去貯存A頁(yè)面實(shí)例(也就是A頁(yè)面的this對(duì)象), 然后在B頁(yè)面直接調(diào)用相應(yīng)的方法(如A.refreshList())進(jìn)行刷新操作.
引入VX后, 我們可以在 onload 生命周期直接調(diào)用watch方法添加訂閱:
// app.js
import VX from '@/utils/suites/vx'
const vx = new VX()
app.vx = vx
app.store = vx.store
app.vx.set('userType', '商戶')
// page a
onLoad () {
app.vx.watch('userType', userType => {
if (userType === '商戶') {
// ...
} else if (userType === '管理員') {
// ...
}
}, {
immediate: true
})
}
// page b
switchUserType () {
app.store.userType = '管理員'
}
可能遇到的問(wèn)題
給watch方法添加的函數(shù)設(shè)置立即執(zhí)行
有的時(shí)候我們希望通過(guò)watch添加函數(shù)的同時(shí)還立即執(zhí)行該函數(shù)一次, 這個(gè)時(shí)候我們需要再定義額外的參數(shù)傳遞到watch中. 問(wèn)題是這個(gè)函數(shù)不一定是同步函數(shù).
簡(jiǎn)單處理如下:
class VX {
async watch (key, fn, options = { immediately: false }, obj = this.store) {
handleDep = fn
walkChains(key, obj)
options.immediately && await fn(options.defaultParams)
}
}
this綁定丟失問(wèn)題
在我在對(duì)VX進(jìn)行刪除屬性方法的擴(kuò)展時(shí), 我往walkChain函數(shù)中添加了一個(gè)執(zhí)行回調(diào)函數(shù)的機(jī)制, 并且在刪除屬性這個(gè)方法直接調(diào)用了walkChain:
+ function walkChains (key, obj, fn) {
const segments = key.split('.')
let deepObj = obj
while (segments.length) {
deepObj = deepObj[segments.shift()]
+ fn && fn()
}
}
del (key, obj = this.store) {
walkChains(key, obj, handleDep.clear)
delete obj[key]
}
因?yàn)閔andleDep.clear當(dāng)成參數(shù)傳遞進(jìn)walkChains中會(huì) 丟失this綁定 , 所以上面那段代碼其實(shí)是有問(wèn)題的, 不過(guò)稍作修改就好了:
del (key, obj = this.store) {
+ walkChains(key, obj, () => handleDep.clear())
delete obj[key]
}
總結(jié)
以上所述是小編給大家介紹的150行代碼帶你實(shí)現(xiàn)微信小程序中的數(shù)據(jù)偵聽(tīng),希望對(duì)大家有所幫助,如果大家有任何疑問(wèn)請(qǐng)給我留言,小編會(huì)及時(shí)回復(fù)大家的。在此也非常感謝大家對(duì)腳本之家網(wǎng)站的支持!
如果你覺(jué)得本文對(duì)你有幫助,歡迎轉(zhuǎn)載,煩請(qǐng)注明出處,謝謝!
- 微信小程序picker組件關(guān)于objectArray數(shù)據(jù)類型的綁定方法
- 微信小程序使用map組件實(shí)現(xiàn)獲取定位城市天氣或者指定城市天氣數(shù)據(jù)功能
- 微信小程序自定義組件的實(shí)現(xiàn)方法及自定義組件與頁(yè)面間的數(shù)據(jù)傳遞問(wèn)題
- windows上利用charles抓取微信小程序數(shù)據(jù)包,手機(jī)APP數(shù)據(jù)包(HTTP與HTTPS數(shù)據(jù)包)
- MAC系統(tǒng)利用charles抓取微信小程序和手機(jī)APP數(shù)據(jù)包(http和https數(shù)據(jù)包)
- 小程序兼容安卓和IOS數(shù)據(jù)處理問(wèn)題及坑
- 微信小程序數(shù)據(jù)存儲(chǔ)與取值詳解
- 微信小程序?qū)崿F(xiàn)給嵌套template模板傳遞數(shù)據(jù)的方式總結(jié)
- 微信小程序ajax實(shí)現(xiàn)請(qǐng)求服務(wù)器數(shù)據(jù)及模版遍歷數(shù)據(jù)功能示例
- 微信小程序wx.request實(shí)現(xiàn)后臺(tái)數(shù)據(jù)交互功能分析
相關(guān)文章
使用javascript為網(wǎng)頁(yè)增加夜間模式
如何給Web頁(yè)面增加夜間模式功能? 其實(shí)所謂的夜間模式就是在頁(yè)面上增加一個(gè)透明的遮罩層,但是遮罩層會(huì)擋住頁(yè)面元素, 解決方法是 添加DIV,給DIV的outline屬性一個(gè)很大的outline-width值,用outline的邊框作為遮罩,這樣既能正常點(diǎn)擊頁(yè)面元素,又能達(dá)到夜間模式的效果2014-01-01
js實(shí)現(xiàn)支持手機(jī)滑動(dòng)切換的輪播圖片效果實(shí)例
這篇文章主要介紹了js實(shí)現(xiàn)支持手機(jī)滑動(dòng)切換的輪播圖片效果,實(shí)例分析了javascript實(shí)現(xiàn)圖片切換的相關(guān)技巧,非常具有實(shí)用價(jià)值,需要的朋友可以參考下2015-04-04
JavaScript實(shí)現(xiàn)通過(guò)select標(biāo)簽跳轉(zhuǎn)網(wǎng)頁(yè)的方法
這篇文章主要介紹了JavaScript實(shí)現(xiàn)通過(guò)select標(biāo)簽跳轉(zhuǎn)網(wǎng)頁(yè)的方法,涉及javascript事件響應(yīng)及窗口操作相關(guān)技巧,需要的朋友可以參考下2016-09-09
JS生成不重復(fù)的隨機(jī)數(shù)組的簡(jiǎn)單實(shí)例
下面小編就為大家?guī)?lái)一篇JS生成不重復(fù)的隨機(jī)數(shù)組的簡(jiǎn)單實(shí)例。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2016-07-07
js登錄滑動(dòng)驗(yàn)證的實(shí)現(xiàn)(不滑動(dòng)無(wú)法登陸)
下面小編就為大家分享一篇js登錄滑動(dòng)驗(yàn)證的實(shí)現(xiàn)(不滑動(dòng)無(wú)法登陸),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-01-01
詳解BootStrap中Affix控件的使用及保持布局的美觀的方法
Affix是BootStrap中的一個(gè)很有用的控件,他能夠監(jiān)視瀏覽器的滾動(dòng)條的位置并讓你的導(dǎo)航始終都在頁(yè)面的可視區(qū)域。本文重點(diǎn)給大家介紹BootStrap中Affix控件的使用及保持布局的美觀的方法,感興趣的朋友一起看看吧2016-07-07
利用原生JS實(shí)現(xiàn)懶加載lazyLoad的三種方法總結(jié)
加載頁(yè)面的時(shí)候,圖片一直都是流量大頭,針對(duì)圖片的性能方法也挺多的比如base64、雪碧圖等;懶加載也是其中一種,這篇文章主要給大家介紹了關(guān)于利用原生JS實(shí)現(xiàn)懶加載lazyLoad的三種方法,需要的朋友可以參考下2021-07-07

