hawx1993 / tech-blog

📦My personal tech blog,not regularly update
http://sf.gg/u/trigkit4/articles
339 stars 30 forks source link

Immutuable data和函数式在React中的应用 #20

Open hawx1993 opened 6 years ago

hawx1993 commented 6 years ago

什么是Immutable Data

Immutable Data 顾名思义就是一旦创建就不能再被更改的数据。在js中实现数据不可变,有两个方法:

但是这两种方法都是shallow处理,遇到嵌套深的结构就需要递归处理,深度拷贝的坏处很明显,即对象越复杂,性能开销越大。在JavaScript中,对象默认是可变的。当你复制一个对象时,JavaScript不得不复制每一个属性来保证这两个对象相互独立。当数据量及其庞大的时候,这种性能的瓶颈就显而易见了。

Immutable data 及其原理

Immutable 实现的原理是 Persistent Data Structure(持久化数据结构),也就是使用旧数据创建新数据时,要保证旧数据同时可用且不变。同时为了避免 deepCopy 把所有节点都复制一遍带来的性能损耗,Immutable 使用了 Structural Sharing(结构共享),即如果对象树中一个节点发生变化,只修改这个节点和受它影响的父节点,其它节点则进行共享。

1.structural sharing 结构共享

当我们对一个Immutable对象进行操作的时候,ImmutableJS基于哈希映射树(hash map tries)和vector map tries,只clone该节点以及它的祖先节点,其他保持不变,这样可以共享相同的部分,大大提高性能。

var obj = {
  count: 1,
  list: [1, 2, 3, 4, 5]
}
var map1 = Immutable.fromJS(obj);
var map2 = map1.set('count', 2);

console.log(map1.list === map2.list); // true

change

在React.js中使用Immutable Data

当我们说一个 React componentpure render,是指它的 render functionpure function。 Pure render 的好处之一就是可以在 shouldComponentUpdate 中做性能优化。最常見的做法就是用 shallow equal 检查 props 和 state 有无改变,有改变才 update component

而React 中自带的PureRenderMixin 也只是简单的浅比较,不能用于深层比较:

var ReactComponentWithPureRenderMixin = {
  shouldComponentUpdate: function(nextProps, nextState) {
    return shallowCompare(this, nextProps, nextState);
  },
};

function shallowCompare(instance, nextProps, nextState) {
  return (
    !shallowEqual(instance.props, nextProps) ||
    !shallowEqual(instance.state, nextState)
  );
}

Immutable 可以给 React 应用带来数十倍的性能提升,数据的不可变性使得追踪变化的开销变小,当state更新时,如果数据没变,React也会去做virtual dom的diff,这就产生了浪费。

然而,shouldComponentUpdate也只能进行shadow compare,

// 最简单的实现: 
shouldComponentUpdate (nextProps) { 
    return this.props.value !== nextProps.value; 
}

当比较的值是数组或者对象时,这种方式就不work了,如果数据是 Immutable Data 的话,那么数据发生变化就会生成新的对象,开发者只需要检查对象应用是否发生变化即可。

Immutable 则提供了简洁高效的判断数据是否变化的方法,只需 === 和 is 比较就能知道是否需要执行 render(),而这个操作几乎 0 成本,所以可以极大提高性能。

import { is } from 'immutable';

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

  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 (thisState[key] !== nextState[key] || !is(thisState[key], nextState[key])) {
      return true;
    }
  }
  return false;
}

函数式编程

Immutable 本身就是函数式编程中的概念,纯函数式编程比面向对象更适用于前端开发。因为只要输入一致,输出必然一致,这样开发的组件更易于调试和组装。通常对于非常简单的组件,我们通常可以使用函数组件:

//函数式声明组件
const Avatar = (props) => {
  return <img src={props.url} />;
}

但是函数组件也是一个 React 组件,当使用时,也会在内部调用 componentWillMountcomponentDidMountcomponentWillUnmount 等生命周期函数。

为了提高性能,我们可以将其作为函数调用,而不是React组件调用。

 ReactDOM.render(
   <div>
-    <Avatar url={avatarUrl} />   // <--- 作为 React 组件使用
+    {Avatar({ url: avatarUrl })} // <--- 作为 JavaScript 函数使用
     <div>{commentBody}</div>
   </div>,
   mountNode
 );

改成函数调用后,没有生成React.createElement,也就没有了 React 组件的生命周期函数。

函数式编程的几大概念:

