您好,登錄后才能下訂單哦!
這篇文章將為大家詳細講解有關如何實現微信小程序中的數據偵聽,小編覺得挺實用的,因此分享給大家做個參考,希望大家閱讀完這篇文章后可以有所收獲。
在小程序項目中, 我們的通常會使用到使用到一個全局對象作為各個頁面通用的數據存儲容器, 將它綁定到app對象后, 就能在每一個頁面都自由的操縱這個對象. 然而在實踐中, 由于這個對象及其屬性不具備響應式條件, 它不能直接參與業務邏輯的編寫, 能力僅僅局限于數據儲存. 若是在VueJS項目中, 我們可能經常使用到 Vue.$watch
去偵聽某個數據是否發生變化, 小程序卻缺乏這種能力.
在這篇文章中, 我將用150行代碼, 手把手帶你打造一個小程序也可以使用的偵聽器(下簡稱VX):
// 一個快速賦值的語法糖函數, 可以創建結構為 { value: a { b: { val: ''} } } 的對象 vx.set('value.a.d', { val: '' }) // 對某個屬性進行偵聽, 如果發生改變, 則執行相應函數(可多次watch以執行多個函數) vx.watch('value.a.d.val', newVal => { console.log(`val改變為 : `, newVal) }) value.a.d.val = 3 // val改編為 : 3
使用VX偵聽器, 我們可以更加方便的管理各個頁面的狀態. 同時, 我們憑借 watch 語法, 可以更優雅地編寫業務邏輯.
坐穩了, 三輪車準備啟動了~ 各位評論見~ :yum:
稍微理一理思路
在全局對象中, 我們不一定要對每一個屬性都進行偵聽, 所以VX主要的功能就是通過set去設置某個具體屬性的setter/getter, 同時通過watch向添加該屬性添加需要訂閱的回調函數.
依賴對象的實現
首先我們需要造一個通用的 依賴對象 , 依賴對象攜帶一個訂閱數組用于存放一組回調函數, 同時它還應該包括一些操作訂閱數組能力(如添加訂閱, 清空訂閱)的函數
class Dep { constructor () { this.subs = [] } // 將回調添加到數組中 addSub (fn) { /*...*/ } delSub (fn) { /*...*/ } // 執行數組中每一項函數 notify (newVal, oldVal) { this.subs.forEach(func => func(newVal, oldVal)) } }
全局對象中每一個響應式屬性(及其每一個子屬性), 都應該和一個新的Dep實例保持一一對應的關系, 這樣我們進行偵聽變化, 執行訂閱的回調函數時, 只需要找到對應的實例執行 notify 通知更新即可.
設置響應式屬性
defineProperty
可能是因為接觸DefineProperty要比接觸Proxy早一些的緣故, 代碼使用了前者進行響應式的實現, Object.defineProperty方法會直接在一個對象上定義一個新屬性, 這里快速過一遍 defineProperty 具體配置:
// @param obj 要在其上定義屬性的對象 // @param key 要定義或修改的屬性的名稱 Object.defineProperty(obj, key, { // 該屬性是否能被枚舉 enumerable: true, // 該屬性能否被刪除 configurable: true, // 訪問該屬性則會執行此方法 get: () => { return val }, // 修改該屬性時會執行此方法 set: newVal => { val = newVal }, // value & writeble 不能和 getter/setter 同時出現 })
通過對defineProperty進行上層封裝, 我們可以輕松的實現在全局對象上設置響應式屬性功能, 在此, 我們結合剛才定義的Dep對象, 將一個新的dep實例綁定到新增屬性的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 } }) }
每當對應屬性被賦值, 就會執行依賴數組中的回調函數.
不過這樣還不夠, 我們還得想辦法獲取到這個dep實例, 才能給它的依賴數組填充函數.
這邊提供一個很簡單的思路, 并不推薦實踐中這么做:
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!') })
雖然代碼能使用了, 就是是看起來怪怪的~ :yum: 我們的三輪車開進了岔路~
通過watch添加訂閱
喝口水我們繼續
<黑客與畫家>一書中曾經提到這樣一個觀點, 我深有體會:
如果你覺得你的代碼奇怪, 那么往往它是錯的
上面的那一串代碼僅僅是能跑通的水平, 我們需要加入更多的細節和思考, 有時候只需要坐下來稍微看一下代碼, 就會有各種想法蹦出來:
構思這種東西有一個特點,那就是它會導致更多的構思。你有沒有注意過,坐下來寫東西的時候,一半的構思是寫作時產生的?
隱藏Dep
這些內容應和外部是解耦的. 首先一點, 我們創建一個偵聽器類, 用于封裝我們偵聽所用到的所有方法, 它包含了我們想要的全局對象以及操作它的方法(如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中給對象某個屬性添加回調, 就不用去直接操作Dep依賴數組了. 只是, 我們在業務代碼中調用watch, 要怎么去獲取obj.key對應的dep呢?
我們設置一個全局的depHandler, 在obj.key的getter中主動將depHandler設置為當前obj.key的dep實例, 那么我們在watch函數里, 只要用任意操作觸發obj.key的getter, 就能通過depHandler得到它的dep實例了, 代碼形如:
+ // 一開始沒有持有dep實例 + let handleDep = null class VX { watch (key, fn, obj = this.store) { + console.log(obj.key) // 使用任意操作觸發obj.key的getter, 那么handleDep將自動引用obj.key的dep實例 + 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 => {} }) } }
主動收集依賴
我們增加 handleDep.addSub(fn)
添加回調函數的邏輯, 其實可以直接放到getter中, 首先在Dep類中封裝一個'主動'收集依賴的 collect 方法, 他會將全局 handleFn 存放到訂閱數組中, 這樣一來, 在watch函數中, 我們只要觸發obj.key的getter, 就可以主動收集依賴了:
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值為對象鏈的情況
在先前的watch函數中, 我們使用console.log(obj.key)
去觸發對應屬性的getter, 如果我們調用方式是 watch('a.b.c')
就無能為力了. 這里我們封裝一個通用方法, 用于處理對象鏈字符串的形式:
// 通過將字符串'a.b'分割為['a', 'b'], 再使用一個while循環就可以走完這個對象鏈 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方法中處理對象鏈字符串稍微有些不同, 因為如果 set('a.b') 時, 沒有在我們全局對象中找到a屬性, 這里應該拋錯.
實際的處理中, 需要推斷'obj.a'以及'obj.a.b'是否存在. 假設沒有'obj.a', 那么我們應該創建一個新的對象, 并且給新的對象添加屬性'b', 所以代碼類似 walkChains 函數, 只是稍作一層判斷:
set (key, val, obj) { const segments = key.split('.') // 這里需要注意, 我們只處理到倒數第二個屬性 while (segments.length > 1) { const handleKey = segments.shift() const handleVal = obj[handleKey] // 存在'obj.a'的情況 if (typeof handleVal === 'object') { obj = handleVal // 不存在'obj.a'則給a屬性賦一個非響應式的對象 } else if (!handleVal) { obj = ( key = handleKey, obj[handleKey] = {}, obj[handleKey] ) } else { console.trace('already has val') } } // 最后一個屬性要手動賦值 key = segments[0] }
業務場景應用
小程序跨頁面刷新數據
我們經常碰到在小程序中由A頁面跳轉到B頁面, 如果B頁面進行了一些操作, 希望A頁面自動刷新數據的情況. 但是由于A頁面跳轉到B頁面不同(也許是redirect,也許是navigate), 處理方法也不盡相同.
使用navigate方式跳轉后, A頁面不會被注銷, 所以我們一般會通過全局對象去貯存A頁面實例(也就是A頁面的this對象), 然后在B頁面直接調用相應的方法(如A.refreshList())進行刷新操作.
引入VX后, 我們可以在 onload 生命周期直接調用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 = '管理員' }
可能遇到的問題
給watch方法添加的函數設置立即執行
有的時候我們希望通過watch添加函數的同時還立即執行該函數一次, 這個時候我們需要再定義額外的參數傳遞到watch中. 問題是這個函數不一定是同步函數.
簡單處理如下:
class VX { async watch (key, fn, options = { immediately: false }, obj = this.store) { handleDep = fn walkChains(key, obj) options.immediately && await fn(options.defaultParams) } }
this綁定丟失問題
在我在對VX進行刪除屬性方法的擴展時, 我往walkChain函數中添加了一個執行回調函數的機制, 并且在刪除屬性這個方法直接調用了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] }
因為handleDep.clear當成參數傳遞進walkChains中會 丟失this綁定 , 所以上面那段代碼其實是有問題的, 不過稍作修改就好了:
del (key, obj = this.store) { + walkChains(key, obj, () => handleDep.clear()) delete obj[key] }
關于“如何實現微信小程序中的數據偵聽”這篇文章就分享到這里了,希望以上內容可以對大家有一定的幫助,使各位可以學到更多知識,如果覺得文章不錯,請把它分享出去讓更多的人看到。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。