Zou Gongyi, a front-end engineer of Meituan Dianping, has 5 years of Web front-end development experience, and is now a member of meituan Dianping ordering team.

preface

This article focuses on immutable. Js, a library from Facebook, and how to integrate immutable into our team’s existing React + Redux mobile project.

This article is long (5000 words or so). Recommended reading time: 20 min

By reading this article, you can learn:

  • What is immutable. Js, and what problem does it solve
  • Immutable. Js features and usage apis
  • In a redux+ React project, what improvements can be made by introducing immutable
  • How to integrate immutable js into React +redux
  • Data comparison before and after integration
  • Some points to note when using immutabe.js

directory

  • An immutable. Js
    • 1.1 Pit of native JS reference types
    • 1.2 immutable. Js is introduced
      • 1.2.1 Persistent Data Structure
      • 1.2.2 Structural Sharing
      • 1.2.3 Support lazy Operation
    • 1.3 Common apis
    • 1.4 Advantages and disadvantages of immutable
  • React +redux integrate immutable. Js practice
    • 2.1 The status quo of ORDERING H5 before the introduction of IMmutable
    • 2.2 How to integrate immutableJS into a React + Redux project
      • 2.2.1 Clear integration scheme and boundary definition
      • 2.2.2 Specific integration code implementation method
    • 2.3 Comparison before and after ordering H5 project optimization
  • Some points to note in using immutable
  • 4. To summarize

An immutable. Js

1.1 Pit of native JS reference types

Consider the following two scenarios:

/ / the scene
var obj = {a:1.b: {c:2}};
func(obj);
console.log(obj)  // Output what??

2 / / scene
var obj = ={a:1};
var obj2 = obj;
obj2.a = 2;
console.log(obj.a);  / / 2
console.log(obj2.a);  / / 2Copy the code

The solution to this problem is to copy a new object with a shallow copy or a deep copy, so that the new object has a different reference address from the old object. In JS, the advantage of reference type data is that frequent manipulation of data is modified on the basis of the original object, no new object is created, so memory can be used effectively, and memory is not wasted. This feature is called mutable, but it is also a disadvantage. Too much flexibility in the complex data scenarios also creates its controllability, assuming an object used in many place, accidentally changed the data at a particular spot, other places it is hard to see how the data is changed, for a solution of this problem, generally as example, just want to copy a new object, again on the new object changes, This undoubtedly leads to more performance issues and wasted memory. To solve this problem, immutable objects are created. Each modification of an immutable object creates a new immutable object, while the old object does not change.

1.2 immutable. Js is introduced

Nowadays, there are many JS libraries that implement immutable data structures. Immutable.js is one of the mainstream ones.

Immutable. Js comes from Facebook and is one of the most popular implementations of Immutable data structures. It implements a complete persistent data structure from the ground up, sharing the structure using advanced technologies like tries. All update operations return new values, but internally the structure is shared to reduce memory footprint (and garbage collection invalidation).

Immutable. Js has three main features:

  • Persistent Data Structure
  • Structural sharing
  • Support lazy operation

Let’s take a look at these three features one by one:

1.2.1 Persistent Data Structure

Generally heard persistence, first reaction should be in the programming, data exists somewhere, need time to take out from this place use directly but said persistence is another meaning, here used to describe a data structure, the general functional programming is very common, refers to a data, when is modified, will still be able to keep the state before the change, In essence, this type of data is immutable, which means immutable. Immutable. Js provides more than ten immutable types (List, Map, Set, Seq, Collection, Range, etc.). What’s the difference between copying and creating a new object each time? It’s just as expensive. Ok, so here’s the second feature that clears up the confusion for you.

1.2.2 Structural Sharing

ImmutableJS uses the advanced tries(dictionary tree) technique to solve performance problems by sharing structures. When we operate on an IMmutable object, ImmutableJS clones the node and its ancestors, leaving everything else unchanged, so that it shares the same parts. Greatly improved performance.

Here’s an example of a little bit of a deflection trying a dictionary tree






















