小程式全域性狀態管理

24號發表於2018-08-17

小程式全域性狀態管理

緣由

使用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()
複製程式碼

具體的程式碼就不解釋了,原始碼裡也有基本的註釋。
目前實現的功能不算多,基本上能用了,如果業務上需求更高了,再進行擴充。
以上,
希望能給一些朋友一點點啟發,順便點個贊哦,嘻嘻!

相關文章