欢迎来到【魔域源码分享】【密算法源码】【自制office源码】immutable 源码-皮皮网网站!!!

皮皮网

【魔域源码分享】【密算法源码】【自制office源码】immutable 源码-皮皮网 扫描左侧二维码访问本站手机端

【魔域源码分享】【密算法源码】【自制office源码】immutable 源码

2024-11-30 00:06:20 来源:{typename type="name"/} 分类:{typename type="name"/}

1.在andorid 出现 java.lang.UnsupportedOperationException
2.SWIG教程《二》
3.深入源码解析LevelDB
4.不是我说,不掌握这些坑,你敢用BigDecimal吗?
5.[译] 关于 Angular 的变化检测,你需要知道的一切
6.这些hook更优雅的管理你的状态

immutable 源码

在andorid 出现 java.lang.UnsupportedOperationException

       关键字: java集合中部分异常java.lang.unsupportedoperationexception一个共同点

        在项目中采用一个枚举的集合,本人采用Collections中的空集合Collections.emptyList()在添加时发生异常:

       常见集合如下:

       private List<VacationCategory> vacationcategorys = Collections.emptyList();

       报错误如下:

       -- Encapsulated exception ------------\

       java.lang.UnsupportedOperationException

        at java.util.AbstractList.add(AbstractList.java:)

        at java.util.AbstractList.add(AbstractList.java:)

        at com.unutrip.callcenter.vacation.web.condition.VacationOrderConditionConvertor.setProductStyle(VacationOrderConditionConvertor.java:)

        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)

        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:)

        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:)

       ..............................

       JDK API解释如下:

       java.lang.CloneNotSupportedException

       不支持克隆异常。当没有实现Cloneable接口或者不支持克隆方法时,魔域源码分享调用其clone()方法则抛出该异常。

       在网上查一下原因是因为部分集合类型一样但是缺少部分方法或不支持。

       如特殊情况如下:

       (1)常常使用Arrays.asLisvt()后调用add,remove这些method时出现java.lang.UnsupportedOperationException异常。这是由于:

       Arrays.asLisvt() 返回java.util.Arrays$ArrayList, 而不是ArrayList。Arrays$ArrayList和ArrayList都是继承AbstractList,remove,add等method在AbstractList中是默认throw UnsupportedOperationException而且不作任何操作。ArrayList override这些method来对list进行操作,但是Arrays$ArrayList没有override remove(int),add(int)等,所以throw UnsupportedOperationException。

       解决方法是使用Iterator,或者转换为ArrayList

       List list = Arrays.asList(a[]);

       List arrayList = new ArrayList(list);

       (2)

       private List<VacationCategory> vacationcategorys = Collections.emptyList();

       执行remove,add等method时,抛出此异常,本人将上述代码改为:

       private List<VacationCategory> vacationcategorys = new ArrayList<VacationCategory>();

       没有此错误,于是我查看一下源代码:

       源码如下:

       此类在Collections的类中:

       /

**

        * The empty list (immutable). This list is serializable.

       

*

        * @see #emptyList()

        */

        public static final List EMPTY_LIST = new EmptyList();

       /

**

        * Returns the empty list (immutable). This list is serializable.

       

*

        * <p>This example illustrates the type-safe way to obtain an empty list:

        * <pre>

        * List<String> s = Collections.emptyList();

        * </pre>

        * Implementation note: Implementations of this method need not

        * create a separate <tt>List</tt> object for each call. Using this

        * method is likely to have comparable cost to using the like-named

        * field. (Unlike this method, the field does not provide type safety.)

       

*

        * @see #EMPTY_LIST

        * @since 1.5

        */

        public static final <T> List<T> emptyList() {

        return (List<T>) EMPTY_LIST;

        }

       /

**

        * @serial include

        */

        private static class EmptyList

        extends AbstractList<Object>

        implements RandomAccess, Serializable {

       // use serialVersionUID from JDK 1.2.2 for interoperability

        private static final long serialVersionUID = L;

       public int size() { return 0;}

       public boolean contains(Object obj) { return false;}

       public Object get(int index) {

        throw new IndexOutOfBoundsException("Index: "+index);

        }

       // Preserves singleton property

        private Object readResolve() {

        return EMPTY_LIST;

        }

       }

       EmptyList此集合竟然没有相应的add,remove等方法,哭了,呜呜...........

       ~~~~(>_<)~~~~