1.2.3 Support lazy Operation

  • Lazy operation Seq
  • Feature 1: Immutable
  • Feature 2: Lazy

This is an interesting feature. What does lazy mean? It’s hard to put into words, but let’s watch a demo, and you’ll see








1.3 Common apis

//Map() transforms a native object into a Map object.
immutable.Map({name:'danny'.age:18})

//List() transforms a List object from a native array.
immutable.List([1.2.3.4.5])

//fromJS() to immutable from native JS()
immutable.fromJS([1.2.3.4.5])    // Will native array --> List
immutable.fromJS({name:'danny'.age:18})   // Add native object --> Map

//toJS() immutable to native JS
immutableData.toJS();

// Check the List or map sizeImmutableData. The size or immutableData. The count ()// is() to judge whether two immutable objects are equal
immutable.is(imA, imB);

//merge() Object merge
var imA = immutable.fromJS({a:1.b:2});
var imA = immutable.fromJS({c:3});
var imC = imA.merge(imB);
console.log(imC.toJS())  //{a:1,b:2,c:3}

// Add, delete, change, check (all operations return the new value, does not change the original value)
var immutableData = immutable.fromJS({
    a:1.b:2C: {d:3}});var data1 = immutableData.get('a') // data1 = 1
var data2 = immutableData.getIn(['c'.'d']) // data2 = 3 getIn for deep structure access
var data3 = immutableData.set('a' , 2);   // a = 2 in data3
var data4 = immutableData.setIn(['c'.'d'].4);   //d = 4 in data4
var data5 = immutableData.update('a'.function(x){return x+4})   A = 5 in data5
var data6 = immutableData.updateIn(['c'.'d'].function(x){return x+4})   //d = 7 in data6
var data7 = immutableData.delete('a')   // A in data7 does not exist
var data8 = immutableData.deleteIn(['c'.'d'])   // The d in data8 does not existCopy the code

The above are only a part of the commonly used method, specific refer to website API: facebook. Making. IO/immutable – j… Immutablejs also has many generic underscore candy bars. Using immutableJS, it is entirely possible to remove tool libraries like Lodash or underscore from a project.

1.4 Advantages and disadvantages of immutable

Advantages:

  • Reduce the complexity of mutable
  • Save memory
  • Historical traceability (travel time) : refers to the time travel is that every moment of the values are retained, think back to which step just simply remove data, consider if the page has a cancel operation now, withdraw before the data is preserved, just need to take out, this feature is especially useful in the story or flux
  • Embrace functional programming: Immutable is a native concept of functional programming. Pure functional programming is characterized by the fact that as long as the inputs are consistent, the output is consistent, making it easier to develop components and debug than object-oriented programming

Disadvantages:

  • You need to relearn the API
  • Resource pack size increased (5000 lines)
  • Easy to confuse with native objects: Because apis are different from native objects, mixing them is error prone.

React +redux integrate immutable. Js practice

In this chapter, we introduce the performance improvement of react+ Redux projects by using immutable

2.1 The status quo of ORDERING H5 before the introduction of IMmutable

At present, react+ Redux is used in the project. Due to the continuous iteration of the project and the increase of the complexity of requirements, the state structure maintained in REDUx is getting bigger and bigger, which is no longer a simple tiled data. For example, there are three or four layers of object and array nesting in menu page state. Object and array in JS are reference types. In the process of continuous operation, the originally complex state has become uncontrollable after the action changes for many times. The result is a state change that re-renders many components whose own state has not changed. The following figure












What’s the reason?

shouldComponentUpdate

shouldComponentUpdate (nextProps, nextState) {
   returnnextProps.id ! = =this.props.id;
};Copy the code

ShouldComponentUpdate shouldComponentUpdate returns true. This component should be re-render, ShallowCompare == shallowCompare == shallowCompare == shallowCompare == shallowCompare == shallowCompare == shallowCompare == shallowCompare == shallowCompare == shallowCompare == shallowCompare == shallowCompare == shallowCompare == shallowCompare == Let’s try using shallowCompare:






