您现在的位置是:网站首页> 编程资料编程资料

Immer 功能最佳实践示例教程_JavaScript_

2023-05-24 386人已围观

简介 Immer 功能最佳实践示例教程_JavaScript_

一、前言

Immer  是 mobx 的作者写的一个 immutable 库,核心实现是利用 ES6 的 proxy,几乎以最小的成本实现了 js 的不可变数据结构,简单易用、体量小巧、设计巧妙,满足了我们对 JS 不可变数据结构的需求。

二、学习前提

阅读这篇文章需要以下知识储备:

  • JavaScript 基础语法
  • es6 基础语法
  • node、npm 基础知识

三、历史背景

在 js 中,处理数据一直存在一个问题:

拷贝一个值的时候,如果这个值是引用类型(比如对象、数组),直接赋值给另一个变量的时候,会把值的引用也拷贝过去,在修改新变量的过程中,旧的变量也会被一起修改掉。

要解决这个问题,通常我们不会直接赋值,而是会选择使用深拷贝,比如JSON.parse(JSON.stringify()),再比如 lodash 为我们提供的 cloneDeep 方法……

但是,深拷贝并不是十全十美的。

这个时候,immer 诞生了!

四、immer 功能介绍

基本思想是,使用 Immer,会将所有更改应用到临时  draft,它是  currentState  的代理。一旦你完成了所有的  mutations,Immer 将根据对  draft state  的  mutations  生成 nextState。这意味着你可以通过简单地修改数据来与数据交互,同时保留不可变数据的所有好处。

一个简单的比较示例

const baseState = [ { title: 'Learn TypeScript', done: true, }, { title: 'Try Immer', done: false, }, ]; 

假设我们有上述基本状态,我们需要更新第二个 todo,并添加第三个。但是,我们不想改变原始的 baseState,我们也想避免深度克隆以保留第一个 todo

不使用 Immer

如果没有 Immer,我们将不得不小心地浅拷贝每层受我们更改影响的 state 结构

const nextState = [...baseState]; // 浅拷贝数组 nextState[1] = { // 替换第一层元素 ...nextState[1], // 浅拷贝第一层元素 done: true, // 期望的更新 }; // 因为 nextState 是新拷贝的, 所以使用 push 方法是安全的, // 但是在未来的任意时间做相同的事情会违反不变性原则并且导致 bug! nextState.push({ title: 'Tweet about it' }); 

使用 Immer

使用 Immer,这个过程更加简单。我们可以利用  produce  函数,它将我们要更改的 state 作为第一个参数,对于第二个参数,我们传递一个名为 recipe 的函数,该函数传递一个  draft  参数,我们可以对其应用直接的  mutations。一旦  recipe  执行完成,这些  mutations  被记录并用于产生下一个状态。 produce  将负责所有必要的复制,并通过冻结数据来防止未来的意外修改。

import produce from 'immer'; const nextState = produce(baseState, draft => { draft[1].done = true; draft.push({ title: 'Tweet about it' }); }); 

使用 Immer 就像拥有一个私人助理。助手拿一封信(当前状态)并给您一份副本(草稿)以记录更改。完成后,助手将接受您的草稿并为您生成真正不变的最终信件(下一个状态)。

第二个示例

如果有一个层级很深的对象,你在使用 redux 的时候,想在 reducer 中修改它的某个属性,但是根据 reduce 的原则,我们不能直接修改 state,而是必须返回一个新的 state

不使用 Immer

const someReducer = (state, action) => { return { ...state, first: { ...state.first, second: { ...state.first.second, third: { ...state.first.second.third, value: action, }, }, }, }; }; 

使用 Immer

const someReducer = (state, action) => { state.first.second.third.value = action; }; 

好处

  • 遵循不可变数据范式,同时使用普通的 JavaScript 对象、数组、Sets 和 Maps。无需学习新的 API 或 "mutations patterns"!
  • 强类型,无基于字符串的路径选择器等
  • 开箱即用的结构共享
  • 开箱即用的对象冻结
  • 深度更新轻而易举
  • 样板代码减少。更少的噪音,更简洁的代码

更新模式

在 Immer 之前,使用不可变数据意味着学习所有不可变的更新模式。

为了帮助“忘记”这些模式,这里概述了如何利用内置 JavaScript API 来更新对象和集合

更新对象

import produce from 'immer'; const todosObj = { id1: { done: false, body: 'Take out the trash' }, id2: { done: false, body: 'Check Email' }, }; // 添加 const addedTodosObj = produce(todosObj, draft => { draft['id3'] = { done: false, body: 'Buy bananas' }; }); // 删除 const deletedTodosObj = produce(todosObj, draft => { delete draft['id1']; }); // 更新 const updatedTodosObj = produce(todosObj, draft => { draft['id1'].done = true; }); 

