Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
hawx1993 opened this issue Dec 4, 2017 · 0 comments
Open

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

hawx1993 opened this issue Dec 4, 2017 · 0 comments

Comments

@hawx1993
Copy link
Owner

hawx1993 commented Dec 4, 2017

什么是Immutable Data

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

  • const(es6)
  • Object.freeze(es5)

但是这两种方法都是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

  • PureRenderMixin

当我们说一个 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
    当一个 React 组件的 props 和 state 发生变化时,React 会根据变化后的 props 和 state 创建一个新的 virtual DOM,然后比较新旧两个 vritual DOM 是否一致,只有当两者不同时,React 才会将 virtual DOM 渲染真实的 DOM 结点,而对 React 进行性能优化的核心就是减少渲染真实 DOM 结点的频率,间接地指出开发者应该准确判断 props 和 state 是否真正发生了变化。

然而,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];// 建议转为使用这种方法

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

  • splice(返回由被删除元素组成的数组,没有删除元素则返回空数组)
  • unshift(返回新数组)
  • shift(删除第一个元素,返回该元素的值)
  • pop(删除最后一个元素,返回该元素的值)
  • push(返回新数组长度,原数组值被改变)
  • fill(返回填充后的数组)
  • copyWithin(返回复制后的数组,长度不变,值改变)
  • reverse(返回颠倒后的元素组成的数组)
  • sort(返回排序后的数组,原数组已经被排序后的数组代替)

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

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 对象。

  • Immutable.fromJS可以将普通的js对象深层次的转化为Immutable对象 (Object=>Map,Array=>List)
  • Immutable.Map只能浅层次的转化为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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant