React中使用的不可变数据

1,496 阅读4分钟

在刚接触react时,经常会使用过下面的方式来更新数据:

const [info, setInfo] = useState({
    name: 'sloth',
    age: 18
})

const deleteAgeProperty = useCallback(() => {
    setInfo(prev => {
        Reflect.deleteProperty(prev, 'age)
        return prev
    })
}, [])

const changeName = useCallback(() => {
       info.name = 'newSloth'
       setInfo(info)
}, [])

但这两种方式并没有让视图正确的更新? 探究其原因是react使用了不可变数据,那今天来与大家来聊聊React中的不可变数据。

什么是不可变数据?

不可变数据是指一旦创建了就不能够被改变的数据。不能够被改变是指当值发生改变时,重新开辟一个新的空间储存新的值,原有的值仍储存在之前的内存空间中不被改变,本质就是数据的引用地址发生了变化。

JS中数据类型分为原始数据类型和引用数据类型。所有的原始数据类型都是不可变数据类型;引用类型都是可变数据类型。

  • 原始类型数据

原始变量及它们的值储存在栈中,当把一个原始变量的值赋值给另一个原始变量时,是把值复制给另外一个变量,这两个原始变量互不影响,因此它们是不可变数据类型。

let a = 1;
let b = a;
a = 2;
console.log(b) // 1

image.png

  • 引用类型数据

引用类型数据是将变量的名称储存在栈中,值储存在堆中,通过一个指针将栈空间的变量名指向储存在堆中的实际对象。当把引用对象传递给另一个变量时,复制的其实是指向实际对象的指针,此时两者指向的是同一个数据。

let obj = { age: 18 };
let objCopy = obj;

image.png 可以看到,obj和objCopy指向堆中的同一个引用地址。当执行obj.age = 3时,不会改变指针指向的地址,而是直接在原来的空间内进行属性修改,所以引用类型的值是可变数据。

image.png

但直接设置obj = {age: 20}时,相当于给obj重新赋值,就会在堆中创建一个新对象,并把obj的指针指向这个新对象:

image.png

在这种情况下,设置obj.age = 30时,objCopy就不会再被改变,因为他们的指针指向了堆中不同的对象。

在文章开头说到,React中使用了数据不可变。info变量是一个引用类型数据,当更新引用类型的变量时,通过info.name = 'xxx'Reflect.deleteProperty的方式并不会引起info变量指向的引用地址发生改变。所以需要创建一个新对象,并通过info=的方式让其指针指向堆中的创建的这个新对象,在新对象中对需要修改的属性赋值,修改如下:

const deleteAgeProperty = useCallback(() => {
    setInfo(prev => {
        const curInfo = { ...prev }
        Reflect.deleteProperty(curInfo, 'age)
        return curInfo
    })
}, [])

const changeName = useCallback(() => {
       setInfo(prev => ({ ...prev, name: 'newSloth' }))
}, [])

此时视图就可以正确的更新了。

不可变数据的优点

  • 在JS中,能够快速的比较引用类型的数据是否发生变化:每次更新数据对象都指向了一个新的引用地址,所以只需要比较引用地址是否相等即可判断数据是否发生了变化;
  • 可回溯,一份数据的多个版本能够被保存下来:常用于撤销操作;

React为什么采用了不可变数据?

使用不可变数据的主要原因之一是为了更加简单的判断数据是否发生变化

以useMemo为例,我们知道useMemo只有在依赖项发生改变的时候才会更新值,从源码来看下是如何比较其依赖项是否发生变化的:

  function updateMemo (nextCreate, deps) {
    // ...  
    // 更新前后依赖项是否相等
    if (areHookInputsEqual(nextDeps, prevDeps)) {
      return prevState[0];
    }
  }
  
  function areHookInputsEqual (nextDeps, prevDeps) {
    for (var i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
        // Object.js 如果不相等返回false
      if (objectIs(nextDeps[i], prevDeps[i])) {
        continue;
      }
      return false;
    }
    return true;
  }

从代码来看,比较依赖使用的是Object.is方法,这个方法针对引用类型的数据,只有当引用地址发生了改变才会判断为数据改变,否则为不改变。因此当我们需要更新hook的状态时,我们可以通过浅克隆的方式来更新数据,让数据对象指向一个新的引用地址,从而就可以正确的更新试图了。

这种简单的比较方式加速了比较的过程,因为react在每次更新时,都会从根节点开始向下遍历,检查数据是否更新。一有更新就重复这个遍历过程,比较的时间缩短了,更新所需要的时间也就间接缩短了。

在react中更加优雅的更新数据的方式

在实际项目中,用到的数据结构可能非常复杂,比如下面这样:

const [info, setInfo] = useState({
    lisi: {
        age: 3,尽可能尽可能
        work: {
            post: 'xxx',
            company: 'xxx'
        }
    }
})

这时候如果需要更新lisi的company信息,需要下面这样写:

setInfo(prev => ({
    ...prev,
    list: {
        ...prev[lisi],
        work: {
            ...prev[lisi].work,
            company: 'yyy'
        }
    }
}))

这样无疑是很复杂的,使用Immer库可以帮我们解决这个问题:

import { useImmer } from 'use-immer';

 const [info, setInfo] = useImmer({
    lisi: {
      age: 3,
      work: {
        post: 'xxx',
        company: 'xxx'
      }
    }
  })

  const changework = useCallback(() => {
    setInfo(draft => {
      draft.lisi.work.company = 'yyy'
    })
  }, [])

以上就是个人对于不可变数据及React为什么使用不可变数据的一些学习,如有错误请指出。