哈嘍,今天我們聊聊小程式的狀態管理~(有這玩意嗎?)
我們要實現什麼
很簡單,實現一個全域性響應式的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多行程式碼),幾乎以零侵入的方式完成,並且實現了功能分離,具有一定的可擴充套件性。
時間倉促,文中肯定會有一些不夠嚴謹的地方,歡迎大家指正和討論。
感謝閱讀的你!
(碼字不易,都看到這裡了,求波點贊不過分吧。 over~)