SWIG教程《二》

       在SWIG教程中,了解变量属性的管理非常重要。使用%immutable指令可以创建只读字段,一旦变量被标记为不可变,除非明确使用%mutable指令将其改为可变状态,否则它始终为只读。

       注意兼容性,先前的%readonly和%readwrite指令虽然仍能工作,但会生成警告信息。更推荐使用%immutable和%mutable指令,同时不要忘记额外的分号。

       重命名功能允许在转换目标语言时处理关键字冲突。rename指令的范围从声明开始到文件结束,因此在引入重命名的头文件时,确保其在rename之后。

       SWIG提供了强大的重命名工具,不仅限于单个命名,还可以批量重命名。例如,通过匹配规则为所有函数添加前缀,将所有蛇形命名转换为驼峰命名,剔除命名中的特定字符,以及对操作符重载函数进行重命名。

       使用ignore指令可以忽略不需要封装的内容。可以选择性地忽略大部分符号,同时仅封装所需的方法或类,如在特定头文件中仅封装名为“Star”的类。

       回调函数的实现需要提前声明,不能直接传入目标函数。一旦声明为回调,无法在目标函数中作为普通函数调用。对于既作为回调也作为普通函数调用的情况,可以采取特定的声明方式。

       SWIG自动生成C++类的默认构造函数和析构函数,但可以通过指令或参数选项禁止。使用%nodefaultctor指令或命令行参数-nodefaultctor可以避免生成默认构造函数。对于析构函数,同样可以使用%nodefaultdtor指令禁止生成。

       SWIG虽然能处理大部分C++语法,但并非完整的解析器,仍有部分语法无法解析。为C++类添加成员函数时,使用%extend指令为C类型的结构体绑定额外的成员函数,以辅助创建和使用。

       SWIG输出的C/C++代码分为五部分,包括用户自定义的预处理器宏、内部支持代码、用户定义的支持代码、自动生成的封装代码,以及初始化模块的函数。

       代码注入功能允许在特定代码段中插入自定义代码,密算法源码使用指令或section名实现。例如,%runtime指令代替了使用%insert("runtime")。%{ ...%}指令实际上是%header %{ ...%}的简写。

       在封装中使用初始化代码时,可以利用Init段进行特定的初始化操作。辅助函数的编写通常通过代码注入来简化,inline指令则提供了一种将代码块在封装和源代码中重复使用的方式,尽管其只适用于函数和变量,对于头文件的封装仍需通过%include指令告知SWIG。

深入源码解析LevelDB

       深入源码解析LevelDB

       LevelDB总体架构中,sstable文件的生成过程遵循一系列精心设计的步骤。首先,遍历immutable memtable中的key-value对,这些对被写入data_block,每当data_block达到特定大小,构造一个额外的key-value对并写入index_block。在这里,key为data_block的最大key,value为该data_block在sstable中的偏移量和大小。同时,构造filter_block,默认使用bloom filter,用于判断查找的key是否存在于data_block中,显著提升读取性能。meta_index_block随后生成,存储所有filter_block在sstable中的偏移和大小,此策略允许在将来支持生成多个filter_block,进一步提升读取性能。meta_index_block和index_block的偏移和大小保存在sstable的脚注footer中。

       sstable中的block结构遵循一致的模式,包括data_block、index_block和meta_index_block。为提高空间效率,数据按照key的字典顺序存储,采用前缀压缩方法处理。查找某一key时,必须从第一个key开始遍历才能恢复,因此每间隔一定数量(block_restart_interval)的key-value,全量存储一个key,并设置一个restart point。每个block被划分为多个相邻的key-value组成的集合,进行前缀压缩,并在数据区后存储起始位置的偏移。每一个restart都指向一个前缀压缩集合的起始点的偏移位置。最后一个位存储restart数组的大小,表示该block中包含多少个前缀压缩集合。

       filter_block在写入data_block时同步存储,当一个new data_block完成,根据data_block偏移生成一份bit位图存入filter_block,并清空key集合,重新开始存储下一份key集合。

       写入流程涉及日志记录,包括db的sequence number、本次记录中的操作个数及操作的key-value键值对。WriteBatch的batch_data包含多个键值对,leveldb支持延迟写和停止写策略,导致写队列可能堆积多个WriteBatch。为了优化性能,写入时会合并多个WriteBatch的batch_data。日志文件只记录写入memtable中的key-value,每次申请新memtable时也生成新日志文件。

       在写入日志时,对日志文件进行划分为多个K的文件块,每次读写以这样的每K为单位。每次写入的日志记录可能占用1个或多个文件块,因此日志记录块分为Full、First、Middle、Last四种类型,读取时需要拼接。

       读取流程从sstable的层级结构开始,0层文件特别,可能存在key重合,自制office源码因此需要遍历与查找key有重叠的所有文件,文件编号大的优先查找,因为存储最新数据。非0层文件,一层中的文件之间key不重合,利用版本信息中的元数据进行二分搜索快速定位,仅需查找一个sstable文件。

       LevelDB的sstable文件生成与合并管理版本,通过读取log文件恢复memtable,仅读取文件编号大于等于min_log的日志文件,然后从日志文件中读取key-value键值对。

       LevelDB的LruCache机制分为table cache和block cache,底层实现为个shard的LruCache。table cache缓存sstable的索引数据,类似于文件系统对inode的缓存;block cache缓存block数据,类似于Linux中的page cache。table cache默认大小为,实际缓存的是个sstable文件的索引信息。block cache默认缓存8M字节的block数据。LruCache底层实现包含两个双向链表和一个哈希表,用于管理缓存数据。

       深入了解LevelDB的源码解析,有助于优化数据库性能和理解其高效数据存储机制。

