优秀的编程知识分享平台

网站首页 > 技术文章 正文

Immer.js是什么?前端 immutable 如此简单!

nanyue 2024-09-01 20:30:03 技术文章 5 ℃

家好,很高兴又见面了,我是"高级前端?进阶?",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发!

本文将给大家带来JavaScript世界中很重要的一个概念,即不变性。同时引入了一个JavaScript的库immer.js,介绍了immer.js是如何解决不可变性问题的。话不多说,直接开始。

1.什么是JavaScript 中的不变性?

JavaScript 中的不变性对于应用程序的可预测性和性能非常重要。 不变性的想法是一旦对原始对象设置了某些操作,原始对象不会改变。 但是为了执行操作,有时又确实需要可变性。 那么如何平衡对可变性的需求,同时保持原始对象、数组不变?

如果使用 Redux,请务必记住 reducer 是纯函数。 纯函数是一种仅根据输入返回特定预期结果的函数。 这意味着如果 x 是输入,它将始终返回 y。

reducer 获取前一个状态和一个动作并返回下一个状态。 为什么这很重要? 这是因为 reduce 返回一个新的状态对象而不是改变以前的状态。

下面是一段代码示例,展示了在 reducer 中不能做的事情:


// reducer直接修改了原始状态
function todos(state = [], action) {
    switch (action.type) {
        case 'MUTATION':
            state.push({
                text: action.text,
                completed: false
            })  
            return state
        default:
            return state
    }
}

在 MUTATION 操作中,使用 push 方法将对象推入数组会导致原始状态发生变化。 这使得 reducer 不纯,因为它改变了原始对象。为了使 reducer 保持纯净,需要防止串改原始对象的状态,这就是Immutable不变性的用武之地。看一下以下对象:

const User = {
    name: '高级前端进阶',
    age: 25,
    ed: {
        school: {
          name: 'A'
        }
    }
}

如果想保持不变性并更新 ed.school.name,可以通过复制该对象来实现。实现此目的的一种方法是使用对象展开。这是一个例子:

const updatedUser = {
    ...User,
  // Rest展开操作
    ed: {
        ...User.ed,
        school : {
          ...User.ed.school,
          name: 'B'
        }
    }
}

上面的代码示例创建了 User 对象的副本并使用对象解构更改了 ed.school.name。 虽然这样可以达到目的,但很难阅读。 需要深入对象树来更改属性,随着对象的增长,创建副本以保持不变性的复杂性也随之增加。

但是,如果可以改变对象不变性规则怎么办? 例如,如果可以将 JavaScript 编写成如下所示:

User.ed.school.name = 'C';

这就是 immer.js 的用武之地。

2.什么是 immer.js?

immer.js 是一个库,它允许在不破坏不变性的情况下处理不可变状态。 它是通过写时复制机制实现的。 以下是 immer.js 工作原理的三步流程:

  • 创建一个 draftState,它是对象 currentState 的临时状态
  • 将所有更改应用到此 draftState
  • 基于 draftState 返回最终状态,同时仍然保持原始对象不变。

immer.js 使用 Proxy 的特殊 JavaScript 对象来包装数据,并允许编写“mutates”包装数据的代码。 带来的好处是可以通过简单地修改数据来与数据交互,同时保留不可变数据的所有好处。

写时复制(Copy-on-write):是一种计算机程序设计领域的优化策略。核心思想是,如果有多个调用者同时请求相同资源,它们会共同获取相同的指针指向相同的资源,直到某个调用者试图修改资源的内容时,系统才会真正复制一份专用副本给该调用者,而其他调用者所见到的最初的资源仍然保持不变。优点是如果调用者没有修改该资源,就不会有副本被创建,从而共享同一份资源。

3.安装 immer.js

可以通过 npm 或 yarn 安装 immer.js。

使用 npm

npm install immer
// 安装
import produce from "immer"

使用yarn

yarn add immer
// yarn安装
import produce from "immer"

如果应用程序没有使用包管理器,则可以通过 CDN 直接将 immer.js 添加到HTML中。

<script src="https://unpkg.com/immer"></script>
//Unpkg源
<script src="https://cdn.jsdelivr.net/npm/immer"></script>
//JSDelivr源码

4.使用 immer.js

4.1 安装后,可以使用 immer.js

immer 包导出了一个名为 produce 的默认函数。正如函数名称所示,它根据对象/数组的 currentState“生成”nextState。下面是 produce 函数的定义。

produce(currentState, producer: (draftState) => void): nextState

它接受两个参数:

  • currentState:对象/数组的当前状态。
  • producer :是一个匿名函数,它接收 draftState 作为其唯一参数,可以在其上执行所有操作。

如果想使用 immmer.js 重写之前的示例,可以这样做。


import produce from "immer"
const User = {
    name: '新名称',
    age: 25,
    ed: {
        school: {
            name: 'School of Code'
        }
    }
}
// 通过produce对User进行修改
const updatedUser = produce(User, draftUser => {
    draftUser.ed.school.name = 'Layercode Academy';
})

上述方法比使用嵌套展开运算符更容易处理,可以使用传统的 JavaScript 语法复制和改变 updatedUser 对象,而不会影响源代码。下面是旧版本的复制实现:

