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

小何何同學發表於2020-04-07

哈嘍,今天我們聊聊小程式的狀態管理~(有這玩意嗎?)

我們要實現什麼

很簡單,實現一個全域性響應式的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多行程式碼),幾乎以零侵入的方式完成,並且實現了功能分離,具有一定的可擴充套件性。

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

感謝閱讀的你!

(碼字不易,都看到這裡了,求波點贊不過分吧。 over~)

相關文章