The reason:






ShallowEqual source code:

function shallowEqual(objA, objB) {
  if (is(objA, objB)) {
    return true;
  }

  if (typeofobjA ! = ='object' || objA === null || typeofobjB ! = ='object' || objB === null) {
    return false;
  }

  var keysA = Object.keys(objA);
  var keysB = Object.keys(objB);

  if(keysA.length ! == keysB.length) {return false;
  }
// If the object is too deep, it cannot return the correct result
  // Test for A's keys different from B.
  for (var i = 0; i < keysA.length; i++) {
    if(! hasOwnProperty.call(objB, keysA[i]) || ! is(objA[keysA[i]], objB[keysA[i]])) {return false; }}return true;
}Copy the code

In this case, it is not possible to use deep comparisons to go through all structures every time, which is a huge performance cost. Immutable.js has a feature called hashCode, which is perfect for this scenario

2.2 How to integrate immutableJS into a React + Redux project

2.2.1 Clear integration scheme and boundary definition

First of all, it is necessary to draw a boundary between which data needs to use immutable data, which data needs to use native JS data structures, and where we need to convert each other

  • In REdux, global state must be immutable, which is undoubtedly at the heart of our use of immutable to optimize Redux
  • The component props is derived from state via redux’s connect, and immutableJS was introduced to reduce unnecessary rendering of the component shouldComponentUpdate, which is the same as the props, If the props are native JS, optimization is meaningless
  • State within a component must be immutable if it needs to be submitted to the store. Otherwise, it is not mandatory
  • Data submitted by a view to an action must be immutable
  • Action Data submitted to the Reducer must be IMmutable
  • The final processing state in the Reducer must be immutable and returned
  • Callback returned from ajax interactions with the server is wrapped uniformly and converted to IMmutable data for the first time

As you can see from these points, immutable is mandatory for almost the entire project, and native JS is used only in the few places where it interacts with external dependencies. The purpose of this method is to prevent the use of native JS and immutable in large projects, causing the coder itself to have no idea what type of data is stored in a variable. One might say that this is possible in a new project, but in an existing mature project, changing all the variables to IMMutableJS is very invasive and risky. They can make state traceable by changing state from fromJS() to immutable and then returning it from native JS via toJS() without modifying any code other than the reducer, and the cost is very small.

