The cause of

One idea: can Vuex be moved into a wechat mini program project to manage some of the global state of the project??

Wechat miniprograms do not provide plug-ins for global state management mechanisms such as Vuex or Redux. For some globalData, we often use app.globalData, but the data defined by app.globalData is not responsive and cannot be changed to drive page view changes.

For a programmer who likes to be lazy, this is not unbearable, the following is to take you to implement it, so that you can steal the next project, we directly to start work. (^ ^) omega

Project preparation

Next we will start, first initialize a wechat applet project, create a store directory under the root directory, and then create globalstate.js, index.js, observer.js, proxys.js four files respectively, they are the leading role this time.

The specific use

After initializing the project, let’s take a look at how the final product will be used in the project business before we rush to code.

For those familiar with Vuex, the following structure is used to define globally shared data. (a. ^ – ^ -)

Globalstate.js export default {state: {name: 'x'}, mutations: {setName(state, name) {state.name = name; } }, actions: { asyncUpdateName({commit}, name) { setTimeout(() => { commit('setName', name) }, 2000) } }, getters: { name: state => state.name } }Copy the code

Let’s take a look at the following small program code, is it also very familiar feeling?

// index.wxml <view> name: {{name}}</view> <button catchtap="setNameEvent"> </button> <button catchtap="asyncUpdateNameEvent"> </button> <button catchtap="getNameEvent"> </button>Copy the code
// index.js const app = getApp() Page(app.$store.createPage({ globalState: [' name '], / / statement of response data onLoad () {}, setNameEvent () {app.$store.com MIT (' elegantly-named setName ', 'synchronization - new name')}, AsyncUpdateNameEvent () {app.$store.dispatch('asyncUpdateName', 'async - new name ')}, getNameEvent() { console.log(app.$store.getters.name); }}))Copy the code

Design ideas

Read the above use process, do not know you have a little own idea train of thought? It is similar to Vuex, but not exactly, so let me briefly describe my own design idea.

  1. First of all, we’re atVueThe use ofVuexOn that page, we can get throughthis.$storeTo access it. But on small programs, every page ofthisAll point to the current page instance, so we can share our own design for each applet page$storeWe need to mount it on the global object, i.eapp(const app = getApp()). Where exactly is it mounted? How do I mount it? We’ll talk more about that in the code.
  2. Secondly, we are inVueProject page can be directly to{{$store.state.xxx}}or{{$store.getters.xxx}}To display global data. It’s a little bit different in the small program, the small program{{xxx}}Grammar,xxxVariables must be on the pagedataIn the definition. So this requires us to take the page before it loads$store.statethis.setDataThis is for the current pagedata“Is hijackingonLoadEvents play on it. Of course, it’s not$store.stateAll the data below will be used on the page, so you can take a declaratively to get the required data.
  3. Finally, in the$storeObjectcommitdispatchEvent, we can modify the globally shared data. But that’s not the end when we usecommitordispatchHow do you update the view after the event has modified the global data? Update views can only be passedthis.setDataThere is no relationship between modifying the data and presenting the view, so how to get the view to use it{{xxx}}Form and we callapp.commit(stateName)What about the form? It’s really about makingxxxstateNameThere’s a correlation.

Ok, these are about three steps. Below we will follow these three steps to write code kangkang.

The source code

The first step

First, we’ll write the core store/index.js file, which will be the subsequent $store object.

