Hello, today we talk about small program state management ~ (have this thing 🤔)

What are we going to achieve

Simply implement a globally responsive globalData, where any changes => automatically update the global corresponding view data.

And I want to do this with as little change in the original code logic as possible.

Why implement it

As anyone who has written a small program knows, state management has always been a pain point for small programs.

Since applets do not officially have a global state management mechanism, the only way to use global variables is to create an application instance by calling app () in app.js and then adding the globalData attribute. However, the globalData is not reactive, which means that changing one of its values on a page (if initialized into data) will not result in view updates, let alone global page and component instances.

The current mainstream practice

Let’s take a look at some of the most popular solutions.

We take WeStore as an example. It is a solution that covers state management, cross-page communication and other functions. The main process is to self-maintain a Store (similar to VUEX) component, inject and collect page dependencies whenever a page or component is initialized, and manually update global data when appropriate. The API provided is also concise, but requires some intrusive changes to the project’s original code if used. Example one: creating a page or component can only be done through the framework’s API. Two: call this.update() explicitly to update the view every time the global object is changed. Here is the source code

Other schemes are similar. But I didn’t want to reinvent the original project (which is lazy), so I started building wheels.

The preparatory work

Before we start, let’s get our heads together. We want to achieve

  1. Make globalData reactive.
  2. Collect the corresponding properties and methods for updating views in each page and component data and globalData.
  3. Notify all collected pages and components to update the view when modifying globalData.

There will be a publish-subscribe model involved, so if you don’t remember this, check out my previous post. (Portal: Publish subscribe mode)

Talk is cheap. Show me the code.

With all this talk, it’s time to do something.

First, we define a dispatch center Observer to collect instance dependencies of global page components so that we can notify of data updates when they are available. There is a problem, however, that collecting whole page component instances is a waste of memory and affects initial rendering (obj below). How to optimize this?

// 1.Observer.js
export default class Observer {
  constructor() {
    this.subscribers = {};
  }

  add (key, obj) { // What should the obJ stored here have?
    if (!this.subscribers[key]) this.subscribers[key] = [];
    this.subscribers[key].push(obj);
  }

  delete () { // Delete dependencies
    // this.subscribers...
  }

  notify(key, value) { // Notification update
    this.subscribers[key].forEach(item= > {
      if (item.update && typeof item.update === 'function') item.update(key, value);
    });
  }
}

Observer.globalDataObserver = new Observer(); // Create instances with static attributes (equivalent to globally unique variables)
Copy the code

A new Watcher class (obj) should be created every time the page component is initialized and the required data and methods should be passed in. So let’s finish the initial injection part first.

// 2.patcherWatcher.js
// Equivalent to mixin Page and Component lifecycle methods
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 can be thought of as setData after diff
    const data = obj.data || {};
    Don't forget to bind the this pointer when adding the watcher pass method
    this._watcher = this._watcher || new Watcher(data, updateMethod.bind(this));
    return _onLoad.apply(this.arguments);
  };
  obj.onUnload = function () {
    // Remove watcher when the page is destroyed
    this._watcher.removeObserver();
    return _onUnload.apply(this.arguments);
  };
  return prePage(obj);
};
/ /... Component is omitted below, basically the same as Page
Copy the code

Then, according to our plan, finish the Watcher part. There is a layer filter for incoming data, so we only need the properties corresponding to globalData (reactiveData) and inject the Observer at initialization.

// 3.Watcher.js
import Observer from './Observer';
const observer = Observer.globalDataObserver;
let uid = 0; // Record the unique 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 = {}; // The intersection of page data and globalData
    this.init();
  }

  init() {
    this.initReactiveData();
    this.createObserver();
  }

  initReactiveData() { // Initialize 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]); // The update is triggered for the first time
      }
    }
  }

  createObserver() { // Add a subscription
    Object.keys(this.reactiveData) props.forEach(prop= > {
      observer.add(prop, this);
    });
  }

  update(key, value) { // Define the update method in the dependencies collected by the Observer
    if (typeof this.updateFn === 'function') this.updateFn({ [key]: value });
  }

  removeObserver() { // Remove subscription by unique ID
    observer.delete(Object.keys(this.reactiveData), this.id); }}Copy the code

Finally, use Proxy to complete a general method of responsifying objects.

There is a small detail here. When changing an array, set will trigger some extra records such as length, but I won’t go into details here. If you are interested, you can see how vue3.0 handles this.

// 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; // Deep traversal
    },
    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) { // All Watcher update methods are called when an update is triggered
  Observer.globalDataObserver.notify(key, value);
}

export { reactive };
Copy the code

Last but not least, just reference it in app.js.

// app.js
require('./utils/patchWatcher');
const { reactive } = require('./utils/Reactive');

App({
  onLaunch: function (e) {
    this.globalData = reactive(this.globalData); // globalData reactivity
    // ...
  },
  // ...
  globalData: { / *... * / }
Copy the code

conclusion

To sum up, we complete the responsiveness of globalData step by step by initializing the injection of the page component => defining the Watcher class => collecting the Watcher into the Observer and triggering the update there => importing app.js globally. The result is that four new files âž•app.js3 lines of code (including comments and more than 100 lines of code) are added, which is almost complete in a zero-intrusion way, and achieve functional separation with certain scalability.

Time is short, the article will certainly have some not rigorous place, welcome everyone to correct and discuss.

Thank you for reading!

(Code word is not easy, all see here, ask wave to like not too much. Over ~)