广告位联系
返回顶部
分享到

Immer功能最佳实践示例教程

JavaScript 来源:互联网 作者:佚名 发布时间:2022-10-31 22:20:39 人浏览
摘要

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

一、前言

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。这意味着你可以通过简单地修改数据来与数据交互,同时保留不可变数据的所有好处。

一个简单的比较示例

1

2

3

4

5

6

7

8

9

10

const baseState = [

  {

    title: 'Learn TypeScript',

    done: true,

  },

  {

    title: 'Try Immer',

    done: false,

  },

];

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

不使用 Immer

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

1

2

3

4

5

6

7

8

9

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  将负责所有必要的复制,并通过冻结数据来防止未来的意外修改。

1

2

3

4

5

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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

const someReducer = (state, action) => {

  return {

    ...state,

    first: {

      ...state.first,

      second: {

        ...state.first.second,

        third: {

          ...state.first.second.third,

          value: action,

        },

      },

    },

  };

};

使用 Immer

1

2

3

const someReducer = (state, action) => {

  state.first.second.third.value = action;

};

好处

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

更新模式

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

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

更新对象

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

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;

});

更新数组

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

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);

});

嵌套数据结构

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

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。

例子:

1

2

3

4

5

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

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

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

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

1

2

3

4

5

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 内部,您也可以通过使用 original 或 current 函数来选择退出 Immer 的某些部分逻辑,并对纯 JavaScript 对象执行一些操作。

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

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

将 produce 拉到尽可能远的地方

始终尝试将 produce “向上”拉动,例如 for (let x of y) produce(base, d => d.push(x)) 比 produce(base, d => { for (let x of y) ) d.push(x)}) 慢得多

六、陷阱

不要重新分配 recipe 参数

永远不要重新分配 draft 参数(例如:draft = myNewState)。相反,要么修改 draft,要么返回新状态。

Immer 只支持单向树

Immer 假设您的状态是单向树。也就是说,任何对象都不应该在树中出现两次,也不应该有循环引用。从根到树的任何节点应该只有一条路径。

永远不要从 producer 那里显式返回 undefined

可以从 producers 返回值,但不能以这种方式返回 undefined,因为它与根本不更新 draft 没有区别!

不要修改特殊对象

Immer 不支持特殊对象 比如 window.location

只有有效的索引和长度可以在数组上改变

对于数组,只能改变数值属性和 length 属性。自定义属性不会保留在数组上。

只有来自 state 的数据会被 draft

请注意,来自闭包而不是来自基本 state 的数据将永远不会被 draft,即使数据已成为新 darft 的一部分

1

2

3

4

5

6

7

8

9

10

11

const onReceiveTodo = todo => {

  const nextTodos = produce(todos, draft => {

    draft.todos[todo.id] = todo;

    // 注意,因为 todo 来自外部,而不是 draft,所以他不会被 draft,

    // 所以下面的修改会影响原来的 todo!

    draft.todos[todo.id].done = true;

    // 上面的代码相当于

    todo.done = true;

    draft.todos[todo.id] = todo;

  });

};

始终使用嵌套 producers 的结果

支持嵌套调用 produce,但请注意 produce 将始终产生新状态,因此即使将 draft 传递给嵌套 produce,内部 produce 所做的更改也不会在传递给它的 draft 中可见,只会反映在产生的输出中。

换句话说,当使用嵌套 produce 时,您会得到 draft 的 draft,并且内部 produce 的结果会被合并回原始 draft(或返回)

错误示范:

1

2

3

4

5

6

// 嵌套的错误写法:

produce(state, draft => {

  produce(draft.user, userDraft => {

    userDraft.name += '!';

  });

});

正确示范:

1

2

3

4

5

6

// 嵌套的正确写法:

produce(state, draft => {

  draft.user = produce(draft.user, userDraft => {

    userDraft.name += '!';

  });

});

Drafts 在引用上不相等

Immer 中的 draft 对象包装在 Proxy 中,因此您不能使用 == 或 === 来测试原始对象与其 draft 之间的相等性,相反,可以使用 original:

1

2

3

4

5

6

7

8

9

const remove = produce((list, element) => {

  const index = list.indexOf(element); // 不会工作!

  const index = original(list).indexOf(element); // 用这个!

  if (index !== -1) {

    list.splice(index, 1);

  }

});

const values = [a, b, c];

remove(values, a);

如果可以的话,建议在 produce 函数之外执行比较,或者使用 .id 之类的唯一标识符属性,以避免需要使用 original。


版权声明 : 本文内容来源于互联网或用户自行发布贡献,该文观点仅代表原作者本人。本站仅提供信息存储空间服务和不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权, 违法违规的内容, 请发送邮件至2530232025#qq.cn(#换@)举报,一经查实,本站将立刻删除。

您可能感兴趣的文章 :

原文链接 : https://juejin.cn/post/7157745748832944141
    Tag :
相关文章
  • 本站所有内容来源于互联网或用户自行发布,本站仅提供信息存储空间服务,不拥有版权,不承担法律责任。如有侵犯您的权益,请您联系站长处理!
  • Copyright © 2017-2022 F11.CN All Rights Reserved. F11站长开发者网 版权所有 | 苏ICP备2022031554号-1 | 51LA统计