// store/index.js import Proxys from './proxys'; Class Store extends Proxys {constructor(Store) {constructor = globalstate.js const {state, mutations, actions, getters } = store; super({ globalState: state || {} }); This.state = object.assign ({}, state); this.mutations = Object.assign({}, mutations); this.actions = Object.assign({}, actions); This.getters = {}; getters && this.handleGetters(getters); This.mit = this.mit.bind (this); // Bind this.mit = this.mit.bind (this); THTM {String} mutationsEventName: Mutations * @param {*} value * @param {Function} callback: The callback performed after the view, which is called in the second argument of this.setData((), () => {}) */ commit(mutationsEventName, value, callback) {if(! this.mutations.hasOwnProperty(mutationsEventName)) { return console.error('[store] unknown mutations type: ' + mutationsEventName); Gradigradie [mutationsEventName](this.state, value); gradigradie [mutationsEventName](this.state, value); gradigradie [mutationsEventName](this. } /** * Execute the method in actions, asynchronous * @param {*} actionsEventName * @param {*} value * @returns a Promise by default, Then the result is the result of the return in actions */ Dispatch (actionsEventName, value) {if(! this.actions.hasOwnProperty(actionsEventName)) { return console.error('[store] unknown actions type: ' + actionsEventName); } return new Promise((resolve, reject) => { try { let actionsResult = this.actions[actionsEventName](this, value); resolve(actionsResult); }catch(err) {reject(err);} reject(err) {reject(err); } }) } handleGetters(getters) { Object.keys(getters).map(key => { Object.defineProperty(this.getters, key, { get: () => getters[key](this.state) }) }) } } export default Store;Copy the code

$store the design of the object is not difficult, mainly is the first globalState. Js (there is a mention in the specific use of the file) file defines the state/mutations/actions/getters mount up, and then define itself has the method, The commit and Dispatch methods are the main ones, and the specific procedure code is commented in detail, so I won’t go into that. Of course, it also inherits Proxys objects, which can be left out for later work.

Let’s take a look at how $store initializes mounted global objects.

// app.js
import Store from './store/index.js';
import globalState from './store/globalState';
App({
  onLaunch() {},
  $store: new Store(globalState)
})
Copy the code

With the $store object mounted, we can access it via app.$store.xxx. This completes the first step.

The second step

The second step is to hijack the onLoad event and perform this.setdata, so that the page can display global data in {{XXX}} form. This work is mainly on Proxys objects, remember! Above said $store object inherits the Proxys objects, so on the Proxys writing method, through $store. XXX is access to oh, this is the basic knowledge well, let’s code on kangkang.

// proxys.js class Proxys { constructor({ globalState }) { this.globalState = globalState; } createPage(options) { const _this = this; const { globalState = [], onLoad } = options; // Require globalState to be an array of characters only if(! (Array.isArray(globalState) && globalState.every(stateName => typeof stateName === 'string'))) { throw new TypeError('The globalState options type require Array of String'); } // Hijack onLoad options. OnLoad = function(... params) { _this.bindWatcher(this, globalState); typeof onLoad === 'function' && onLoad.apply(this, params); } onUnload options. OnUnload = function() {typeof onUnload === 'function' && onUnload. Apply (this); } delete options.globalState; return options; } /** * Hijack the globalState property and initialize the page data when the page loads * @param {Object} instance: @param {Array[string]} globalState: stateName */ bindWatcher(instance, globalState) { const instanceData = {}; Globalstate. forEach(stateName => {$store.state = $store.state); if(Object.keys(this.globalState).includes(stateName)) { instanceData[stateName] = this.globalState[stateName]; }}) // Give instance.setdata (instanceData) to the current page instance; } } export default Proxys;Copy the code

The main thing the Proxys object does is intercept the onLoad event and globalState property from the createPage method, and use the globalState property to find the global data on the $store.state. Perform the this.setData operation to initialize the page’s data data.

// index.js
const app = getApp()
Page(app.$store.createPage({
  globalState: ['name'],
  onLoad() {},
  ...
}))

Copy the code

At this point, we have completed the two steps, and the page can display and modify the global data. However, now that we have modified the globally shared data, the view has not been updated, which is the most critical third step we need to implement. (Knock on the blackboard)

Step 3 (Emphasis)

Change data –> Update view. To do this, we need to do two things:

  1. Knowing what global data the page uses is essentiallyglobalStateProperties. A dependency collection process is performed for this data, which is to register them with a subscriber (Watcher) identity, and then consolidate them into a subscriber (Dep). In fact, these subscribers are a callback function, the function is to do thingsthis.setDataOperation only).
  2. When the data changes, notify the corresponding subscriber in the subscriber to update the view, and change$store.stateAll the data will pass throughcommitHow? We can start here.

Say so much, in fact, it is a simple publish subscribe design pattern, if there are familiar with Vue source partners may be more familiar.

