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:
- 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).
- 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.