小程式全域性狀態管理
緣由
使用vue的朋友可能用過vuex,使用react的朋友可能用redux,好處自然不用多說,用過的都說好。
微信小程式,我去年就曾接觸過,那時候小程式連元件的概念都沒有,現在小程式似乎更火了,也增加了很多新概念,包括自定義元件。
小程式的入口檔案app.js裡會呼叫App()方法建立一個應用程式例項,有一些公共的、全域性的資料可以儲存在app.globalData屬性裡,但是globalData並不具備響應式功能,資料變化時,不會自動更新檢視。多個頁面或者元件共享同一狀態時,處理起來也是相當麻煩的。
所以,我花了一點時間,簡單實現了一個適用於小程式的狀態管理。
demo
app.js
//app.js
import store from './store/index';
// 建立app
App(store.createApp({
globalData: {
userInfo: {},
todos: [{
name: '刷牙',
done: true
}, {
name: '吃飯',
done: false
}]
}
// ...其他
}))
複製程式碼
pages/index/index.wxml
<view class="container">
<view>
{{title}}
</view>
<view class="info">
<view>
姓名:{{userInfo.name}}
</view>
<view>
年齡:{{userInfo.age}}
</view>
</view>
<button type="button" bindtap="addTodo">增加todo</button>
<!-- todos元件 -->
<todos />
</view>
複製程式碼
pages/index/index.js
//index.js
import store from '../../store/index';
// 建立頁面
Page(store.createPage({
data: {
title: '使用者資訊頁'
},
// 依賴的全域性狀態屬性 這些狀態會被繫結到data上
globalData: ['userInfo', 'todos'],
// 這裡可以定義需要監聽的狀態,做一些額外的操作,this指向了當前頁面例項
watch: {
userInfo(val) {
console.log('userInfo更新了', val, this);
}
},
onLoad() {
this.getUserInfo().then(userInfo => {
// 通過dispatch更新globalData資料
store.dispatch('userInfo', userInfo);
})
},
// 模擬從服務端獲取使用者資訊
getUserInfo() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({
name: '小明',
age: 20
})
}, 100)
})
},
// 增加todo
addTodo() {
// 注意:這裡從this.data上獲取todos而不是globalData
const todos = [...this.data.todos, {
name: '學習',
donw: false
}];
store.dispatch('todos', todos);
}
// 其他...
}))
複製程式碼
components/todos/index.wxml
<view class="container">
<view class="todos">
<view wx:for="{{todos}}" wx:key="{{index}}">
<view>{{item.name}}</view>
<view>{{item.done?'已完成':'未完成'}}</view>
</view>
</view>
</view>
複製程式碼
components/todos/index.js
import store from '../../store/index';
// 建立元件
Component(store.createComponent({
// 依賴的全域性狀態屬性
globalData: ['todos'],
// 這裡可以定義需要監聽的狀態,做一些額外的操作,this指向了當前元件例項
watch: {
todos(val) {
console.log('todos更新了', val, this);
// 做其他事。。。
}
}
// 其他...
}))
複製程式碼
點選"增加todo"之前
點選"增加todo"之後實現原理
是不是很神奇,全是程式碼中那個store的功勞。
原理其實非常簡單,實現起來也就100+行程式碼。
主要就是使用觀察者模式(或者說是訂閱/分發模式),通過dispatch方法更新資料時,執行回撥,內部呼叫this.setData方法更新檢視。
不多說,直接貼一下原始碼。
code
store/observer.js
const events = Symbol('events');
class Observer {
constructor() {
this[events] = {};
}
on(eventName, callback) {
this[events][eventName] = this[events][eventName] || [];
this[events][eventName].push(callback);
}
emit(eventName, param) {
if (this[events][eventName]) {
this[events][eventName].forEach((value, index) => {
value(param);
})
}
}
clear(eventName) {
this[events][eventName] = [];
}
off(eventName, callback) {
this[events][eventName] = this[events][eventName] || [];
this[events][eventName].forEach((item, index) => {
if (item === callback) {
this[events][eventName].splice(index, 1);
}
})
}
one(eventName, callback) {
this[events][eventName] = [callback];
}
}
const observer = new Observer();
export {
Observer,
observer
}
複製程式碼
store/index.js
import {
Observer
} from './observer'
const bindWatcher = Symbol('bindWatcher');
const unbindWatcher = Symbol('unbindWatcher');
class Store extends Observer {
constructor() {
super();
this.app = null;
}
// 建立app
createApp(options) {
const {
onLaunch
} = options;
const store = this;
options.onLaunch = function (...params) {
store.app = this;
if (typeof onLaunch === 'function') {
onLaunch.apply(this, params);
}
}
return options;
}
// 建立頁面
createPage(options) {
const {
globalData = [],
watch = {},
onLoad,
onUnload
} = options;
const store = this;
// 儲存globalData更新回撥的引用
const globalDataWatcher = {};
// 儲存watch監聽回撥的引用
const watcher = {};
// 劫持onLoad
options.onLoad = function (...params) {
store[bindWatcher](globalData, watch, globalDataWatcher, watcher, this);
if (typeof onLoad === 'function') {
onLoad.apply(this, params);
}
}
// 劫持onUnload
options.onUnload = function () {
store[unbindWatcher](watcher, globalDataWatcher);
if (typeof onUnload === 'function') {
onUnload.apply(this);
}
}
delete options.globalData;
delete options.watch;
return options;
}
// 建立元件
createComponent(options) {
const {
globalData = [],
watch = {},
attached,
detached
} = options;
const store = this;
// 儲存globalData更新回撥的引用
const globalDataWatcher = {};
// 儲存watch監聽回撥的引用
const watcher = {};
// 劫持attached
options.attached = function (...params) {
store[bindWatcher](globalData, watch, globalDataWatcher, watcher, this);
if (typeof attached === 'function') {
attached.apply(this, params);
}
}
// 劫持detached
options.detached = function () {
store[unbindWatcher](watcher, globalDataWatcher);
if (typeof detached === 'function') {
detached.apply(this);
}
}
delete options.globalData;
delete options.watch;
return options;
}
// 派發一個action更新狀態
dispatch(action, payload) {
this.app.globalData[action] = payload;
this.emit(action, payload);
}
/**
* 1. 初始化頁面關聯的globalData並且監聽更新
* 2. 繫結watcher
* @param {Array} globalData
* @param {Object} watch
* @param {Object} globalDataWatcher
* @param {Object} watcher
* @param {Object} instance 頁面例項
*/
[bindWatcher](globalData, watch, globalDataWatcher, watcher, instance) {
const instanceData = {};
for (let prop of globalData) {
instanceData[prop] = this.app.globalData[prop];
globalDataWatcher[prop] = payload => {
instance.setData({
[prop]: payload
})
}
this.on(prop, globalDataWatcher[prop]);
}
for (let prop in watch) {
watcher[prop] = payload => {
watch[prop].call(instance, payload);
}
this.on(prop, watcher[prop])
}
instance.setData(instanceData);
}
/**
* 解綁watcher與globalDataWatcher
* @param {Object} watcher
* @param {Object} globalDataWatcher
*/
[unbindWatcher](watcher, globalDataWatcher) {
// 頁面解除安裝前 解綁對應的回撥 釋放記憶體
for (let prop in watcher) {
this.off(prop, watcher[prop]);
}
for (let prop in globalDataWatcher) {
this.off(prop, globalDataWatcher[prop])
}
}
}
export default new Store()
複製程式碼
具體的程式碼就不解釋了,原始碼裡也有基本的註釋。
目前實現的功能不算多,基本上能用了,如果業務上需求更高了,再進行擴充。
以上,
希望能給一些朋友一點點啟發,順便點個贊哦,嘻嘻!