大家好,很高兴又见面了,我是"高级前端?进阶?",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发!
本文将给大家带来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
翻译:高级前端进阶(部分修改)