Here’s the code to see how to register and collect subscribers:

// proxys.js import Observer from "./observer"; class Proxys extends Observer { constructor({ globalState }) { super(); . } createPage(options) { ... // Easy onUnload unload operation const globalStateWatcher = {}; options.onLoad = function(... Params) {// do dependency collection _this.bindWatcher(this, globalState, globalStateWatcher); typeof onLoad === 'function' && onLoad.apply(this, params); } options.onunload = function() {// Unload_this.unbindWatcher (globalStateWatcher); typeof onUnload === 'function' && onUnload.apply(this); }... } bindWatcher(instance, globalState, globalStateWatcher) { const instanceData = {}; globalState.forEach(stateName => { if(Object.keys(this.globalState).includes(stateName)) { instanceData[stateName] = this.globalState[stateName]; // The callback execution time corresponds to the Notify emit notification, which uses an observer object to implement the update view globalStateWatcher[stateName] = (newValue, callback) => { instance.setData({ [stateName]: newValue }, () => { callback && callback(newValue); This.on (stateName, globalStateWatcher[stateName])}}) instance.setData(instanceData); } /** * At first we temporarily stored all globalState callbacks on globalStateWatcher, * now we need to unbind the corresponding callbacks before uninstalling the page. * @param {*} globalStateWatcher * @param {*} watcher */ unbindWatcher(globalStateWatcher) {for(let key in globalStateWatcher) { this.off(key, globalStateWatcher[key]); }} @param {String} stateName * @param {*} newValue * @param {Function} callback: */ notify(stateName, newValue, callback) {this.emit(stateName, callback); newValue, callback); } } export default Proxys;Copy the code

The main thing to do is to register and collect subscribers at the onLoad event, clean up at the onUnload event, and provide a notify method for notifying subsequent data changes.

It also inherits an Observer object that is primarily used to hold subscribers, known as the subscriber (Dep), and to register, trigger, and destroy subscribers.

// observer.js class Observer { constructor() { this.Deps = {}; } on(WatcherName, cb) { ! this.Deps[WatcherName] && (this.Deps[WatcherName] = []); this.Deps[WatcherName].push(cb); } emit(WatcherName, ... params) { if (this.Deps[WatcherName]) { this.Deps[WatcherName].forEach(cb => { cb.apply(this, params); }) } } off(WatcherName, cb) { ! this.Deps[WatcherName] && (this.Deps[WatcherName] = []); this.Deps[WatcherName].forEach((itemCb, index) => { itemCb === cb && this.Deps[WatcherName].splice(index, 1); }) } } export default Observer;Copy the code

The purpose of the Observer object is to “add, delete, check, and modify” subscribers. Its target is individual subscribers.

After registering and collecting subscribers, the final step is to notify subscribers when data changes are made. This method requires a stateName parameter, which is the key to the global data and an identifier that the subscriber stores to the subscriber, so getting it is the key.

notify(stateName, newValue, callback) { 
    this.emit(stateName, newValue, callback); 
}
Copy the code

Let’s look at the code first:

// store/index.js import Proxys from './proxys'; class Store extends Proxys { ... commit(mutationsEventName, value, callback) { if(! this.mutations.hasOwnProperty(mutationsEventName)) { return console.error('[store] unknown mutations type: ' + mutationsEventName); } this.mutations[mutationsEventName](this.state, value); // Get the stateName by mutationsEventName, The mutationsEventName convention is in the form of set + stateName camel. Let targetState = MutationSeventName.split ('set')[1]; let stateName = targetState && targetState.replace(targetState[0], targetState[0].toLowerCase()); // Make some prompts for the mutationsEventName format if(! targetState || (targetState && ! this.state.hasOwnProperty(stateName))) { console.warn('The mutationsEventName is required to conform to the (set + stateName)'); } super.notify(stateName, value, callback); } } export default Store;Copy the code

By intercepting the change in the commit method, we get the stateName in the contracted form and call notify.