更新数组

import produce from 'immer'; const todosArray = [ { id: 'id1', done: false, body: 'Take out the trash' }, { id: 'id2', done: false, body: 'Check Email' }, ]; // 添加 const addedTodosArray = produce(todosArray, draft => { draft.push({ id: 'id3', done: false, body: 'Buy bananas' }); }); // 索引删除 const deletedTodosArray = produce(todosArray, draft => { draft.splice(3 /*索引 */, 1); }); // 索引更新 const updatedTodosArray = produce(todosArray, draft => { draft[3].done = true; }); // 索引插入 const updatedTodosArray = produce(todosArray, draft => { draft.splice(3, 0, { id: 'id3', done: false, body: 'Buy bananas' }); }); // 删除最后一个元素 const updatedTodosArray = produce(todosArray, draft => { draft.pop(); }); // 删除第一个元素 const updatedTodosArray = produce(todosArray, draft => { draft.shift(); }); // 数组开头添加元素 const addedTodosArray = produce(todosArray, draft => { draft.unshift({ id: 'id3', done: false, body: 'Buy bananas' }); }); // 根据 id 删除 const deletedTodosArray = produce(todosArray, draft => { const index = draft.findIndex(todo => todo.id === 'id1'); if (index !== -1) { draft.splice(index, 1); } }); // 根据 id 更新 const updatedTodosArray = produce(todosArray, draft => { const index = draft.findIndex(todo => todo.id === 'id1'); if (index !== -1) { draft[index].done = true; } }); // 过滤 const updatedTodosArray = produce(todosArray, draft => { // 过滤器实际上会返回一个不可变的状态,但是如果过滤器不是处于对象的顶层,这个依然很有用 return draft.filter(todo => todo.done); }); 

嵌套数据结构

import produce from 'immer'; // 复杂数据结构例子 const store = { users: new Map([ [ '17', { name: 'Michel', todos: [{ title: 'Get coffee', done: false }], }, ], ]), }; // 深度更新 const nextStore = produce(store, draft => { draft.users.get('17').todos[0].done = true; }); // 过滤 const nextStore = produce(store, draft => { const user = draft.users.get('17'); user.todos = user.todos.filter(todo => todo.done); }); 

异步 producers & createDraft

允许从 recipe 返回 Promise 对象。或者使用 async / await。这对于长时间运行的进程非常有用,只有在 Promise 链解析后才生成新对象

注意,如果 producer 是异步的,produce 本身也会返回一个 promise。

例子:

import produce from 'immer'; const user = { name: 'michel', todos: [] }; const loadedUser = await produce(user, async draft => { draft.todos = await (await fetch('http://host/' + draft.name)).json(); }); 

请注意,draft 不应从异步程序中“泄露”并存储在其他地方。异步过程完成后,draft 仍将被释放

createDraft 和 finishDraft

createDraftfinishDraft 是两个底层函数,它们对于在 immer 之上构建抽象的库非常有用。避免了为了使用 draft 始终创建函数。

相反,人们可以创建一个 draft,对其进行修改,并在未来的某个时间完成该 draft,在这种情况下,将产生下一个不可变状态。

例如,我们可以将上面的示例重写为:

import { createDraft, finishDraft } from 'immer'; const user = { name: 'michel', todos: [] }; const draft = createDraft(user); draft.todos = await (await fetch('http://host/' + draft.name)).json(); const loadedUser = finishDraft(draft); 

五、性能提示

预冻结数据

当向 Immer producer 中的状态树添加大型数据集时(例如从 JSON 端接收的数据),可以在首先添加的数据的最外层调用 freeze(json) 来浅冻结它。这将允许 Immer 更快地将新数据添加到树中,因为它将避免递归扫描和冻结新数据的需要。

可以随时选择退出

immer 在任何地方都是可选的,因此手动编写性能非常苛刻的 reducers ,并将 immer 用于所有普通的的 reducers 是非常好的。即使在 producer 内部,您也可以通过使用 originalcurrent 函数来选择退出 Immer 的某些部分逻辑,并对纯 JavaScript 对象执行一些操作。

对于性能消耗大的的搜索操作,从原始 state 读取,而不是 draft

Immer 会将您在 draft 中读取的任何内容也递归地转换为 draft。如果您对涉及大量读取操作的 draft 进行昂贵的无副作用操作,例如在非常大的数组中使用 find(Index) 查找索引,您可以通过首先进行搜索,并且只在知道索引后调用 produce 来加快速度。这样可以阻止 Immer 将在 draft 中搜索到的所有内容都进行转换。或者,使用 original(someDraft) 对 draft 的原始值执

-六神源码网