不是我说,不掌握这些坑,你敢用BigDecimal吗?

       一直从事金融相关项目,对BigDecimal了如指掌,也见证了许多因为对其不了解或使用不当导致的资损事件。

       如果你从事金融相关项目,或者你的项目中涉及到金额的计算,务必花时间阅读这篇文章,全面了解BigDecimal。

       Java的java.math包提供了BigDecimal API类,用于对超过位有效位的数进行精确的运算。双精度浮点型变量double可以处理位有效数,但在实际应用中,可能需要对更大或更小的数进行运算和处理。

       一般情况下,对于不需要精确计算精度的数字,可以直接使用Float和Double处理,但Double.valueOf(String)和Float.valueOf(String)会丢失精度。因此,如果需要精确计算的结果,则必须使用BigDecimal类来操作。

       BigDecimal对象提供了传统的+、-、*、/等算术运算符对应的方法,通过这些方法进行相应的操作。BigDecimal都是不可变的(immutable)的,在进行每一次四则运算时,都会产生一个新的对象,所以在做加减乘除运算时要记得保存操作后的值。

       在使用BigDecimal时,有4种使用场景下的坑,你一定要了解一下,如果使用不当,必定很惨。掌握这些案例,当别人写出有坑的代码,你也能够一眼识别出来,大牛就是这么练成的。

       第一:浮点类型的坑

       在使用Float、Double等浮点类型进行计算时,有可能得到的是一个近似值,而不是精确的值。

       比如下面的代码:结果是多少?0.1吗?不是,执行上面代码执行的结果是0.。之所以产生这样的结果,是因为0.1的二进制表示是无限循环的。由于计算机的资源是有限的,所以是没办法用二进制精确地表示0.1,只能用「近似值」来表示,就是沙漠指标源码在有限的精度情况下,最大化接近0.1的二进制数,于是就会造成精度缺失的情况。

       关于上述的现象大家都知道,不再详细展开。同时,还会得出结论在科学计数法时可考虑使用浮点类型,但如果是涉及到金额计算要使用BigDecimal来计算。

       那么,BigDecimal就一定能避免上述的浮点问题吗?来看下面的示例:

       上述单元测试中的代码,a和b结果分别是什么?

       上面的实例说明,即便是使用BigDecimal,结果依旧会出现精度问题。这就涉及到创建BigDecimal对象时,如果有初始值,是采用new BigDecimal的形式,还是通过BigDecimal#valueOf方法了。

       之所以会出现上述现象,是因为new BigDecimal时,传入的0.1已经是浮点类型了,鉴于上面说的这个值只是近似值,在使用new BigDecimal时就把这个近似值完整地保留下来了。

       而BigDecimal#valueOf则不同,它的源码实现如下:

       在valueOf内部,使用Double#toString方法,将浮点类型的值转换成了字符串,因此就不存在精度丢失问题了。

       此时就得出一个基本的第一,在使用BigDecimal构造函数时,尽量传递字符串而非浮点类型;第二,如果无法满足第一条,则可采用BigDecimal#valueOf方法来构造初始化值。

       这里延伸一下,BigDecimal常见的构造方法有如下几种:

       其中涉及到参数类型为double的构造方法,会出现上述的问题,使用时需特别留意。

       第二:浮点精度的坑

       如果比较两个BigDecimal的值是否相等,你会如何比较?使用equals方法还是compareTo方法呢?

       先来看一个示例:

       乍一看感觉可能相等,但实际上它们的本质并不相同。

       equals方法是基于BigDecimal实现的equals方法来进行比较的,直观印象就是比较两个对象是否相同,那么代码是如何实现的呢?

       仔细阅读代码可以看出,equals方法不仅比较了值是否相等,还比较了精度是否相同。上述示例中,由于两者的精度不同,所以equals方法的结果当然是false了。而compareTo方法实现了Comparable接口,真正比较的是值的大小,返回的值为-1(小于),0(等于),1(大于)。

       基本通常情况,如果比较两个BigDecimal值的大小,采用其实现的compareTo方法;如果严格限制精度的比较,那么则可考虑使用equals方法。

       另外,这种场景在比较0值的时候比较常见,比如比较BigDecimal("0")、BigDecimal("0.0")、BigDecimal("0."),此时一定要使用compareTo方法进行比较。

       第三:设置精度的坑

       在项目中看到好多同学通过BigDecimal进行计算时不设置计算结果的精度和舍入模式,真是着急人,虽然大多数情况下不会出现什么问题。但下面的场景就不一定了:

       执行上述代码的结果是什么?ArithmeticException异常!

       这个异常的发生在官方文档中也有说明:

       If the quotient has a nonterminating decimal expansion and the operation is specified to return an exact result, an ArithmeticException is thrown. Otherwise, the exact result of the division is returned, as done for other operations.

       总结一下就是,如果在除法(divide)运算过程中,如果商是一个无限小数(0.…),而操作的结果预期是一个精确的数字,那么将会抛出ArithmeticException异常。

       此时,只需在使用divide方法时指定结果的精度即可:

       执行上述代码,输入结果为0.。

       基本在使用BigDecimal进行(所有)运算时,iview表格源码一定要明确指定精度和舍入模式。

       拓展一下,舍入模式定义在RoundingMode枚举类中,共有8种:

       通常我们使用的四舍五入即RoundingMode.HALF_UP。

       第四:三种字符串输出的坑

       当使用BigDecimal之后,需要转换成String类型,你是如何操作的?直接toString?

       先来看看下面的代码:

       执行的结果是上述对应的值吗?并不是:

       也就是说,本来想打印字符串的,结果打印出来的是科学计数法的值。

       这里我们需要了解BigDecimal转换字符串的三个方法

       三种方法展示结果示例如下:

       计算法

       基本**根据数据结果展示格式不同,采用不同的字符串输出方法,通常使用比较多的方法为toPlainString()**。

       另外,NumberFormat类的format()方法可以使用BigDecimal对象作为其参数,可以利用BigDecimal对超出位有效数字的货币值,百分值,以及一般数值进行格式化控制。

       使用示例如下:

       输出结果如下:

       小结

       本篇文章介绍了BigDecimal使用中场景的坑,以及基于这些坑我们得出的“最佳实践”。虽然某些场景下推荐使用BigDecimal,它能够达到更好的精度,但性能相较于double和float,还是有一定的损失的,特别在处理庞大、复杂的运算时尤为明显。故一般精度的计算没必要使用BigDecimal。而必须使用时,一定要规避上述的坑。

