import {update, map} from 'san-update';
let source = {
count: 1,
values: [1, 2]
};
update(source, {values: {$map: value => value + 1}});
map(source, 'values', value => value + 1);
// {
// count: 1,
// values: [2, 3]
// }
update(source, {count: {$map: value => value + 1}});
map(source, 'count', value => value + 1);
// Error: Usage of $map command on non array object is forbidden.
$filter
用于对类型为数组的属性进行filter操作,如果属性的类型不是数组,则会抛出异常
import {update, filter} from 'san-update';
let source = {
count: 1,
values: [1, 2]
};
update(source, {values: {$filter: value => value > 1}});
filter(source, 'values', value => value > 1);
// {
// count: 1,
// values: [2]
// }
update(source, {count: {$filter: value => value > 1}});
filter(source, 'count', value => value > 1);
// Error: Usage of $filter command on non array object is forbidden.
$reduce
用于对类型为数组的属性进行reduce操作,如果属性的类型不是数组,则会抛出异常
import {update, reduce} from 'san-update';
let source = {
count: 1,
values: [1, 2]
};
update(source, {values: {$reduce: [(sum, value) => sum + value, 0]}});
// 也支持没有initialValue
reduce(source, 'values', (sum, value) => sum + value);
// {
// count: 1,
// values: 3
// }
update(source, {count: {$reduce: (sum, value) => sum + value}});
reduce(source, 'count', (sum, value) => sum + value);
// Error: Usage of $reduce command on non array object is forbidden.
san-update
本库来源于diffy-update,在此基础上梳理了API并移除了差异计算功能,使之成为纯粹的对象更新工具,并纳入san体系中
本库实现了一个更新对象的函数,同时随更新过程输出新旧对象的差异结构
为何要开发这个库
在当前的前端形势下,不可变(Immutable)的概念开始出现在开发者的视野中,以不可变作为第一考虑的设计和实现会让程序普遍拥有更好的可维护性
而在不可变的前提下,我们不能对一个对象的属性进行直接的操作(赋值、修改、删除等),因此更新一个对象变得复杂:
如果我们需要修改更深层次的属性,则会变得更为复杂:
这是相当麻烦的,每次更新都会需要大量的代码,因此偷懒的话我们会用深克隆来搞定这事:
但是深克隆存在一些严重的问题:
基于此,社区上出现了一些用声明式的指令更新对象的辅助库,比如React Immutability Helpers,这些库封装了上面的逻辑,且选择了效率最优(仅复制未更新的属性,不需要深克隆)的方案
但是社区的库普遍存在一些问题,如:
set、push、unshift、merge等功能外,其它功能难以方便地补充san-update希望在社区经验的基础之上,通过提供更强大的功能和方便的使用方式(如链式调用)来简化基于不可变对象的系统开发使用
前置环境
san-update完全由ES2015+编写,如果环境无法满足要求,则在使用前需要添加对应的polyfill或shim,并使用babel进行编译。针对
babel至少需要es2015 preset基本场景
update函数可以提供对象更新的功能:快捷方式
除此之外,本库还提供了一系列快捷函数,如
set、push、unshift、merge、defaults等,这些函数可用于快速更新对象的某个属性,可以通过API文档进行查阅applyWith
除与
update能接受的指令相同的快捷函数之外,还提供一个applyWith函数,该函数的声明如下:这个函数与
apply功能类似,区别是可以通过selectors属性指定一个或多个选择器,每个选择器接收source对象并返回一个值,这些值将作为factory函数的前n个参数,并加上需要更新的属性的当前值作为最后一个参数调用factory函数,将函数返回值作为属性的新值需要注意的是,
applyWith函数仅通过快捷方式提供,在update、macro和chain模块上均没有该功能。可用指令
以下指令可以在
update方法中使用,同时也有同名的对应快捷方式函数$set
用于将属性的值设置为提供的新值
$push
用于在类型为数组的属性末尾增加一个元素,如果属性的类型不是数组,则会抛出异常
$unshift
用于在类型为数组的属性头部增加一个元素,如果属性的类型不是数组,则会抛出异常
$pop
用于移除类型为数组的属性的最后一个元素,如果属性的类型不是数组,则会抛出异常
$pop指令对应的值可以为一个boolean类型的值,当其值为true时会执行移除操作,值为false时则不进行更新。同时值也可以为一个函数,该函数接受属性当前的值并返回一个boolean类型的值用于判断是否进行更新$shift
用于移除类型为数组的属性的第一个元素,如果属性的类型不是数组,则会抛出异常
$shift指令对应的值可以为一个boolean类型的值,当其值为true时会执行移除操作,值为false时则不进行更新。同时值也可以为一个函数,该函数接受属性当前的值并返回一个boolean类型的值用于判断是否进行更新$removeAt
用于移除类型为数组的属性中指定位置的元素,如果属性的类型不是数组,则会抛出异常
如果提供的索引在数组范围之外(值为负或超出数组长度),则不会进行更新
$remove
用于移除类型为数组的属性中的指定元素,如果属性的类型不是数组,则会抛出异常
如果数组中出现多次指定的元素,则只会移除第一个,如果未找到元素,则不进行更新
$splice
用于对类型为数组的属性进行splice操作,如果属性的类型不是数组,则会抛出异常
$splice指令接收的值为一个数组,其内容与Array#splice方法相同,分别为[start, deleteCount, ...items]$map
用于对类型为数组的属性进行map操作,如果属性的类型不是数组,则会抛出异常
$filter
用于对类型为数组的属性进行filter操作,如果属性的类型不是数组,则会抛出异常
$reduce
用于对类型为数组的属性进行reduce操作,如果属性的类型不是数组,则会抛出异常
$merge
用于在属性中合并相同的键值,
$merge指令使用浅合并$defaults
用于在向属性中不存在的键填充值,
$defaults指令使用浅合并$apply
用于通过一个函数对属性的值进行更新,提供的函数接收属性当前值作为唯一的参数,其返回值作为属性的新值
$omit
用于移除一个属性
$omit指令对应的值可以为一个boolean类型的值,当其值为true时会执行移除操作,值为false时则不进行更新。同时值也可以为一个函数,该函数接受属性当前的值并返回一个boolean类型的值用于判断是否进行更新$composeBefore
用于更新类型为函数的属性,将该函数进行封装,封装后的函数先执行
$composeBefore指令接收的函数,后续使用该函数返回的值作为参数执行原函数。如果属性的类型不是函数,则会抛出异常$composeAfter
用于更新类型为函数的属性,将该函数进行封装,封装后的函数先执行原函数,后续使用原函数返回值作为参数执行
$composeAfter指令接收的函数。如果属性的类型不是函数,则会抛出异常链式调用
chain模块提供了链式更新一个对象的方法,可以使用chain或者immutable来引入这一函数,使用方法如下:链式调用后的对象每次调用对应的更新方法(如
set、push等),都会得到一个新的对象,原有的对象不会受影响,比如:chain是延迟执行的,所以假设已经对foo进行了操作,再对着foo.bar(或更深层级的属性)进行操作,会出现不可预期的行为,如以下代码:这并不会给你预期的结果,所以在使用链式调用的时候要注意每个指令的路径。
使用builder构建更新函数
当一个更新指令会被经常使用时,我们常用的方式是将这个指令保存为一个常量,以避免每一次构建指令的消耗。但是任何对象更新库的指令都是一个不容易理解的底层数据结构,因此为了更方便直观地构建一个可被反复使用的更新对象的函数,
san-update提供builder功能来声明更新的函数。builder的使用方式和链式调用相似,区别在于构造时不需要传入待更新的对象,而其最终返回的函数则是一个接收待更新对象的函数。builder#build返回的更新函数上还附有withDiff函数,可以使用该函数生成差异对象。除此之外,也可以使用builder#buildWithDiff直接返回带有差异功能的更新函数。与链式调用相同,
builder的每一个操作都会生成一个全新的对象,原有的对象不会受到影响函数式编程
san-update/fp模块提供了与快捷方式同名的一系列函数,这些函数与快捷方式不同的是不接收source参数,而是先使用其它参数生成一个更新用的函数,随后再调用该函数提供source参数来获得更新后的对象。这些函数可以很方便地用于函数式编程:上面的代码等效于以下直接使用快捷函数的代码,区别在于
setName可以被多次复用:使用
flow或者compose等函数组合的功能可以非常方便地构建出一个完整表达业务逻辑的更新函数:差异获取
withDiff函数可在更新对象的同时提供一个新旧对象的差异:差异是一个与更新的对象结构相似的对象,当一个属性有更新时,该属性下会存在名为
$change的属性,该属性标识这个对象有更新的同时也提供了更新的形式,其值可分为"change"、"add"或"remove"。因此,为了避免后续对一个差异对象的结构如下:
其中
oldValue和newValue标记更新前后的值,当$change为"remove"时newValue的值恒定为undefined,当$change为"add"时则oldValue的值恒定为undefined。因此,为了避免后续对如果使用
push、unshift、splice、pop、shift、removet、removeAt指令对数组进行了操作,则会在差异对象中生成一个splice属性,其中的index、deleteCount、insertions表达了更新的位置、删除的数量、插入的新元素。需要注意的是如果使用apply、set等操作对数组进行更新则不会有splice属性产生,数组将被当作普通的对象仅提供新旧值。细节
关于无效更新
假设有一个对象:
对其进行一次更新,但并没有修改其任何属性的值:
这样的更新被称为“无效更新”。
在
san-update中,无效更新会返回一个全新的对象,即bar === foo为false。san-update尊重JavaScript这一语言的弱类型特性,因此不会假设使用了本库的环境是完全Immutable的,即不去假设foo.x = 2这样的代码永不存在。因此,为了避免后续对foo的非Immutable修改影响到bar的结构和内容,san-update会在任何时候都创建一个新的对象,无论这一更新是否为无效更新。关于属性缺失
当对一个对象进行深度的操作时,可能会遇上属性缺失的情况:
如上代码,
foo对象并没有x属性,此时对x.y进行修改会在路径访问上出现一个空缺。在React Immutability Helpers中,这样的操作是会导致异常的。但在
san-update中并不会,san-update会补齐访问路径中的所有属性,即上面的代码会正常返回一个{x: {y: 1}}的对象。也因为这一策略,
san-update无法成为一个校验静态类型结构的工具,如果与系统配合使用,还需要自行选择react-types或JSON Schema等工具确保类型的静态性和正确性。但需要注意的是,
san-update只会用对象({})去补上不存在的属性,因此如果你期望一个数组的话,san-update得到的结果会与你的预期不符:API文档
更新历史
2.1.0
pop、shift、removeAt、remove函数applyWith快捷函数fp模块2.0.0
本版本源码与2.0.0-rc.1一到
2.0.0-rc.1
withDiff函数提供计算对象差异的功能$pop、$shift、$remove、$removeAt指令"foo.bar[2]"的字符串表达属性的访问路径macro添加别名builder和updateBuilder$slice指令$invoke指令更名为$apply1.4.0
$map、$filter、$reduce、$slice、$composeBefore、$composeAfter指令1.3.0
$push、$unshift、$splice指令将抛出异常$merge指令在原属性存在和不存在时的行为差异1.2.0
macro功能用于构建更新函数1.1.1
omit快捷函数未导出的BUG1.1.0
immutable作为chain的别名clone函数不会复制原型属性的错误omit指令1.0.0
withDiff及其相关功能