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

不可变数据方案之immer.js原理介绍

JavaScript 来源:互联网 作者:佚名 发布时间:2023-11-09 22:36:57 人浏览
摘要

本篇文章是JavaScript 函数式编程 学习系列第三篇 前一篇JavaScript数据类型对函数式编程的影响讲到了不可变数据的重要性,而让数据不可变的原理就是 拷贝数据。 但如果拷贝的是一个树

本篇文章是JavaScript 函数式编程 学习系列第三篇

前一篇 JavaScript数据类型对函数式编程的影响 讲到了不可变数据的重要性,而让数据不可变的原理就是 “拷贝数据”。

但如果拷贝的是一个树形结构,层次比较深,看是一个对象,但实际上里面有上百个对象,比如:

1

2

3

4

5

6

7

8

9

// 某某公司组织架构

const org = {

    name: "某某公司",

    children: [

        { name: "研发部", children: [{ name: "张三" }, { name: "李四" }] },

        { name: "产品部", children: [{ name: "王五" }] },

        // 省略 10 个部门,每个部门 10 个人

    ]

}

这个 org 数据中的 children 是 Array 类型的对象,children 里面的部门一个是一个基本对象,然后再往下又是 Array 对象 ...... ,上面结构看起来还很简单,但实际上写出来的都有了 9 个对象,如果这个组织有一百个人,至少 100 多个对象,如果为了保持数据不可变,每次修改对象,都要对整个 org 进行拷贝的话,那么操作个几十次上百次,很容易造成性能问题,要是原始的数据意外没有销毁的话,还容易造成内存泄露(这是我曾经刚出来工作一两年干过的事情,操作一个增删改查的列表页,没操作几次,浏览器就变卡了,到后面必须得重新刷新页面?????)。

因此,当数据规模大、数据拷贝行为频繁时,拷贝将会给我们的应用性能带来巨大的挑战。

于是社区出现了很多来让可变数据不可变的方案,核心目的都是为了 从最小单元去进行拷贝,没改变的对象数据则进行复用,而其中最具有代表性和影响力的就是 immutable.js 和 immer.js 。

immutable.js 底层是持久化数据结构,内部实现比较复杂,后续有机会会专门写一篇 immutable.js 的原理相关的文章。

相比而言,immer.js 的底层是 Proxy 代理模式,这种方式的实现过程比 immutable.js 会简单不少。

了解 immer.js

immer.js 最重要最核心的就是 produce 函数,也是默认导出函数,其他的导出其实都算是一些辅助性工具函数。

下面我们来看一下 produce 的使用示例,验证它是不是实现了 从最小单元去进行拷贝,没改变的对象数据则进行复用 这个目的。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

import produce from "immer"

const state = [

    { label: "HTML", info: { desc: "超文本标记语言" } },

    { label: "CSS", info: { desc: "层叠样式表" } }

];

const state1 = produce(state, draft => {

    // 新增了一个对象

    draft.push({ label: "ES5", info: { desc: "基于原型和头等函数的多范式高级解释型编程语言" } });

    // 修改了了一个对象

    draft[1].label = "CSS3";

})

console.log(state === state1) // false

console.log(state.length === state1.length) // false

console.log(state[0] === state1[0]) // true

console.log(state[1] === state1[1]) // false

console.log(state[1].info === state1[1].info) // true

可以看出来,每个最小单元的对象,如果进行了修改,则会拷贝对象,如果没有进行修改的对象,则会进行复用。

我们把它画成一个图:

  • draft 新增了字对象,因此改变了 draft 自身。
  • draft[1] 修改了 label,改变了自身draft[1],但实际上会一层层传递上去,也相当于修改了 draft

因此,只要对子节点的任何操作,实际上都会拷贝当前对象,当前对象被拷贝,就会影响上一层的对象也会被拷贝,层层递进,最后拷贝到了根结点,但是都是浅拷贝,因此子节点没有变的对象都可以复用。

比如我再修改一下:

1

2

3

const state2 = produce(state1, draft => {

    draft[2].label = "ES";

})

这时候的情况就是这样:

immer.js 原理

immer.js 是基于 Proxy 来监听对象的 get 和 set 操作,然后对数据进行处理和判断是否返回新的对象。

我们来使用 Proxy 来进行模拟 produce 函数。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

function produce<D extends object>(base: D, recipe: (draft: D) => void) {

  // 用于存储改变后的新数据

  let newData: any;

  // 给 base 对象添加代理

  const proxy = new Proxy(base, {

    set(obj, key: string, value: any) {

      // 检查 newData 是否存在,如果不存在,创建 newData

      if (!newData) {

        // 浅拷贝对象

        newData = { ...obj }

      }

      // 修改 newData,而不是 base,永远不要修改 base

      newData[key] = value

      return true

    }

})

  // 将 对象的代理 作为入参传入 recipe,让外界修改的是代理,而不是原本的对象数据

  recipe(proxy)

  // 为了避免意外的修改发生,返回一个被“冻结”的对象,保证数据的纯度

  // 如果 newData 不存在,表示没有执行写操作,返回 base 即可

  return Object.freeze(newData as D || base)

}

