3
头图

This is the 104th original without water. If you want to get more original good articles, please search the official and follow us~ This article first appeared on the front-end blog of Zheng Caiyun: 160ff7dc04ea3f 15 minutes to learn Immutable

1. What is Immutable?

Immutable Data is data that cannot be changed once it is created. Any modification or addition or deletion of the Immutable object will return a new Immutable object. The main principle is to use the Persistent Data Structure (persistent data structure), that is, after each modification, we will get a new version, and the old version can be kept intact, that is, when using old data to create new data, we must ensure that the old data At the same time available and unchanged. At the same time, in order to avoid the performance loss caused by deepCopy copying all nodes, Immutable uses Structural Sharing, that is, for the parts that have not been modified in this operation, we can directly copy the corresponding old nodes. This is actually It is structure sharing.

2. What are the advantages of Immutable?

2.1 Reduce complexity and avoid side effects

In Javascript, objects are all reference types. In the scenario of passing data by reference, there will be multiple variables pointing to the same memory address, which will cause uncontrollable side effects, as shown in the following code:

let obj1 = { name: '张三' };
let obj2 = obj1;
obj2.name = '李四';
 
console.log(obj1.name); // 李四

After using Immutable:

import { Map } from 'immutable';
let obj1 = Map({ name: '张三'});
let obj2 = obj1;
obj2.set({name:'李四'});
console.log(obj1.get('name'));  // 张三

When we use Immutable to reduce the complexity of Javascript objects, our state becomes predictable.

2.2 Save memory

Immutable uses a structure sharing mechanism, so it will reuse memory as much as possible.

import { Map } from 'immutable';
let obj1 = Map({
  name: 'zcy',
  filter: Map({age:6})
});
let obj2 = obj1.set('name','zcygov');
console.log(obj1.get('filter') === obj2.get('filter')); // true
// 上面 obj1 和 obj2 共享了没有变化的 filter 属性

2.3 Convenient backtracking

Immutable creates a new object every time it is modified, and the object remains unchanged, then the changed record can be saved, and the state of the application becomes controllable and traceable, which facilitates the realization of the undo and redo functions. Please see the following code example :

import { Map } from 'immutable';
let historyIndex = 0;
let history = [Map({ name: 'zcy' })];
function operation(fn) {
  history = history.slice(0, historyIndex + 1);
  let newVersion = fn(history[historyIndex]);
  // 将新版本追加到历史列表中
  history.push(newVersion);
  // 记录索引,historyIndex 决定我们是否有撤销和重做
  historyIndex++;
}
function changeHeight(height) {
  operation(function(data) {
    return data.set('height', height);
  });
}
// 判断是否有重做
let hasRedo = historyIndex !== history.length - 1;
// 判断是否有撤销
let hasUndo = historyIndex !== 0; 

2.4 Functional programming

Immutable itself is a concept in functional programming, and pure functional programming is more suitable for front-end development than object-oriented. Because as long as the input is consistent, the output must be consistent, so the developed components are easier to debug and assemble.

2.5 Rich API

Immutable implements a complete set of Persistent Data Structure and provides many easy-to-use data types. Like Collection , List , Map , Set , Record , Seq , as well as a series of operations their methods, including sort, filter, data packets, reverse, flatten and create a subset of the other methods, please refer to the specific API official document

3. How to use Immutable in react

We all know that updating the parent component in React will cause the child component to re-render. When we pass in the component's props and state only one layer, we can directly use React.PureComponent , it will automatically help us to make a shallow comparison, and thus control The return value of shouldComponentUpdate.

However, when more than one layer of props or state is passed in, or the types of Array and Object are passed in, the shallow comparison becomes invalid. Of course, we can also in shouldComponentUpdate() use use deepCopy and deepCompare to avoid unnecessary render() , but deepCopy and deepCompare are generally very time consuming performance. At this time we need Immutable .

The following example is optimized by way of shallow comparison:

