
哈嘍,今天我們聊聊小程序的狀態管理~(有這玩意嗎)
我們要實現什么
很簡單,實現一個全局響應式的globalData,任何地方修改=>全局對應視圖數據自動更新。
并且我希望在此過程中盡量不去change原有的代碼邏輯。
為啥要實現
寫過小程序的都知道,狀態管理一直是小程序的一大痛點。
由于小程序官方沒有一個全局狀態管理機制,想要使用全局變量只能在app.js里調用App()創建一個應用程序實例,然后添加globalData屬性。但是,這個globalData并不是響應式的,也就是說在某個頁面中修改了其某個值(如果初始化注入到data中)無法完成視圖更新,更別說全局頁面和組件實例的更新了。
當前的主流做法
我們先來了解下當下比較流行的方案。
我們以westore為例,這是鵝廠出的一款覆蓋狀態管理、跨頁通訊等功能的解決方案,主要流程是通過自維護一個store(類似vuex)組件,每當頁面或組件初始化時注入并收集頁面依賴,在合適的時候手動update實現全局數據更新。提供的api也很簡潔,但是如果使用的話需要對項目原有代碼做一些侵入式的改變。比如說一:創建頁面或組件時只能通過該框架的api完成。二:每次改變全局對象時都要顯式的調用this.update()以更新視圖。
其他一些方案也都是類似的做法。但我實在不想重構原項目(其實就是懶),于是走上了造輪子的不歸路。
準備工作
正式開始前,我們先理一下思路。我們希望實現
- 將globalData響應式化。
- 收集每個頁面和組件data和globalData中對應的屬性和更新視圖的方法。
- 修改globalData時通知所有收集的頁面和組件更新視圖。
其中會涉及到發布訂閱模式,這塊不太記得的可以看看我之前的文章喲。
Talk is cheap. Show me the code.
說了這么多,也該動動手了。
首先,我們定義一個調度中心Observer用來收集全局頁面組件的實例依賴,以便有數據更新時去通知更新。 但這里有個問題,收集整個頁面組件實例未免太浪費內存且影響初始化渲染(下面的obj),如何優化呢?
// 1.Observer.js export default class Observer { constructor() { this.subscribers = {}; } add (key, obj) { // 添加依賴 這里存放的obj應該具有哪些東東? if (!this.subscribers[key]) this.subscribers[key] = []; this.subscribers[key].push(obj); } delete () { // 刪除依賴 // this.subscribers... } notify(key, value) { // 通知更新 this.subscribers[key].forEach(item => { if (item.update && typeof item.update === 'function') item.update(key, value); }); } } Observer.globalDataObserver = new Observer(); // 利用靜態屬性創建實例(相當于全局唯一變量) 復制代碼
相信很多同學想到了,其實我們只需要收集到頁面組件中data和更新方法(setData)就夠了,想到這里,不妨自定義一個Watcher類(上面的obj),每次頁面組件初始化時new Watcher(),并傳入需要的數據和方法,那我們先完成初始化注入的部分。
// 2.patcherWatcher.js // 相當于mixin了Page和Component的一些生命周期方法 import Watcher from './Watcher'; function noop() {} const prePage = Page; Page = function() { const obj = arguments.length > 0 && arguments[0] !== void 0 ? arguments[0] : {}; const _onLoad = obj.onLoad || noop; const _onUnload = obj.onUnload || noop; obj.onLoad = function () { const updateMethod = this.setState || this.setData; // setState可以認為是diff后的setData const data = obj.data || {}; // 頁面初始化添加watcher 傳入方法時別忘了綁定this指向 this._watcher = this._watcher || new Watcher(data, updateMethod.bind(this)); return _onLoad.apply(this, arguments); }; obj.onUnload = function () { // 頁面銷毀時移除watcher this._watcher.removeObserver(); return _onUnload.apply(this, arguments); }; return prePage(obj); }; // 。。。下面省略了Component的寫法,基本上和Page差不多 復制代碼
接著,根據我們的計劃,完成Watcher的部分。這里會對傳入的data做層過濾,我們只需要和globalData對應的屬性(reactiveData),并在初始化時注入Observer。
// 3.Watcher.js import Observer from './Observer'; const observer = Observer.globalDataObserver; let uid = 0; // 記錄唯一ID export default class Watcher { constructor() { const argsData = arguments[0] ? arguments[0] : {}; this.$data = JSON.parse(JSON.stringify(argsData)); this.updateFn = arguments[1] ? arguments[1] : {}; this.id = ++uid; this.reactiveData = {}; // 頁面data和globalData的交集 this.init(); } init() { this.initReactiveData(); this.createObserver(); } initReactiveData() { // 初始化reactiveData const props = Object.keys(this.$data); for(let i = 0; i < props.length; i++) { const prop = props[i]; if (prop in globalData) { this.reactiveData[prop] = getApp().globalData[prop]; this.update(prop, getApp().globalData[prop]); // 首次觸發更新 } } } createObserver() { // 添加訂閱 Object.keys(this.reactiveData) props.forEach(prop => { observer.add(prop, this); }); } update(key, value) { // 定義observer收集的依賴中的update方法 if (typeof this.updateFn === 'function') this.updateFn({ [key]: value }); } removeObserver() { // 移除訂閱 通過唯一id observer.delete(Object.keys(this.reactiveData), this.id); } } 復制代碼
最后,利用Proxy完成一個通用的響應式化對象的方法。
這里有個小細節,更改數組時set會觸發length等一些額外的記錄,這里就不細說了,有興趣的同學可以了解尤大在vue3.0的是如何處理的(避免多次 trigger)。
// 4.reactive.js import Observer from './Observer'; const isObject = val => val !== null && typeof val === 'object'; function reactive(target) { const handler = { get: function(target, key) { const res = Reflect.get(target, key); return isObject(res) ? reactive(res) : res; // 深層遍歷 }, set: function(target, key, value) { if (target[key] === value) return true; trigger(key, value); return Reflect.set(target, key, value); } }; const observed = new Proxy(target, handler); return observed; } function trigger(key, value) { // 有更改記錄時觸發更新 => 會調用所有Watcher中update方法 Observer.globalDataObserver.notify(key, value); } export { reactive }; 復制代碼
最后的最后,在app.js引用就好啦。
// app.js require('./utils/patchWatcher'); const { reactive } = require('./utils/Reactive'); App({ onLaunch: function (e) { this.globalData = reactive(this.globalData); // globalData響應式化 // ... }, // ... globalData: { /*...*/ } 復制代碼
總結
綜上,我們一步一步從 頁面組件初始化注入=>定義Watcher類=>將Watcher收集到Observer中 并在此觸發更新=>app.js全局引入 這幾個步驟完成globalData的響應式化,結果是通過新增4個文件 app.js3行代碼(包括注釋等共100多行代碼),幾乎以零侵入的方式完成,并且實現了功能分離,具有一定的可擴展性。
時間倉促,文中肯定會有一些不夠嚴謹的地方,歡迎大家指正和討論。
感謝閱讀的你!