然后我们来测试一下:

1

2

3

4

5

6

const state = { label: "HTML", info: { desc: "超文本标记语言" } };

const state1 = produce(state, (draft) => {

    draft.label = "H5";

})

console.log(state === state1) // false

console.log(state.info === state1.info) // true

可以看出实现的这个极简版的 produce 已经可以实现 从最小单元去进行拷贝,没改变的对象数据则进行复用,但仅限于修改对象的第一层结构,如果直接修改 draft.info.desc 会发现 state 和 state1 都会被改变。

Proxy 只会对当前传入进去的一个对象单元进行代理,如果有子对象,并不会进行代理,因此,深层次对象还需要再加处理,就像深拷贝一样,需要进行递归处理。

immer.js 源码的代码并不少,主要是为了兼容性、处理各种数据类型、以及扩展API,因此做了很多处理,这个后续会单独出一篇分析它内部源码的实现,这里先说一下其内部主要方案:

  • 默认导出的 produce 本身是一个 Immer 类的一个属性方法,也导出了 Immer 类。
  • 兼容了 Map、Set 数据结构,Proxy 本身支持了数组类型。
  • 需要兼容ES5时,使用 Object.defineProperty 来进行兼容。
  • 扩展了不少 API ,主要是为了增强各种功能和使用体验。
  • 内部核心实现方法是 createProxy ,其内部通过 get 拦截属性获取方法来实现动态给子对象 Proxy 化,也就是只有用到的属性才会变成 Proxy Object ,没有用到的并不会变。
  • 内部基本上把 Proxy 的 handler 中的 属性 都使用到了。

总结

数据不可变的原理就是 “拷贝数据”,而市面上的不可变数据方案的目的就是让操作数据变成对最小单元对象数据的拷贝和操作,以提高代码执行效率和性能。


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

您可能感兴趣的文章 :

原文链接 :
    Tag :
相关文章
  • 让chatgpt将html中的图片转为base64的方法

    让chatgpt将html中的图片转为base64的方法
    故事要从我们公司的新官网说起,新官网是叫外包做的,前后端没有分离,对,你没听错,都到了 2023 年的今天,新项目依然是前后端混在
  • JavaScript深拷贝方法structuredClone使用介绍
    对于深拷贝,最容易也应该是常见的方法是使用JSON.parse() + JSON.stringify(),还有一个借助第三方脚本库 lodash ,其中方法cloneDeep可以实现深拷
  • requestAnimationFrame使用示例介绍

    requestAnimationFrame使用示例介绍
    requestAnimationFrame--use是什么 告诉浏览器用来执行一个动画,并且在下一次重绘之前调用其指定的回调函数取更新动画,所以该方法的参数就
  • 不可变数据方案之immer.js原理介绍

    不可变数据方案之immer.js原理介绍
    本篇文章是JavaScript 函数式编程 学习系列第三篇 前一篇JavaScript数据类型对函数式编程的影响讲到了不可变数据的重要性,而让数据不可变的
  • WebComponent使用教程介绍

    WebComponent使用教程介绍
    WebComponent 是官方定义的自定义组件实现方式,它可以让开发者不依赖任何第三方框架(如Vue,React)来实现自定义页面组件;达到组件复用
  • element plus的样式修改和扩展实例介绍
    一、用户故事 我们开发了一个业务组件库。业务组件库是需要基于公司内部的一个UI组件库。而公司的UI组件库又出基于element ui的。 公司的
  • element弹窗表格的字体模糊bug解决方法

    element弹窗表格的字体模糊bug解决方法
    有一个BUG,就是在使用element弹窗表格的字体异常的模糊。如下图: 这个问题其实已经存在很久了。客户屡有反馈,但是不多。我们基本自测
  • 如何在JavaScript中使用媒体查询介绍

    如何在JavaScript中使用媒体查询介绍
    说起媒体查询想必大家最先想到的都是CSS中@media,没错,这是我们最常用的媒体查询方法,主要用来为我们的网站做适配处理。 比如: 1
  • js二进制数据及其互相转化的实现
    file 在js中有很多二进制数据,有file,base64,Blob,ArrayBuffer,FileReader。这些二进制数据在文件导出和下载的时候是经常会用到的,我们这篇文章就
  • JS实现简单的操作杆旋转示例介绍

    JS实现简单的操作杆旋转示例介绍
    JS 简单的操作杆旋转实现 首先说明一下,请直接忽略背景图,这里主要实现的是杆旋转控制方向盘旋转。 鼠标移出控制区域,控制球复位
  • 本站所有内容来源于互联网或用户自行发布,本站仅提供信息存储空间服务,不拥有版权,不承担法律责任。如有侵犯您的权益,请您联系站长处理!
  • Copyright © 2017-2022 F11.CN All Rights Reserved. F11站长开发者网 版权所有 | 苏ICP备2022031554号-1 | 51LA统计