import React, { Component } from 'react';
import ReactDOM from 'react-dom';
class Counter extends Component {
      state = { counter: { number: 0 } }
      handleClick = () => {
          let amount = this.amount.value ? Number(this.amount.value) : 0;
          this.state.counter.number = this.state.counter.number + amount;
          this.setState(this.state);
      }
      // 通过浅比较判断是否需要刷新组件
      // 浅比较要求每次修改的时候都通过深度克隆每次都产生一个新对象
      shouldComponentUpdate(nextProps, nextState) {
          for (const key in nextState) {
              if (this.State[key] !== nextState[key]) {
                  return true;
              }
          }
          return false;
      }
  }
  render() {
      console.log('render');
      return (
          <div>
              <p>{this.state.number}</p>
              <input ref={input => this.amount = input} />
              <button onClick={this.handleClick}>+</button>
          </div>
      )
  }
}
ReactDOM.render(
    <Caculator />,
    document.getElementById('root')
)

It is also possible to judge whether the values of the two states are equal by deep comparison. If this is done, the performance is very low.

shouldComponentUpdate(nextProps, prevState) {
// 通过 lodash 中 isEqual 深度比较方法判断两个值是否相同
return !_.isEqual(prevState, this.state);
}

Immutable provides a concise and efficient method to determine whether the data has changed. You only need to === and is to know whether you need to execute render() . This operation costs almost zero, so performance can be greatly improved. The modified shouldComponentUpdate is like this:

import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { is, Map } from 'immutable';
class Caculator extends Component {
    state = {
        counter: Map({ number: 0 })
    }
    handleClick = () => {
        let amount = this.amount.value ? Number(this.amount.value) : 0;
        let counter = this.state.counter.update('number', val => val + amount);
        this.setState({counter});
    }
    shouldComponentUpdate(nextProps = {}, nextState = {}) {
        if (Object.keys(this.state).length !== Object.keys(nextState).length) {
            return true;
        }
        // 使用 immutable.is 来进行两个对象的比较
        for (const key in nextState) {
            if (!is(this.state[key], nextState[key])) {
                return true;
            }
        }
        return false;
    }
    render() {
        return (
            <div>
                <p>{this.state.counter.get('number')}</p>
                <input ref={input => this.amount = input} />
                <button onClick={this.handleClick}>+</button>
            </div>
        )
    }
}
ReactDOM.render(
    <Caculator />,
    document.getElementById('root')
)

Immutable.is hashCode or valueOf (for JavaScript objects) of two objects. Because immutable uses the Trie data structure for storage, as long as the hashCode two objects are equal, the value is the same. Such an algorithm avoids deep traversal comparison and has very good performance.
After using Immutable, as shown in the figure below, when the state of the red node changes, all nodes in the tree will not be rendered, but only the green part of the figure will be rendered:

(Picture quoted from: Immutable Detailed Explanation and Practice React)

So using Immutable.is can reduce the repeated rendering of React and improve performance.

4. Immutable combined with Redux

The following is a small example of numerical accumulation used by Immutable in combination with Redux:

import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types'
import { createStore, applyMiddleware } from 'redux'
import { Provider, connect } from 'react-redux'
import immutable, { is, Map } from 'immutable';
import PureComponent from './PureComponent';
const ADD = 'ADD';
// 初始化数据时,使用 Map 保证数据不会被轻易修改
const initState = Map({ number: 0 });
function counter(state = initState, action) {
    switch (action.type) {
        case ADD:
            // 返回数据时采用 update 更新对象数据
            return state.update('number', (value) => value + action.payload);
        default:
            return state
    }
}
const store = createStore(counter);
class Caculator extends PureComponent {
    render() {
        return (
            <div>
                <p>{this.props.number}</p>
                <input ref={input => this.amount = input} />
                <button onClick={() => this.props.add(this.amount.value ? Number(this.amount.value) : 0)}>+</button>
            </div>
        )
    }
}
let actions = {
    add(payload) {
        return { type: ADD, payload }
    }
}
const ConnectedCaculator = connect(
    state => ({ number: state.get('number') }),
    actions
)(Caculator)
ReactDOM.render(
    <Provider store={store}><ConnectedCaculator /></Provider>,
    document.getElementById('root')
)