When we call app.com MIT (mutatinsEventName, value, () => {}) to change the global data, there are many ways to get the stateName. Here are four ways I can think of:

  1. Is obtained in the form of a covenantstateName.mutationsIs defined asSet + stateName humpIn the form. This is the easiest way.
  2. When initializing a hijacking, keep a copystateEach time the data is modified, the data before and after the comparison, also can know which data has been modified, so as to obtainstateName. This is also easier to achieve, but is to maintain the otherstateThe data is a little tricky.
  3. To match in a regular formmutationsMethod, the internal pairstate.stateNameIs also availablestateName. This is a little more complicated.
  4. The most violent of all is simply not to do itmutationsactions, directly implement one$store.globalUpdate(stateName, value, () = {})Method, passed every time you change itstateName, and then directly callnotifyCan.

Change data -> Update view goal is achieved here.

Specific source

The entire process involves four files: globalstate.js, index.js, observer.js, and proxxy.js. The full source code for globalstate.js and observer.js is already available, and the full source code for the remaining two files is listed below.

// store/index.js import Proxys from './proxys'; class Store extends Proxys { constructor(store) { const { state, mutations, actions, getters } = store; super({ globalState: state || {} }); this.state = Object.assign({}, state); this.mutations = Object.assign({}, mutations); this.actions = Object.assign({}, actions); this.getters = {}; getters && this.handleGetters(getters); this.commit = this.commit.bind(this); } handleGetters(getters) { Object.keys(getters).map(key => { Object.defineProperty(this.getters, key, { get: () => getters[key](this.state) }) }) } commit(mutationsEventName, value, callback) { if(! this.mutations.hasOwnProperty(mutationsEventName)) { return console.error('[store] unknown mutations type: ' + mutationsEventName); } this.mutations[mutationsEventName](this.state, value); let targetState = mutationsEventName.split('set')[1]; let stateName = targetState && targetState.replace(targetState[0],targetState[0].toLowerCase()); if(! targetState || (targetState && ! this.state.hasOwnProperty(stateName))) { console.warn('The mutationsEventName is required to conform to the (set + stateName)'); } super.notify(stateName, value, callback); } dispatch(actionsEventName, value) { if(! this.actions.hasOwnProperty(actionsEventName)) { return console.error('[store] unknown actions type: ' + actionsEventName); } return new Promise((resolve, reject) => { try { let actionsResult = this.actions[actionsEventName](this, value); resolve(actionsResult); }catch(err) { reject(err); } }) } } export default Store;Copy the code
// proxys.js import Observer from "./observer"; class Proxys extends Observer { constructor({ globalState }) { super(); this.globalState = globalState; } createPage(options) { const _this = this; const { globalState = [], onLoad } = options; if(! (Array.isArray(globalState) && globalState.every(stateName => typeof stateName === 'string'))) { throw new TypeError('The globalState options type require Array of String'); } const globalStateWatcher = {}; options.onLoad = function(... params) { _this.bindWatcher(this, globalState, globalStateWatcher); typeof onLoad === 'function' && onLoad.apply(this, params); } options.onUnload = function() { _this.unbindWatcher(globalStateWatcher); typeof onUnload === 'function' && onUnload.apply(this); } delete options.globalState; return options; } bindWatcher(instance, globalState, globalStateWatcher) { const instanceData = {}; globalState.forEach(stateName => { if(Object.keys(this.globalState).includes(stateName)) { instanceData[stateName] = this.globalState[stateName]; globalStateWatcher[stateName] = (newValue, callback) => { instance.setData({ [stateName]: newValue }, () => { callback && callback(newValue); }) } this.on(stateName, globalStateWatcher[stateName]) } }) instance.setData(instanceData); } unbindWatcher(globalStateWatcher) { for(let key in globalStateWatcher) { this.off(key, globalStateWatcher[key]); } } notify(stateName, newValue, callback) { this.emit(stateName, newValue, callback); } } export default Proxys;Copy the code

At this point, this article is finished, flower flower. Of course, it is now equivalent to a one-way data binding, and Vue two-way data binding is still so long, but also enough, look at the back of the need to make up again.

I hope this article has been helpful to you and look forward to your comments if you have any questions. Same old, like + comment = you got it, favorites = you got it.