歡迎光臨~泉州哈哈網絡科技有限公司
當前位置: 您的位置:首 頁 > 行業資訊 > 最新資訊 > 你可以零侵入式實現小程序的全局狀態管理嗎

你可以零侵入式實現小程序的全局狀態管理嗎

  • 時間:2021-02-02 15:47:20瀏覽量:365來源:管理員

  • 我們要實現什么

    很簡單,實現一個全局響應式的globalData,任何地方修改=>全局對應視圖數據自動更新。

    并且我希望在此過程中盡量不去change原有的代碼邏輯。

    為啥要實現

    寫過小程序的都知道,狀態管理一直是小程序的一大痛點。

    由于小程序官方沒有一個全局狀態管理機制,想要使用全局變量只能在app.js里調用App()創建一個應用程序實例,然后添加globalData屬性。但是,這個globalData并不是響應式的,也就是說在某個頁面中修改了其某個值(如果初始化注入到data中)無法完成視圖更新,更別說全局頁面和組件實例的更新了。

    當前的主流做法

    我們先來了解下當下比較流行的方案。

    我們以westore為例,這是鵝廠出的一款覆蓋狀態管理、跨頁通訊等功能的解決方案,主要流程是通過自維護一個store(類似vuex)組件,每當頁面或組件初始化時注入并收集頁面依賴,在合適的時候手動update實現全局數據更新。提供的api也很簡潔,但是如果使用的話需要對項目原有代碼做一些侵入式的改變。比如說一:創建頁面或組件時只能通過該框架的api完成。二:每次改變全局對象時都要顯式的調用this.update()以更新視圖。

    其他一些方案也都是類似的做法。但我實在不想重構原項目(其實就是懶),于是走上了造輪子的不歸路。

    準備工作

    正式開始前,我們先理一下思路。我們希望實現

    1. 將globalData響應式化。
    2. 收集每個頁面和組件data和globalData中對應的屬性和更新視圖的方法。
    3. 修改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多行代碼),幾乎以零侵入的方式完成,并且實現了功能分離,具有一定的可擴展性。

    時間倉促,文中肯定會有一些不夠嚴謹的地方,歡迎大家指正和討論。

    感謝閱讀的你!


    Other News

    其它新聞

    閩ICP備18015456號-2