1.函数是一等公民 2.数据是不可变的 3.强制使用纯函数(没有任何副作用,输出完全由输入决定 4.函数只接受一个参数(科里化 5.函数 无状态

Immutable 可以让代码更容易维护,在js操作数组和对象的原生方法中,很容易违反这种原则,例如:

const arr = [1,2,3];
arr.push(4);//push会改变原数组,这是不建议的操作

const newArr = [...arr, 4];// 建议转为使用这种方法

数组中会改变原数组的方法有:

也不要直接去修改一个对象的字段:

const user = {name: 'trigkit4'};

user.age = 24; //这样会污染user对象
const newMe = {...me, age: 24}; //这样,user不会被修改

函数只接受一个参数,也就是函数的科里化,例如在Redux中middleware的实现:

const someMiddleware = store => next => action => {
  // 实现middleware
};

Immutable API

对 Immutable 对象的任何修改或添加删除操作都会返回一个新的 Immutable 对象。

Map,无序 Iterable

import Immutable from 'immutable';
const { Map } = Immutable;
let map1 = Immutable.Map({
    a: 1,
    b: 2,
    c: 3
});
var map2 = map1.set('b', 50);//使用set更新数据,返回新的Map类型数据map2
const map3 = map1.update('a', () => (7));// 更新数据

const map4 = Map({ b: 3 });
console.log(map1.merge(map4));// Map {"a": 1, "b": 3, "c": 3}
console.log(map1.get('b'));// 2
console.log(map2.get('b')); // 50
console.log(map3);//Map { "a": 7, "b": 2, "c": 3 }

console.log(map1);//Map { "a": 1, "b": 2, "c": 3 };// map1数据依然没有变化

map2包含了更新后的数据,而map1数据依然保持不变。任何数据的修改都不影响最原始的数据,在让我们在引用数据的时候毫无后顾之忧

List

有序索引集,类似于 JavaScript 中的 Array

import Immutable from 'immutable';
const { List } = Immutable;

const arr1 = List([1, 2, 3]);
const arr2 = arr1.set(-1, 7);//set(index: number, value: T)
const arr3 = arr1.insert(1, 2);// insert(index: number, value: T)
const arr4 = arr1.clear();

console.log(arr1);// List [1, 2, 3 ]
console.log(arr1.size);//3
console.log(arr2);// List [1, 2, 7 ]
console.log(arr3);// List [1,2,2,3]
console.log(arr4);// List []

set

无序列表,且不能重复。类似于ES6的Set

import Immutable from 'immutable';
const { Set } = Immutable;

const set1 = Set([1, 2, 3]);
const set2 = set1.add(1).add(5);
const set3 = set1.delete(3);
const set4 = Set([2, 3, 4, 5, 6]);

console.log(set1);// Set { 1, 2, 3 }
console.log(set2);// Set { 1, 2, 3, 5}
console.log(set3);// Set {1, 2}
console.log(set4);// Set{2,3,4,5,6}
console.log(set1.intersect(set4));// 取交集 Set {3,2}
console.log(set1.subtract(set4));// 取差集 Set { 1 }

is

Object.is()类似,都是对值的比较

import Immutable from 'immutable';
const { is } = Immutable;

console.log(is([1,2,3],[1,2,3]));//false
console.log(is('hello','hello'));//true
console.log(is(0,'0'));//false
console.log(is({name: 'react'},{name: 'react'}));//false

let map1 = Immutable.Map({a:1, b:1, c:1});
let map2 = Immutable.Map({a:1, b:1, c:1});
console.log(is(map1,map2));// true

实例

React推荐将初始state设为Immutable:

import React from 'react'
import { List, Map } from 'immutable';

class ImmutableItem extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      data: Map({ count: 0, items: List() })
    }
    this.handleCountClick = this.handleCountClick.bind(this)
    this.handleAddItemClick = this.handleAddItemClick.bind(this)
  }
  handleCountClick() {
    this.setState(({ data }) => ({
      data: data.update('count', v => v + 1)// 提供一个返回状态更新的函数,用新的不可变数据更新React 状态
    }))
  }
  handleAddItemClick() {
    this.setState(({ data }) => ({
      data: data.update('items', list => list.push(data.get('count')))
    }));
  }
  render() {
    const data = this.state.data;
    return (
      <div>
        <button onClick={this.handleCountClick}>Add to count</button>
        <button onClick={this.handleAddItemClick}>Save count</button>
        <div>
          Count: {data.get('count')}
        </div>
        Saved counts:
        <ul>
          {data.get('items').map((item, index) =>
            <li key={index}>Saved: {item}</li>
          )}
        </ul>
      </div>
    );
  }
}
export default ImmutableItem