[译] 关于 Angular 的变化检测,你需要知道的一切

       关于 Angular 的变化检测,你需要知道的一切

       探究内部实现和具体用例

       要全面了解 Angular 的变化检测机制,查看源码是不可或缺的步骤,因为网络上相关文章少之又少。大部分文章只涉及每个组件自身的变化检测器、不可变变量(immutable)和变化检测策略(change detection strategy)的概念,但缺乏深入剖析。本文旨在深入探讨不可变变量如何触发变化检测,以及变化监测策略对检测的影响。同时,你将能将这些知识应用到优化性能的场景中。

       本文分为两部分,第一部分侧重于技术细节,包含大量源码链接。我们基于 Angular 4.0.1 版本来解释变化检测机制的工作原理。该版本与 Angular 2.4.1 版本有所差异。若想了解更多,可参考 Stack Overflow 上的相关讨论。

       第二部分将展示如何运用变化检测。值得注意的是,尽管两个版本的 API 有所不同,但第二部分的内容对两个版本都适用。

       核心概念:视图(view)

       Angular 的教程常提到应用是一颗组件树,但在 Angular 内部,视图(view)是一种较低级的抽象概念。视图与组件之间存在直接关联,每个视图都通过 component 属性与对应的组件类关联。所有操作在视图中执行,包括属性检查和 DOM 更新。因此,从技术角度来看,更准确的说法是 Angular 应用是一颗视图树。组件可以视为视图的更高阶概念。源码中对视图有如下描述:

       视图是应用 UI 的基本组成元素,由一组在创建和销毁时共同存在的最小集合构成。视图的属性可以改变,但元素结构(数量和顺序)不能改变。想要改变元素结构,只能通过 ViewContainerRef 插入、移动或移除嵌入的视图。每个视图可以包含多个视图容器。

       在本文中,我们将交替使用“组件视图”和“组件”概念。

       值得注意的是,网络上有关变化检测的文章和 StackOverflow 回答中的“视图”常被称作变化检测器对象(Change Detector Object)或 ChangeDetectorRef。实际上,变化检测并非独立对象,而是在视图上运行的。

       每个视图通过 nodes 属性关联子视图,以便对子视图进行操作。

       视图的状态

       每个视图都有一个 state 属性,这是极其重要的属性,决定了是否对视图及其所有子视图执行变化检测。state 属性有多种可能的值,与本文相关的有:

       如果ChecksEnabled 为 false 或视图状态为 Errored 或者 Destroyed,变化检测将跳过此视图及其所有子视图。默认情况下,所有视图均以 ChecksEnabled 作为初始值,除非使用了 ChangeDetectionStrategy.OnPush。

       Angular 中有许多高级概念用于操作视图。在文章中已提及部分概念,如 ViewRef。它封装了组件视图并提供了一个名为 detectChanges 的方法,该方法会在异步事件触发时在最顶层的 ViewRef 上执行变化检测。最顶层的 ViewRef 执行变化检测后,会递归地对其子视图执行检测。

       通过 ChangeDetectorRef 令牌将 viewRef 注入组件构造函数中,可以实现此操作:

       从其定义可知这一点:

       变化检测操作

       执行变化检测的主要逻辑在 checkAndUpdateView 方法中,该方法主要针对子组件视图执行操作,并递归地调用此方法以遍历从宿主组件到所有组件的所有视图。这意味着,下一次递归中,子组件就成为了新的父组件。

       执行变化检测的主要步骤如下:

       有几个关键点需要注意:

       首先,子组件在子视图被检测之前会触发onChanges 生命周期钩子,即使子视图的变化检测被跳过了。这一点至关重要,后续部分将展示如何利用这一点。

       第二,当检测视图时,更新视图的 DOM 是变化检测机制的一部分。因此,如果组件未被检测,DOM 将不会更新,模板中的组件属性发生改变时也是如此。在第一次检测之前,模板已被渲染。这里所说的更新 DOM 指的是更新插值。例如,some { { name}},在第一次检测之前,已经将 DOM 元素 span 渲染好。检测过程中,只会渲染 { { name}} 部分。

       另一个有趣的是,子组件视图的状态可以在变化检测过程中改变。所有组件视图默认初始化为 ChecksEnabled。但是,使用 OnPush 策略的组件在第一次检测后不再执行变化检测(步骤第 9 步)。OnPush 文档说明,只有绑定发生变化时才会执行检测。因此,需要将 ChecksEnabled 位设置为启用检测(步骤第 2 步操作):

       只有当父视图绑定发生变化,且子组件视图初始化为 ChangeDetectionStrategy.OnPush 时,才会更新状态。

       最后,当前视图的变化检测负责启动子视图的变化检测(步骤第 8 步)。以下是相关的代码:

       现在你知道了视图状态控制了是否对此视图及其子视图进行变化检测。那么问题来了——我们能控制这个状态吗?答案是可以,这也是本文第二部分将探讨的内容。

       有些生命周期钩子在更新 DOM 前调用(步骤 3、4、5),有些在之后(步骤 9)。例如,组件结构为 A -> B -> C,它们的生命周期钩子调用和更新绑定的顺序是:

       总结

       假设我们有如图所示的组件树,根据前面所述,每个组件都有一个与之关联的视图。每个视图初始化为 ViewState.ChecksEnabled,这意味着 Angular 进行变化检测时,树中的每个组件都会被检测。

       如果我们希望禁用 AComponent 及其子组件的变化检测,只需将 ViewState.ChecksEnabled 设置为 false。由于状态操作是低级操作,Angular 提供了许多视图的公共方法。每个组件都可以通过 ChangeDetectorRef 令牌获取关联的视图。Angular 文档定义了该类的公共接口:

       接下来,我们将探讨如何使用这些接口。

       detach

       detach 允许我们操作状态,它可以对当前视图禁用检查:

       在代码中实现如下:

       这确保了接下来的变化检测中,从 AComponent 开始,左子树的所有组件都会被跳过(橙色的组件不会被检测):

       需要注意的是,改变的是 AComponent 的状态,其所有子组件都不会被检测。第二点是,由于整个左子树的组件都不执行变化检测,它们模板中的 DOM 也不会更新。以下例子简要描述了这种情况:

       当组件首次被检测时,span 将被渲染为“See if I change: false”。两秒后,changed 属性变为 true,但 span 中的文字不会更新。然而,如果去掉了 this.cd.detach(),就会按照预期更新。

       reattach

       如第一部分所述,如果 AComponent 的输入绑定 aProp 发生变化,AComponent 的 Onchanges 生命周期钩子就会被触发。这意味着一旦得知输入属性发生变化,即可启动当前组件的变化检测器来检测变化,然后在下一个周期将其分离。以下是实现此功能的代码片段:

       由于reattach 只是简单地设置 ViewState.ChecksEnabled 位:

       这与将 ChangeDetectionStrategy 设置为 OnPush 的效果相似:在第一次变化检测后禁用检测,当父组件绑定的属性发生变化时启用,检测完之后再次禁用。

       需要注意的是,OnChanges 钩子仅在禁用检查的子树的最顶端组件触发,子树中的其他组件不会触发。然而,我们可以通过这个钩子执行自定义逻辑,然后将组件标记为可以执行一次变化检测。由于 Angular 只检测对象引用,我们在此可以检查对象的属性:

       markForCheck

       reattach 方法仅对当前组件启用检测,如果父组件未启用变化检测,效果有限。我们需要一个能够检测所有父组件直到根组件的方法,这个方法就是 markForCheck:

       从代码中可以看出,它仅向上迭代直至根节点,使所有父组件都启用检查。

       何时使用这个方法?与 ngOnChanges 一样,在使用 OnPush 策略时也会触发 ngDoCheck 生命周期钩子。再次强调,只有禁用检查的子树的最顶端组件会触发,子树中的其他组件都不会触发。但是,我们可以通过这个钩子执行一些自定义逻辑,然后将组件标记为可以执行一次变化检测。由于 Angular 只检测对象引用,我们在此可以检查对象的属性:

       detectChanges

       有一种方法可以对当前组件和所有子组件执行一次变化检测,这就是 detectChanges 方法。此方法会对当前组件视图执行变化检测,不管组件的状态如何。也就是说,视图仍会禁用检测,并且在接下来常规的变化检测中,不会检测此组件。例如:

       尽管变化检测器引用仍保持分离,但 DOM 元素仍会随着输入绑定的变化而变化。

       checkNoChanges

       这是变化检测器的最后一个方法,主要作用是确保当前执行的变化检测中,没有变化发生。简单来说,它执行本文第一部分提到的列表中的第 1、7、8 步。如果发现绑定发生变化或 DOM 需要更新,会抛出异常。

       还有疑问?

       若对本文有任何疑问,欢迎在 Stack Overflow 上提问,并在本文评论区贴上链接。这样整个社区都能受益。感谢。

       关注我以获取更多资讯

       若发现译文存在错误或其他需要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可获得相应奖励积分。本文永久链接即为本文在 GitHub 上的 MarkDown 链接。

       掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 Android、 iOS、 前端、 后端、 区块链、 产品、 设计、 人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划、 官方微博、 知乎专栏。