const updatedUser = {
    ...User,
  // 嵌套展开运算符克隆对象
    ed: {
        ...User.ed,
        school : {
          ...User.ed.school,
          name: 'Layercode Academy'
        }
    }
}

还可以像处理对象一样处理数组。比如下面的例子:

import produce from "immer"
const todosArray = [
    {id: "001", done: false, body: "Take out the trash"},
    {id: "002", done: false, body: "Check Email"}
]
// produce方法修改数组
const addedTodosArray = produce(todosArray, draft => {
    draft.push({id: "003", done: false, body: "Buy bananas"})
})

4.2 useImmer hooks

如果在 React.js 应用程序中使用,则可以使用名为 useImmer 的钩子来操作功能组件内的状态。

要使用 userImmer,除了安装 immer.js 之外,还需要安装它。

npm install use-immer

安装后,可以在应用程序中使用 useImmer钩子函数,就像使用 useState 钩子函数一样。使用 useImmer,可以直接在更新状态的函数内改变状态。

import React from "react";
import { useImmer } from "use-immer";
function App() {
  const [person, updatePerson] = useImmer({
    name: "Cody"
  });
  function updateName(name) {
    updatePerson(draft => {
      draft.name = name;
    });
  }
  return (
    <div className="App">
      <h1>
        Hello {person.name}
      </h1>
      <input
        onChange={e => {
          updateName(e.target.value);
        }}
        value={person.name}
      />
    </div>
  );
}

从 useImmer 获得的 updatePerson 位于 immer.js 之上。这意味着可以直接改变 updatePerson 中的状态,就像使用 immer.js 的 produce 函数一样。

4.3 使用 immer.js 简化 Redux reducer

使用 immer.js 的柯里化 producer,可以大大简化redux reducer 逻辑。

将函数作为第一个参数传递给 produce 旨在用于柯里化。 这意味着获得了一个预绑定的producer,它只需要一个状态来从中产生新的数据。 producer 函数在 draft 中传递,任何其他参数传递给 curried 函数。

const INITIAL_STATE = {}
const byId = (state = INITIAL_STATE, action) => {
    switch (action.type) {
        case RECEIVE_PRODUCTS:
            return {
                ...state,
                ...action.products.reduce((obj, product) => {
                    obj[product.id] = product
                    return obj
                }, {})
            }
        default:
            return state
    }
}

可以使用 immer 的 curried producer 来简化这个 reducer。

import produce from "immer"
const INITIAL_STATE = {}
const byId = produce((draft, action) => {
    switch (action.type) {
        case RECEIVE_PRODUCTS:
            action.products.forEach(product => {
                draft[product.id] = product
            })
            break
    }
}, INITIAL_STATE)

draft 是原始状态的副本,action 将是这个 reducer 将接收的常规动作。您还可以提供一个初始状态 (INITIAL_STATE) 作为 produce 函数的第二个参数。 这可以避免编写 default 案例。

5 Immer基集成功能选择

为了确保 Immer 尽可能小,不需要的功能可以自由配置。 这可确保在打包到生产应用程序时,未使用的功能不会占用太多空间。可以选择加入以下功能如下:

例如,如果想在Map上使用produce,需要在应用程序启动时启用一次这个特性,具体方法如下:

// 应用入口代码
import {enableMapSet} from "immer"
enableMapSet()
import produce from "immer"
const usersById_v1 = new Map([
    ["michel", {name: "Michel Weststrate", country: "NL"}]
])
const usersById_v2 = produce(usersById_v1, draft => {
    draft.get("michel").country = "UK"
})
expect(usersById_v1.get("michel").country).toBe("NL")
expect(usersById_v2.get("michel").country).toBe("UK")

Immer 在Gzip压缩后只有~3KB左右,而启用每个插件都会增加 < 1 KB的体积。细目如下:

Import size report for immer:
┌───────────────────────┬───────────┬────────────┬───────────┐
│        (index)        │ just this │ cumulative │ increment │
├───────────────────────┼───────────┼────────────┼───────────┤
│ import * from 'immer' │   5662    │     0      │     0     │
│        produce        │   3100    │    3100    │     0     │
│       enableES5       │   3761    │    3770    │    670    │
│     enableMapSet      │   3885    │    4527    │    757    │
│     enablePatches     │   3891    │    5301    │    774    │
│   enableAllPlugins    │   5297    │    5348    │    47     │
└───────────────────────┴───────────┴────────────┴───────────┘

6.本文总结

本文主要和大家介绍immer.js这个库。因为笔者还没有在生产项目中使用、部署过immer,所以很多探索也就浅尝辄止,但是文末的参考资料提供了大量优秀文档以供学习,如果有兴趣可以自行阅读。

参考资料

https://nofluffjuststuff.com/magazine/2016/11/the_duality_of_pure_functions

https://github.com/immerjs/immer

https://immerjs.github.io/immer/

https://www.cnblogs.com/wallcwr/p/14543777.html

原文链接:https://usecsv.com/community/immerjs-immutability

原文作者:Amit Merchant

翻译:高级前端进阶(部分修改)

最近发表
标签列表