React的Immutable特性及使用Immer优化开发体验

React的Immutable特性及使用Immer优化开发体验

  • 前言:
  • 一、为什么React强调使用Immutable
    • 1. 从使用角度出发
      • (1)官方说明
      • (2)原因
    • 2. 从原理设计需求出发
  • 二、使用 Immer 简化不可变数据的处理
    • 1. 语法如下:
    • 2. 运用immer到React中
    • 3. immer实现原理解析
  • 三、React Hooks:use-immer
    • 1. 使用use-immer
    • 2. 手写实现useImmer


前言:

  • 文章内容参考文档如下:

React官方文档、Immer官方文档、use-immer

一、为什么React强调使用Immutable

1. 从使用角度出发

(1)官方说明

state 中可以保存任意类型的 JavaScript 值,包括对象。但是,你不应该直接修改存放在 React state 中的对象。相反,当你想要更新一个对象时,你需要创建一个新的对象(或者将其拷贝一份),然后将 state 更新为此对象。

从上面我们可以知道React中强调数据是不可变的(Immutable)。

(2)原因

  • React中组件的重新render时机之一是依据状态变量的变化进行判断
  • 而React中对状态变量的更新判断是浅层的比较(shallow compare props),比如使用Object.is()
  • 如果我们直接修改一个类型为object的状态变量的属性值,那么实际该对象的引用地址没有改变所以浅比较判断为值未发生改变,所以组件也就不会实时更新

2. 从原理设计需求出发

  • Immutable 实现的原理是 Persistent Data Structure(持久化数据结构)
  • 也就是使用旧数据创建新数据时,要保证旧数据同时可用且不变。
  • 同时为了避免深拷贝把所有节点都复制一遍带来的性能损耗,Immutable 使用了结构共享的思路,即如果对象树中一个节点发生变化,只修改这个节点和受它影响的父节点,其它节点则进行共享。
  • 使用Immutable的好处:
    • 容易开发出撤销重做类似的功能,如时间旅行的实现
    • 调试时可以保证过往的日志结构不被破坏
    • 利于react内部diff算法的实现和优化

二、使用 Immer 简化不可变数据的处理

前言:Immer 是一个非常流行的库,它可以让你使用简便但可以直接修改的语法编写代码,并会帮你处理好复制的过程

安装库:npm install immer

1. 语法如下:

  • 可以使用 Immer 库中的 produce 函数修改不可变数据
  • 函数类型:produce(currentState, recipe: (draft) => void): nextState
    • currentState: 传递给 produce 的不可变状态
    • recipe: 它捕获了不可变状态应该如何更新
    • draft: 不可变状态的代理。
const baseState = [
    {
        title: "Learn TypeScript",
        done: true
    },
    {
        title: "Try Immer",
        done: false
    }
]

const nextState = produce(baseState, draft => {
    draft[1].done = true
})
console.log(Object.is(baseState,nextState)) //false

2. 运用immer到React中

  • 我们可以使用 produce函数 来处理不可变值,优化开发体验。
  • 在更新函数中配置produce函数及其相关操作即可,如:
import React, { useCallback, useState } from 'react'
import { produce } from "immer"

const index: React.FC = () => {
    const [list, setList] = useState([{ id: 1 }, { id: 2 }])
    const add = useCallback(() => {
        setList(produce(draft => {
            const id = draft[draft.length - 1].id + 1
            draft.push({ id })
        }))
    }, [])

    return (
        <>
            <div>
                <p>immer:</p>
                {list.map(item => <div key={item.id}>id:{item.id}</div>)}
                <button onClick={add}>add</button>
            </div>
        </>
    )
}

export default index

3. immer实现原理解析

  • immer 基于 Proxy 对源对象进行代理监听
  • 设置变量 copies 为map集合,用于记录源对象被修改的部分
  • 当需要修改某个值时,先浅拷贝被更改值所在的对象,然后对拷贝对象该属性值进行更新操作,并把拷贝对象添加到 copies 中
  • 最后所有操作完成后,先对根的源对象进行浅拷贝
  • 然后遍历源对象的所有属性值对拷贝对象进行赋值操作,如果某一个属性值在copies中则使用copies中的值,如果不在copies中,则复用源对象的值

注:以上仅对immer实现原理的简要分析,详细可见官方库源码

三、React Hooks:use-immer

1. 使用use-immer

  • use-immer 基于 Immer库 封装了便于React使用的Hooks供开发者配置状态变量
  • 安装:npm install use-immer
  • 基本使用方法如下:
import React, { useCallback, useState } from 'react'
import { useImmer } from 'use-immer'

const index: React.FC = () => {
    const [list, setList] = useImmer([{ id: 1 }, { id: 2 }])
    const add = useCallback(() => {
        setList(draft => {
            const id = draft[draft.length - 1].id + 1
            draft.push({ id })
        })
    }, [])

    return (
        <div>
            <p>use-immer:</p>
            {list.map(item => <div key={item.id}>id:{item.id}</div>)}
            <button onClick={add}>add</button>
        </div>
    )
}

export default index

补充:use-immer中还配有Reducer相关的Hook

2. 手写实现useImmer

  • 基于Immer库我们自己手写实现一个 useImmer 的Hook并不难
  • 主要是要对传入数据的类型做好判断,按不同类型需要进行不同操作
  • 同时我们可以使用Immer提供的 freeze() 冻结不可变值,防止用户直接修改
  • 实现源码如下:
import { useCallback, useState } from 'react'
import { produce, freeze, Draft } from "immer"

export type DraftFunction<S> = (draft: Draft<S>) => void;
export type Updater<S> = (arg: S | DraftFunction<S>) => void;
export type ImmerHook<S> = [S, Updater<S>];

export function useImmer<S = any>(initialValue: S | (() => S)): ImmerHook<S>;
export function useImmer(initialValue: any) {
    const [value, setValue] = useState(
        // 使用 freeze 深度冻结变量,表示值不可修改
        freeze(typeof initialValue === 'function' ? initialValue() : initialValue, true)
    )
    const updateValue = useCallback((updater: any) => {
        if (typeof updater === "function") {
            setValue(produce(updater))
        } else {
            updateValue(freeze(updater))
        }
    }, [])
    return [value, updateValue]
}

提示:文章到此结束,文章为个人学习记录,侵删。

你可能感兴趣的:(react,react.js,前端,前端框架,性能优化)