这些hook更优雅的管理你的状态

       本文是深入浅出ahooks源码系列文章的第十二篇,这个系列的目标主要有以下几点:

       加深对Reacthooks的理解。

       学习如何抽象自定义hooks。构建属于自己的Reacthooks工具库。

       培养阅读学习源码的习惯,工具库是一个对源码阅读不错的选择。

       今天我们来聊聊ahooks中那些可以帮助我们更优雅管理我们state(状态)的那些hook。一些比较特殊的,比如cookie/localStorage/sessionStorage,useUrlState等,我们已经单独拿出来细讲了,感兴趣可以看看笔者的历史文章。

useSetState

       管理object类型state的Hooks,用法与class组件的this.setState基本一致。

       先来了解一下可变数据和不可变数据的含义和区别如下:

       可变数据(mutable)即一个数据被创建之后,可以随时进行修改,修改之后会影响到原值。

       不可变数据(Immutable)就是一旦创建,就不能再被更改的数据。对Immutable对象的任何修改或添加删除操作都会返回一个新的Immutable对象。

       我们知道,ReactFunctionComponents中的State是不可变数据。所以我们经常需要写类似如下的代码:

setObj((prev)=>({ ...prev,name:'Gopal',others:{ ...prev.others,age:'',}}));

       通过useSetState,可以省去对象扩展运算符操作这个步骤,即:

setObj((prev)=>({ name:'Gopal',others:{ age:'',}}));

       其内部实现也比较简单,如下所示:

       调用设置值方法的时候,会根据传入的值是否为函数。如果是函数,则入参为旧状态,输出新的状态。否则直接作为新状态。这个符合setState的使用方法。

       使用对象拓展运算符,返回新的对象,保证原有数据不可变。

constuseSetState=<SextendsRecord<string,any>>(initialState:S|(()=>S),):[S,SetState<S>]=>{ const[state,setState]=useState<S>(initialState);//合并操作,并返回一个全新的值constsetMergeState=useCallback((patch)=>{ setState((prevState)=>{ //新状态constnewState=isFunction(patch)?patch(prevState):patch;//也可以通过类似Object.assign的方式合并//对象拓展运算符,返回新的对象,保证原有数据不可变returnnewState?{ ...prevState,...newState}:prevState;});},[]);return[state,setMergeState];};

       可以看到,其实就是将对象拓展运算符的操作封装到内部。

       还有其他更优雅的方式?我们可以使用use-immer

       useImmer(initialState)非常类似于useState。该函数返回一个元组,元组的第一个值是当前状态,第二个是updater函数,它接受一个immerproducer函数或一个值作为参数。

       使用如下:

const[person,updatePerson]=useImmer({ name:"Michel",age:});functionupdateName(name){ updatePerson(draft=>{ draft.name=name;});}functionbecomeOlder(){ updatePerson(draft=>{ draft.age++;});}

       当向更新函数传递一个函数的时候,draft参数可以自由地改变,直到producer函数结束,所做的改变将是不可变的,并成为下一个状态。这更符合我们的使用习惯,可以通过draft.xx.yy的方式更新我们对象的值。

useBoolean和useToggle

       这两个都是特殊情况下的值管理。

       useBoolean,优雅的管理boolean状态的Hook。

       useToggle,用于在两个状态值间切换的Hook。

       实际上,useBoolean又是useToggle的一个特殊使用场景。

       先看useToggle。

       这里使用了typescript函数重载声明入参和出参类型,根据不同的入参会返回不同的结果。比如第一个入参为boolean布尔值,则返回一个元组,第一项为boolean值,第二个为更新函数。优先级从上到下依次变低。

       入参可能有两个值,第一个为默认值(认为是左值),第二个是取反之后的值(认为是右值),可以不传,不传的时候,则直接根据默认值取反!defaultValue。

       toggle函数。切换值,也就是上面的左值和右值的转换。

       set。直接设置值。

       setLeft。设置默认值(左值)。

       setRight。如果传入了reverseValue,则设置为reverseValue。否则设置为defautValue的取反值。

//TS函数重载的使用functionuseToggle<T=boolean>():[boolean,Actions<T>];functionuseToggle<T>(defaultValue:T):[T,Actions<T>];functionuseToggle<T,U>(defaultValue:T,reverseValue:U):[T|U,Actions<T|U>];functionuseToggle<D,R>(//默认值defaultValue:D=falseasunknownasD,//取反reverseValue?:R,){ const[state,setState]=useState<D|R>(defaultValue);constactions=useMemo(()=>{ constreverseValueOrigin=(reverseValue===undefined?!defaultValue:reverseValue)asD|R;//切换stateconsttoggle=()=>setState((s)=>(s===defaultValue?reverseValueOrigin:defaultValue));//修改stateconstset=(value:D|R)=>setState(value);//设置为defaultValueconstsetLeft=()=>setState(defaultValue);//如果传入了reverseValue,则设置为reverseValue。否则设置为defautValue的反值constsetRight=()=>setState(reverseValueOrigin);return{ toggle,set,setLeft,setRight,};//useToggleignorevaluechange//},[defaultValue,reverseValue]);},[]);return[state,actions];}

       而useBoolean是对useToggle的一个使用。如下,比较简单,不细说

exportdefaultfunctionuseBoolean(defaultValue=false):[boolean,Actions]{ const[state,{ toggle,set}]=useToggle(defaultValue);constactions:Actions=useMemo(()=>{ constsetTrue=()=>set(true);constsetFalse=()=>set(false);return{ toggle,set:(v)=>set(!!v),setTrue,setFalse,};},[]);return[state,actions];}usePrevious

       保存上一次状态的Hook。

       其原理,是每次状态变更的时候,比较值有没有发生变化,变更状态:

       维护两个状态prevRef(保存上一次的状态)和curRef(保存当前状态)。

       状态变更的时候,使用shouldUpdate判断是否发生变化,默认通过Object.is判断。开发者可以自定义shouldUpdate函数,并决定什么时候记录上一次状态。

       状态发生变化,更新prevRef的值为上一个curRef,并更新curRef为当前的状态。

constdefaultShouldUpdate=<T>(a?:T,b?:T)=>!Object.is(a,b);functionusePrevious<T>(state:T,shouldUpdate:ShouldUpdateFunc<T>=defaultShouldUpdate,):T|undefined{ //使用了useRef的特性,一直保持引用不变//保存上一次值constprevRef=useRef<T>();//当前值constcurRef=useRef<T>();//自定义是否更新上一次的值if(shouldUpdate(curRef.current,state)){ prevRef.current=curRef.current;curRef.current=state;}returnprevRef.current;}useRafState

       只在requestAnimationFramecallback时更新state,一般用于性能优化。

       window.requestAnimationFrame()告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行。

       假如你的操作是比较频繁的,就可以通过这个hook进行性能优化。

       重点看setRafState方法,它执行的时候,会取消上一次的setRafState操作。重新通过requestAnimationFrame去控制setState的执行时机。

       另外在页面卸载的时候,会直接取消操作,避免内存泄露。

functionuseRafState<S>(initialState?:S|(()=>S)){ constref=useRef(0);const[state,setState]=useState(initialState);constsetRafState=useCallback((value:S|((prevState:S)=>S))=>{ cancelAnimationFrame(ref.current);ref.current=requestAnimationFrame(()=>{ setState(value);});},[]);//unMount的时候,去除监听useUnmount(()=>{ cancelAnimationFrame(ref.current);});return[state,setRafState]asconst;}useSafeState

       用法与React.useState完全一样,但是在组件卸载后异步回调内的setState不再执行,避免因组件卸载后更新状态而导致的内存泄漏。

       代码如下:

       在更新的时候,通过useUnmountedRef判断如果组件卸载,则停止更新。

functionuseSafeState<S>(initialState?:S|(()=>S)){ //判断是否卸载constunmountedRef=useUnmountedRef();const[state,setState]=useState(initialState);constsetCurrentState=useCallback((currentState)=>{ //如果组件卸载,则停止更新if(unmountedRef.current)return;setState(currentState);},[]);return[state,setCurrentState]asconst;}

       useUnmountedRef这个我们之前提过,简单回顾下,其实就是在hook的返回值中标记组件为已卸载。

constuseUnmountedRef=()=>{ constunmountedRef=useRef(false);useEffect(()=>{ unmountedRef.current=false;//如果已经卸载,则会执行return中的逻辑return()=>{ unmountedRef.current=true;};},[]);returnunmountedRef;};useGetState

       给React.useState增加了一个getter方法,以获取当前最新值。

       其实现如下:

       其实就是通过useRef记录最新的state的值,并暴露一个getState方法获取到最新的。

setObj((prev)=>({ name:'Gopal',others:{ age:'',}}));0

       这在某一些情况下,可以避免React的闭包陷阱。如官网例子:

setObj((prev)=>({ name:'Gopal',others:{ age:'',}}));1

       假如这里不使用getCount(),而是直接使用count,是获取不到最新的值的。

总结与思考

       React的functionComponent的状态管理还是比较灵活,我们可以针对一些场景进行封装和优化,从而更优雅的管理我们的state状态,希望ahooks这些封装能对你有所帮助。

原文:/post/

Rematch 源码系列四、Third-Party plugins

       本文深入探讨了rematch的两个常用第三方插件:immer与loading。immer插件旨在简化state的修改过程,通过引入immerjs,允许开发者在reducer中使用mutable状态,进而生成immutable状态,简化了常规操作。immer插件的实现相对简单,只需将常规reducer包裹一层,使之通过immerjs处理即可。

       immer插件的核心在于其对reducer的封装,通过immer.produce方法处理draft状态,简化了mutable状态的管理,避免了复杂的clone和赋值操作。当状态为简单数据类型时,不会使用immer.produce,以保持代码的简洁性。更多关于immer.produce和combineReducers的使用和原理可参考官方文档。

       然而,immer插件的设计存在缺陷,即许多reducer配置若不能以数组形式存储,而是被替换,则可能导致插件配置失效。rematch v2版本通过引入更细粒度的plugin hooks(如onReducer)解决了这一问题,提升了配置的灵活性。

       紧接着是loading插件,专注于管理异步操作的状态,包括网络请求等。其核心在于onModel钩子的使用,定义了全局和模型级别的loading状态,并为特定操作定义了show和hide两个reducer,动态跟踪和控制加载状态。

       loading插件的实现通过初始化代码定义了全局和模型级别的loading状态,并使用onModel钩子处理模型操作,对特定的effect动作进行管理,包装原始动作以实现状态控制。两个reducer,show和hide,分别用于增加和减少操作状态的计数,以此实现对加载状态的动态更新。

       本文综述了rematch的immer和loading插件的实现原理、使用场景及优化策略,为开发者提供了深入理解这些工具的框架。后续文章将探讨rematch v1升级到v2的设计变化以及TypeScript支持的实现,期待与开发者共同探索rematch的最新进展和优化。