您现在的位置: 微信小程序 > 微信小程序运营 > 经验 >

你可以零侵入式实现小程序的全局状态管理吗

来源:微信小程序 编辑:Yiyongtong.com 发布时间:2020-05-18 11:05热度:

哈喽,今天我们聊聊小程序的状态管理~(有这玩意吗)

我们要实现什么

很简单,实现一个全局响应式的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多行代码),几乎以零侵入的方式完成,并且实现了功能分离,具有一定的可扩展性。

时间仓促,文中肯定会有一些不够严谨的地方,欢迎大家指正和讨论。

感谢阅读的你!