export default function indexReducer(state, action) {
    switch (action.type) {
    case RECEIVE_MENU:
        state = immutable.fromJS(state);   / / to immutable
        state = state.merge({a:1});
        return state.toJS()    // Go back to native JS}}Copy the code

Two questions:

  1. FromJS () and toJS() are deep immutable and native objects that require high performance overhead and should not be used (see the next section for a detailed comparison).
  2. The props and state components are still native JS, shouldComponentUpdate still can’t do depth comparison using imMutableJS

2.2.2 Specific integration code implementation method

redux-immutable

In REdux, the first step must use combineReducers to merge reducer state and initialize state. The combineReducers of Redux only supports state in native JS form. So we need to use the combineReducers provided by Redux-immutable instead of the original method

import {combineReducers} from 'redux-immutable';
import dish from './dish';
import menu from './menu';
import cart from './cart';

const rootReducer = combineReducers({
    dish,
    menu,
    cart,
});

export default rootReducer;Copy the code

The initialState in the Reducer must also be initialized to imMUTABLE

const initialState = Immutable.Map({});
export default function menu(state = initialState, action) {
    switch (action.type) {
    case SET_ERROR:
        return state.set('isError'.true); }}Copy the code

State becomes immutable, so that other files on the corresponding page need to be written accordingly

//connect
function mapStateToProps(state) {
    return {
        menuList: state.getIn(['dish'.'list']),  // Use get or getIn to get variables in state
        CartList: state.getIn(['dish'.'cartList'])}}Copy the code

Javascript variables that are native to a page need to be changed to immutable

Server-side interaction Ajax encapsulation

The front-end code uses IMMUTABLE, but the data sent by the server is still JSON, so you need to encapsulate it in Ajax and convert the data returned by the server to IMmutable

/ / pseudo code
$.ajax({
    type: 'get'.url: 'XXX'.dataType: 'json', success(res){ res = immutable.fromJS(res || {}); callback && callback(res); }, error(e) { e = immutable.fromJS(e || {}); callback && callback(e); }});Copy the code

This way, ajax returns are treated as immutable throughout the page without any confusion

shouldComponentUpdate

Top priority! ShouldComponentUpdate shouldComponentUpdate = immutable; shouldComponentUpdate = immutable; We have chosen a base class that encapsulates a layer of Component to handle shouldComponentUpdate, which inherits directly from the base class in the component

// Basecomponent.js Component base class method

import React from 'react';
import {is} from 'immutable';

class BaseComponent extends React.Component {
    constructor(props, context, updater) {
        super(props, context, updater);
    }

    shouldComponentUpdate(nextProps, nextState) {
        const thisProps = this.props || {};
        const thisState = this.state || {};
        nextState = nextState || {};
        nextProps = nextProps || {};

        if (Object.keys(thisProps).length ! = =Object.keys(nextProps).length ||
            Object.keys(thisState).length ! = =Object.keys(nextState).length) {
            return true;
        }

        for (const key in nextProps) {
            if(! is(thisProps[key], nextProps[key])) {return true; }}for (const key in nextState) {
            if(! is(thisState[key], nextState[key])) {return true; }}return false; }}export default BaseComponent;Copy the code

If a component needs to use shouldComponentUpdate in a unified wrapper, it inherits the base class directly

import BaseComponent from './BaseComponent';
class Menu extends BaseComponent {
    constructor() {
        super(a); }..................... }Copy the code

If the component doesn’t want to use wrapped methods, override shouldComponentUpdate in that component

2.3 Comparison before and after ordering H5 project optimization

Here are just a few screenshot examples to optimize the pre-search page:













Some points to note in using immutable

1. FromJS and toJS will deeply transform data, resulting in high overhead, so avoid using them as much as possible. Map() and List() are used for single-layer data conversion.

(Doing a simple comparison of fromJS and Map performance, we can see that the fromJS overhead is 4 times that of Map under the same conditions.)

2. Js is weak, but Map key must be string! (See the illustration on the official website below)

3. All additions, deletions, and changes to IMmutable variables must have an assignment to the left, because all operations do not change the original value, but only create a new variable

//javascript
var arr = [1.2.3.4];
arr.push(5);
console.log(arr) / / [1, 2, 3, 4, 5]

//immutable
var arr = immutable.fromJS([1.2.3.4])
// Incorrect usage
arr.push(5);
console.log(arr) / / [1, 2, 3, 4]
// Correct usage
arr = arr.push(5);
console.log(arr) / / [1, 2, 3, 4, 5]Copy the code

4. After imMutableJS is introduced, there should be no object array copy code (example below)

// Es6 object replication
var state = Object.assign({}, state, {
    key: value
});

/ / array replication
var newArr = [].concat([1.2.3])Copy the code

5. Obtaining the value of the deep set object does not need to do every level of nulling

//javascript
var obj = {a:1}
var res = obj.a.b.c   //error

//immutable
var immutableData=immutable.fromJS({a:1})
var res = immutableData.getIn(['a'.'b'.'c'])  //undefinedCopy the code

6. Immutable objects can be converted to json.stringify () directly, without explicitly calling toJS() manually to convert to native

7. You can use size to determine whether an object is empty

8. To view the true value of an IMmutable variable during debugging, add a breakpoint in Chrome and use the.tojs () method in Console to view it

4. To summarize

In general, immutable.js solves many of the pain points of native JS, and makes a lot of performance optimization. Immuable. js is a product launched at the same time as React, which perfectly matches the react+redux state stream processing. Redux is all about a single stream of data that can be traced, both of which are immutable. Not all react+redux scenarios should use immutable. Js is recommended for projects that are large enough and the state structure is complex enough. ShouldComponentUpdate should be handled manually for small projects.