Global state

For parent-child communication, the framework has provided a feasible solution: the parent component passes parameters to the child component through prop(s) properties, and the child component sends messages to the parent component through custom events. Not between parent and child components, which can become quite cumbersome if passed through layers. The simplest direct solution is to set a global variable to be shared by multiple components, but there are some problems with using global variables directly, such as:

  • It is possible for multiple components to change variable values at the same time, making this process untraceable and debugging problems cumbersome;
  • How do I notify every component that references a global variable when its value changes?

Characteristics of the state management library

In response to these problems, several state management libraries have emerged, such as Vue’s Vuex and React’s Redux. The so-called “state” is the data model passed and referenced between different components. The state management library has three characteristics: predictable, centralized and adjustable.

A predictable

Predictability is one of the features of pure functions. It means that if state A generates state C after operation on B, the same result C can be obtained at any time and on any platform (client, server, App) as long as A and B do not change. For example, functions in the following code are unpredictable:

function getTime() {
    return new Date().getTime()
}
function getDom(id) {
    return document.getElementById(id)
}
Copy the code

GetTime () gets different values at different times, and getDom() only works on web pages, so the results are unpredictable. The following functions are predictable:

function nextDay(time) {
    return new Date(time + 1000 * 60 * 60 * 24)}function filter(a, b) {
    return a + b
}
Copy the code

centralized

Both Vuex and Redux build only a centralized state tree into which all state data is mounted as child properties.

adjustable

Adjustable refers to the ability to track and debug changes in state and usage using browser plug-ins. Vuex provides the vue. jsDevTools plug-in, and Redux also provides Redux DevTools.

Implementation principle of state management library

After understanding the characteristics of the state management library, we will analyze the source code of the two core operations of write and read.

The Vuex status is modified

Here is a simple example of code that calls increment() in mutation by executing store.mit (‘increment’) to achieve the modified state operation. So let’s look at how the commit() function works.

const store = new Vuex.Store({
    state: {
        count: 0
    },
    mutations: {
        increment(state, payload) {
            state.count += payload
        }
    }
}) 
store.commit('increment'.1) 
console.log(store.state.count)
Copy the code

The source code of the commit part is shown as follows. As can be seen from the code, the attributes of the Mutations object under the store initialization are obtained through Mutations [type] first (hereinafter referred to as “mutations”). In the example code, The value of type is increment. Because Vuex provides a module mechanism, mutations with the same name may appear in different modules, so it is saved in the form of array.

Then call the _withCommit() function, assign the current variable (_research) to true (right), and revert to the previous value after the callback. This callback function iterates over and executes mutations. Payload is the parameter passed in when the commit is called, and corresponds to the value 1 in the sample code.

Store.prototype.commit = function commit(_type, _payload, _options) {
    var this$1 = this; .var mutation = {
        type: type,
        payload: payload
    };
    var entry = this._mutations[type];...this._withCommit(function() {
        entry.forEach(function commitIterator(handler) { handler(payload); }); }); . }; Store.prototype._withCommit =function _withCommit(fn) {
    var committing = this._committing;
    this._committing = true;
    fn();
    this._committing = committing;
};
Copy the code

Vuex reads state

Vuex creates an internal Vue instance and assigns the value to the store. _VM attribute. In this instance, the data model $$state is created. Corresponds to the object {count: 1} in the sample code. The $$state property is used in mutations as well as for access through store.state.

store._vm = new Vue({
    data: {
        $$state: state
    },
    computed: computed
});
Copy the code

We then hijack the state property of the prototype object store. prototype and return vm.data.$$state when store.state is read. That way, when you modify it via mutations, it returns the latest value instantly.

Object.defineProperties(Store.prototype, prototypeAccessors$1);
prototypeAccessors$1.state.get = function() {
    return this._vm._data.$$state
};
Copy the code

Modify state in Redux

The following is a simple example of Redux code, from which you can see that the status update is triggered by the store.dispatch() function and the current status information is retrieved by the store.getState() function.

