React中为什么要强调使用Immutable【深度刨析】

858 阅读3分钟

前言:我相信大部分做过React的都知道Immutable这个东西,不知不觉我们就用到这个,但很难有人彻底的讲清楚Immutable的作用,React为什么要强调Immutable,究竟Immutable给React框架带来的意义是什么?我以前刚学React的时候也有这个困惑。为了让初学着少走弯路,我们这次来深入探究一下Immutable这个概念。

一. 谈谈什么是数据的不可变性?

1.我们都知道js数据类型分两大类:基本数据类型存储在栈内存、引用类型值存储在堆内存

  • 基本数据类型: 值是不可变的,javascript中的原始值(undefined、null、布尔值、数字和字符串)是不可更改的。

  • 引用数据类型:对象或数组,引用类型是可以直接改变其值的。

    //不可变的类型 let str = "abc"; console.log(str[1]="f");
    //可变类型 let a = [1,2,3]; a[1] = 5; console.log(a[1]); // 5

所以你会发现在JS中引用类型会带来共享问题、以及数据可变性问题。

2 .React是一种函数式编程提倡使用纯函数,排斥数据共享以及可变性。 那我们继续追一下React这样设计的原因。

 React追求数据不可变性的主要的好处之一是为了加速了diff算法中reconcile的过程,React只需要检查object的reference有没有变即可确定数据有没有变。

举个例子:在使用useMemo,useState,shouldComponentUpdate  时,react只会去shallow compare props,只是去检查props的reference有没有变,如果变了,就reRender(重新渲染),简单来讲是这样的:Object.keys(prevProps).some(key => prevProps[key] !== nextProps[key]),只对比对象一层,如果是深对象,就无法判断了。

const [todos, setTodos] = useState([{o:1,a:2}]);
const onClick = () => { 
     todos[0].a = 3;
     setTodos(todos);//不能reRender
}

所以上面的做法是错的。setTodos后,往往不会reRender 。 所以一般新手都会很困惑。

正确的做法是这样,浅拷贝出来后再修改,如果是更深的对象,需要深拷贝后再修改。

const onClick = () => { 
    let list =[...todos]
    list[0].a=3
    setTodos(list);
}

简单讲就是React要求useState 和 useReducer等这些hook 在update state的时候要返回一个新的 reference。 

二. 那么我们日常开发中如何保证其不可变性呢?

1. 使用纯函数的操作

宗旨就是:保持其纯函数的特性。见如下例子:

//比如数组-增加元素
let arr1= [] 
const addArr1 =(arr,ele)=> {arr.push(ele); return arr;}//这样改变了原有数组不妥
const addArr2 = (arr,ele)=> arr1.concat(ele) // 不会改变原数组 纯函数  Object.assign //对象的操作

2.像我上面的例子根据情况使用深拷贝或浅拷贝,先copy一份,改变reference后再增删改。

 使用自己封装一个deepclone

const deepClone = (obj, hash= new WeakMap()) => {
  if (hash.has(obj)) {
    return hash.get(obj)
  }
  // 暂只考虑object、array
  const type = getDataType(obj)
  let o
  // 根据类型初始化
  if (type === ARRAY) {
    o = []
  } else if (type === OBJECT) {
    o = {}
  } else {
    // 基本类型不可变
    return obj
  }
  if (o) {
    hash.set(obj, o)
  }
  if (type === ARRAY) {
    for (let i = 0; i < obj.length; i++) {
      o.push(deepClone(obj[i], hash))
    }
    return o
  } else if (type === OBJECT) {
    for (const key in obj) {
      o[key] = deepClone(obj[key], hash)
    }
    return o
  }
}

或使用lodash等这些第三方库实现。

3. 使用facebook提供的immutable.js库

在immutableJs对对象和数组,有单独对应的数据类型Map和List 。 Map中的set,setIn,merge等操作都是返回一个新的reference的数据,像List的push, unshift等操作,也都是返回新的数据,它不可对数据进行可变修改,保持数据不可变性,才是我们的目标。

像上面的例子如果我们用immutableJs可以这么玩

import { fromJs } from 'immutable'
const [todos, setTodos] = useState(fromJs([{o:1,a:2}]));
const onClick = () => { 
  setTodos(todos.get(0).set('a',3));
}

三.总结

React中immutable的核心思想就是不改变"历史记录",函数式编程所说的"数据不可变"其实就是求你不要改变"历史记录",你可以创建新的"历史"但是不应该去改变"历史"。

但Immutable 不是 React 的核心。React 唯一需要的是知道 state 何时改变了,但 React 无法监控深层次的 state 对象变化。那如何避免程序员不小心改变了深层次的对象但 React 不知道呢?那就引导你们使用 Immutable,使得不能修改对象,只能创建新对象,这样就能保证 React 一定能监控到变化,这就Immutable的最终目的。谢谢大家的阅读。