However, due to the built-in Redux combineReducers and reducer is initialState are native Object object, and can not be used with native Immutable, of course, we can rewrite combineReducers achieve effects compatible manner, as shown in the following code:

// 重写 redux 中的 combineReducers
function combineReducers(reducers) {
// initialState 初始化为一个 Immutable Map对象
return function (state = Map(), action) {
       let newState = Map();
       for (let key in reducers) {
            newState = newState.set(key, reducers[key](state.get(key), action));
       }
       return newState;
   }
}
let reducers = combineReducers({
   counter
});
const ConnectedCaculator = connect(
   state => {
       return ({ number: state.getIn(['counter', 'number']) })
   },
   actions
)(Caculator)

You can also use the redux-immutable middleware to achieve the use of redux and Immutable. For applications using Redux, your entire state tree should be an Immutable.JS object, and you don’t need to use ordinary JavaScript objects. .

5. Points to note when using Immutable

  • Don't mix ordinary JS objects and Immutable objects (don't treat Immutable objects as attributes of Js objects, or vice versa).
  • Treat the entire Reudx state tree as an Immutable object.
  • In addition to display components, Immutable objects should be used elsewhere (to improve efficiency, and display components are pure components and should not be used).
  • Use the toJS method sparingly (this method is very performance-consuming, it will deeply traverse the data and convert it into JS objects).
  • Your Selector should always return an Immutable object (that is, mapStateToProps, because react-redux uses a shallow comparison to determine whether to re-redering, and using toJs, a new object will be returned every time, that is, the reference is different).

6. Summary

In reality, there are many ways to optimize our React application, such as lazy loading of components, using serviceWorks to cache application state, using SSR, etc., but before considering optimization, it is best to understand the working principle of React components, understand the Diff algorithm, and understand these Only after the concept can we better target and optimize our application.

If there is something wrong in the article, please correct me.

Reference link:

Immutable Detailed Explanation and Practice in React

Immutable Data Structures and JavaScript

Immutable official document

Open source works

  • Front-end tabloid of Zheng Caiyun

open source address www.zoo.team/openweekly/ (WeChat exchange group on the homepage of the tabloid official website)

Recruitment

The ZooTeam front-end team (ZooTeam), a young, passionate and creative front-end team, belongs to the product R&D department of Zheng Caiyun, and the Base is located in picturesque Hangzhou. The team now has more than 40 front-end partners with an average age of 27 years old. Nearly 30% are full-stack engineers, a proper youth storm troupe. The membership consists of not only “veteran” soldiers from Ali and Netease, as well as newcomers from Zhejiang University, University of Science and Technology of China, Hangzhou Electric Power and other schools. In addition to the daily business docking, the team also conducts technical exploration and actual combat in the material system, engineering platform, building platform, performance experience, cloud application, data analysis and visualization, and promotes and implements a series of internal technical products. Explore the new boundaries of the front-end technology system.

If you want to change that you have been tossed by things, hope to start to toss things; if you want to change and have been warned, you need more ideas, but you can’t break the game; if you want to change you have the ability to make that result, but you don’t need you; if If you want to change what you want to accomplish, you need a team to support it, but there is no position for you to lead people; if you want to change the established rhythm, it will be "5 years of work time and 3 years of work experience"; if you want to change the original The comprehension is good, but there is always the ambiguity of the window paper... If you believe in the power of belief, believe that ordinary people can achieve extraordinary things, believe that you can meet a better self. If you want to participate in the process of business take-off, and personally promote the growth of a front-end team with in-depth business understanding, complete technical system, technology to create value, and influence spillover, I think we should talk. Anytime, waiting for you to write something, send it to ZooTeam@cai-inc.com


政采云前端团队
3.8k 声望4k 粉丝

Z 是政采云拼音首字母,oo 是无穷的符号,结合 Zoo 有生物圈的含义。寄望我们的前端 ZooTeam 团队,不论是人才梯队,还是技术体系,都能各面兼备,成长为一个生态,卓越且持续卓越。