function counter(state = 0, action) {
    switch (action.type) {
    case 'INCREMENT':
        return state + 1
    case 'DECREMENT':
        return state - 1
    default:
        return state
    }
}
let store = createStore(counter) 
store.subscribe(() = >console.log(store.getState())) 
store.dispatch({
    type: 'INCREMENT'
}) 
store.dispatch({
    type: 'INCREMENT'
}) 
store.dispatch({
    type: 'DECREMENT'
})
Copy the code

The dispatch() function is used to distribute actions and can be thought of as a method to trigger data updates. Its implementation is very simple, part of the source code is as follows:

function dispatch(action) {...
    try {
        isDispatching = true;
        currentState = currentReducer(currentState, action);
    } finally {
        isDispatching = false;
    }...
    return action;
}
Copy the code

The dispatch() function calls the currentReducer() function, which corresponds to the counter() function in the sample code, with the currentState currentState and our defined action as arguments.

Read state in Redux

The code for the getState() function is simple to implement, first checking whether it is a dispatch state, then throwing an error if it is, otherwise returning currentState, whose value will have been updated at dispatch().

function getState() {
    if (isDispatching) {
        throw new Error('You may not call store.getState() while the reducer is executing. ' + 'The reducer has already received the state as an argument. ' + 'Pass it down from the top reducer instead of reading it from the store.');
    }
    return currentState;
}
Copy the code

Communication modes of other components

The state management library is not the only way to transfer data across components. Here are two ways to communicate across components, taking Vue as an example.

Global context

In Vue, a set of apis are provided to address communication between ancestor and descendant components, namely provide and Inject. Provide can specify data or methods that we want to provide to descendant components in ancestor components, while in any descendant component we can use Inject to receive data or methods provided by provide.

Event listeners

Event listener is to use the event mechanism of the component library itself to set up a global event proxy, which is responsible for transferring data to each component.

Here is a simple example. Create a Vue instance eventBus and inject it into the Vue component via Object.defineProperty so that the Vue instance can be accessed from within the component via this.$bus. When any of the component buttons are clicked, the new status is passed in via the event bubble this.$bus.$emit, and the other components listen on the event to get the latest status.

<div id="app"> 
 <button v-on:click="click()">{{this.count}}</button> 
 <button-counter></button-counter> 
 <button-counter></button-counter> 
 <button-counter></button-counter> 
</div>
<script>
var EventBus = new Vue();
Object.defineProperties(Vue.prototype, {
  $bus: {
    get: function() {
      return EventBus
    }
  }
}) 
 Vue.component('button-counter', {
  mounted() {
    this.$bus.$on('count', c = >this.count = c)
  },
  data() {
    return {
      count: 0}},methods: {
    click() {
      this.$bus.$emit('count'.this.count + 1)}},template: '<button v-on:click="click">You clicked me {{ this.count }} times. </button>'
}) 
const app = new Vue({
  el: '#app'.data: {
    count: 0
  },
  mounted() {
    this.$bus.$on('count', c = >this.count = c)
  },
  methods: {
    click() {
      this.$bus.$emit('count'.this.count + 1)}}})</script>
Copy the code

conclusion

This article describes three different ways to communicate across components. The communication parties do not belong to parent-child components, that is, there is no direct dependency/reference relationship. Therefore, data is transmitted through “third parties”, including the event mechanism or global context provided by Vue and React libraries, as well as the state management libraries developed for them.

Vuex and Redux, the most common global state management libraries, are analyzed in depth to understand their implementation principle. Vuex creates an instance of Vue internally and uses the data model of this instance to do status updates; Redux uses pure functions with no side effects to generate immutable data.

Component libraries by default provide a global context approach to cross-component communication. They are lightweight and suitable for use in small Web applications, with the disadvantage of being difficult to track debug state changes. The event-listening approach can also be implemented without relying on additional third-party libraries, but requires manually triggering view updates within the component when an event change is heard.


Public account “Grace Front end”

For every aspiring “front-end ER” to lead the way, build dream aspirations.

Just to be an excellent front end Engineer!