修改gulpfile
本库实现了一个更新对象的函数,同时随更新过程输出新旧对象的差异结构
在当前的前端形势下,不可变(Immutable)的概念开始出现在开发者的视野中,以不可变作为第一考虑的设计和实现会让程序普遍拥有更好的可维护性
而在不可变的前提下,我们不能对一个对象的属性进行直接的操作(赋值、修改、删除等),因此更新一个对象变得复杂:
let newObject = clone(source); newObject.foo = 1;
如果我们需要修改更深层次的属性,则会变得更为复杂:
let newObject = clone(source); newObject.foo = clone(newObject.foo); newObject.foo.bar = 1; // 有其它属性都需要依次操作
这是相当麻烦的,每次更新都会需要大量的代码,因此偷懒的话我们会用深克隆来搞定这事:
let newObject = deepClone(source); newObject.foo.bar = 1; // 其它修改
但是深克隆存在一些严重的问题:
基于此,社区上出现了一些用声明式的指令更新对象的辅助库,比如React Immutability Helpers,这些库封装了上面的逻辑,且选择了效率最优(仅复制未更新的属性,不需要深克隆)的方案
但是随之而来的一个问题是,当我们更新完一个对象,如何知道更新了什么?如果我们所有的针对更新的操作都在更新后立即进行,那么在编码时我们可以人为地基于更新指令进行:
let newObject = update(source, {foo: {$set: 1}}); view.globalDatasource = newObject; view.updateUserInterfaceOfFoo();
但现实中几乎不可能存在如此理想的场景,更多的时候我们仅仅拿到一个未知来源的newObject。如果根据newObject强制进行界面的完全刷新,自然会导致性能的损失。我们更希望找到对象更新前后的差异,可以针对性地进行后续的操作,因此就会引入diff这一概念,比如使用flibit/diff:
newObject
diff
let differences = diff(oldObject, newObject); for (let node of differences) { this.updateForPath(node.path, node.rhs); }
但是值得注意的是,差异分析本身是一个基于两个对象的深度遍历的操作,它是耗时的,在一个系统中引入这样一个环节必然会损失掉一定的性能
基于以上的原因,我们更希望有这样的一个库,它可以提供基本的对象更新的功能,且在更新的同时实时计算出对象前后的差异。因为更新的过程中知道更新的指令,所以可以在没有额外的遍历损耗的情况下直接得到差异,diffy-update库正是以此为目标而诞生的
diffy-update
diffy-update完全由ES2015+编写,如果环境无法满足要求,则在使用前需要添加对应的polyfill或shim,并使用babel进行编译,全局至少要包含Object.entries函数的实现
polyfill
shim
Object.entries
针对babel除es2015 preset外,至少需要function bind插件得以正常工作
babel
仅withDiff函数会提供差异对象:
withDiff
import {withDiff, isDiffNode} from 'diffy-update'; let source = { name: { firstName: 'Navy', lastName: 'Wong' } }; let [target, diff] = withDiff(source, {name: {firstName: {$set: 'Petty'}}}); console.log(target); // { // name: { // firstName: 'Pretty', // lastName: 'Wong' // } // } console.log(isDiffNode(diff.name.firstName)); // true console.log(diff); // { // name: { // firstName: { // changeType: 'change', // oldValue: 'Navy', // newValue: 'Pretty' // } // } // }
当前版本仅实现了针对基本类型和对象的差异计算,针对数组的差异计算将在后续版本中提供
差异对象的结构与输入的source对象相同,其中如果有一个属性有被修改,则该属性会变为一个“差异节点”,使用isDiffNode进行判断即可,如果一个属性为差异节点,则会仅包含以下属性:
source
isDiffNode
changeType
"add"
"remove"
"change"
oldValue
undefined
newValue
update模块的默认导出是withDiff函数的快捷方式,仅返回更新后的对象,不提供差异对象,可用于函数内部更新对象等常用场景
update
除此之外,本库还提供了一系列快捷函数,如set、push、unshift、merge、defaults等,这些函数可用于快速更新对象的某个属性,可以通过API文档进行查阅
set
push
unshift
merge
defaults
chain模块提供了链式更新一个对象的方法,使用方法如下:
chain
import chain from 'update/chain'; let source = { name: { firstName: 'Navy', lastName: 'Wong' }, age: 20, children: ['Alice', 'Bob'] }; let target = chain(source) .set(['name', 'firstName'], 'Petty') .set('age', 21) .push('children', 'Cary'); console.log(target); // { // name: { // firstName: 'Pretty', // lastName: 'Wong' // }, // age: 21, // children: ['Alice', 'Bob', 'Petty'] // }
在使用chain后得到的对象也有withDiff方法,可以同时获得更新后的对象和差异对象。
chain后的对象每次调用对应的更新方法(如set、push等),都会得到一个新的对象,原有的对象不会受影响,比如:
import chain from 'update/chain'; let source = { name: { firstName: 'Navy', lastName: 'Wong' }, age: 20, children: ['Alice', 'Bob'] }; let updateable = chain(source); let nameUpdated = updateable.set(['name', 'firstName'], 'Petty'); let ageUpdated = nameUpdated.set('age', 21); console.log(nameUpdated); // 注意age并没有受影响 // // { // name: { // firstName: 'Pretty', // lastName: 'Wong' // }, // age: 20, // children: ['Alice', 'Bob', 'Petty'] // }
chain是延迟执行的,所以假设已经对foo进行了操作,再对着foo.bar(或更深层级的属性)进行操作,会出现不可预期的行为,如以下代码:
foo
foo.bar
import chain from 'update/chain'; let source = { name: { firstName: 'Navy', lastName: 'Wong' }, age: 20, children: ['Alice', 'Bob'] }; let target = chain(source) .set('ownedCar', {brand: 'Benz'}) .merge('ownedCar', {type: 'C Class'}); // 注意ownedCar.type并没有生效 // // { // name: { // firstName: 'Pretty', // lastName: 'Wong' // }, // age: 20, // children: ['Alice', 'Bob', 'Petty'], // ownedCar: { // brand: 'Benz' // } // }
这并不会给你预期的结果,所以在使用链式调用的时候要注意每个指令的路径。
在一个完整的应用模型中,如果每一次对数据的操作都映射为后续的操作(如UI更新),则可能出现一些不可预期的问题:
所以在成熟的应用中,我们通常会在进行若干次数据变化后,根据整体的变化来进行后续的逻辑,这就要求每次变化产生的差异可以相互合并,生成一个最终的差异以供后续使用
diffy-update库提供了merge模块来支持差异的合并,我们可以使用mergeDiff函数将多次withDiff生成的差异进行合并:
mergeDiff
import {withDiff} from 'diffy-update/update'; import {mergeDiff} from 'diffy-upadte/merge'; let source = { age: 21, name: { firstName: 'Gray', lastName: 'Zhang' } }; let [ageUpdated, diffOnAge] = withDiff(source, {age: {$set: 22}}); let [nameUpdated, diffOnName] = withDiff(ageUpdated, {name: {firstName: {$set: 'Pretty'}}}); console.log(nameUpdated); // { // age: 22, // name: { // firstName: 'Pretty', // lastName: 'Zhang' // } // } console.log(diffOnAge); // { // age: { // changeType: 'change', // oldValue: 21, // newValue: 22 // } // } console.log(diffOnName); // { // name: { // firstName: { // changeType: 'change', // oldValue: 'Gray', // newValue: 'Pretty' // } // } // } // 注意要提供最先和最后的对象,即`source`和`nameUpdated`,中间过程产生的`ageUpdated`没用 let totalDiff = mergeDiff(diffOnAge, diffOnName, source, nameUpdated); console.log(totalDiff); // { // age: { // changeType: 'change', // oldValue: 21, // newValue: 22 // }, // name: { // firstName: { // changeType: 'change', // oldValue: 'Gray', // newValue: 'Pretty' // } // } // }
差异的合并是智能的,它包括:
需要注意的是,差异合并本身是一个消耗资源的计算工作(虽然很快),因此在实现上并不追求输出最小的差异集,而是在性能和正确性之间取得一个折衷,在可接受的速度之下输出相对优化后的差异结果
在使用diffy-update后,我们可以制作一个非常简易的UI-数据绑定模型,其基本逻辑为:
setImmediate
当然一个完整的模型需要更多的细节考虑,但大致思路如上所示,diffy-update在这一模型中作为底层的工具库,可以提供非常大的帮助
npm i npm run doc open doc/api/index.html
$change
diffNode
dist
$splice
splice
版权所有:中国计算机学会技术支持:开源发展技术委员会 京ICP备13000930号-9 京公网安备 11010802032778号
diffy-update
本库实现了一个更新对象的函数,同时随更新过程输出新旧对象的差异结构
为何要开发这个库
在当前的前端形势下,不可变(Immutable)的概念开始出现在开发者的视野中,以不可变作为第一考虑的设计和实现会让程序普遍拥有更好的可维护性
而在不可变的前提下,我们不能对一个对象的属性进行直接的操作(赋值、修改、删除等),因此更新一个对象变得复杂:
如果我们需要修改更深层次的属性,则会变得更为复杂:
这是相当麻烦的,每次更新都会需要大量的代码,因此偷懒的话我们会用深克隆来搞定这事:
但是深克隆存在一些严重的问题:
基于此,社区上出现了一些用声明式的指令更新对象的辅助库,比如React Immutability Helpers,这些库封装了上面的逻辑,且选择了效率最优(仅复制未更新的属性,不需要深克隆)的方案
但是随之而来的一个问题是,当我们更新完一个对象,如何知道更新了什么?如果我们所有的针对更新的操作都在更新后立即进行,那么在编码时我们可以人为地基于更新指令进行:
但现实中几乎不可能存在如此理想的场景,更多的时候我们仅仅拿到一个未知来源的
newObject。如果根据newObject强制进行界面的完全刷新,自然会导致性能的损失。我们更希望找到对象更新前后的差异,可以针对性地进行后续的操作,因此就会引入diff这一概念,比如使用flibit/diff:但是值得注意的是,差异分析本身是一个基于两个对象的深度遍历的操作,它是耗时的,在一个系统中引入这样一个环节必然会损失掉一定的性能
基于以上的原因,我们更希望有这样的一个库,它可以提供基本的对象更新的功能,且在更新的同时实时计算出对象前后的差异。因为更新的过程中知道更新的指令,所以可以在没有额外的遍历损耗的情况下直接得到差异,
diffy-update库正是以此为目标而诞生的使用
前置环境
diffy-update完全由ES2015+编写,如果环境无法满足要求,则在使用前需要添加对应的polyfill或shim,并使用babel进行编译,全局至少要包含Object.entries函数的实现针对
babel除es2015 preset外,至少需要function bind插件得以正常工作基本场景
仅
withDiff函数会提供差异对象:当前版本仅实现了针对基本类型和对象的差异计算,针对数组的差异计算将在后续版本中提供
差异对象的结构与输入的
source对象相同,其中如果有一个属性有被修改,则该属性会变为一个“差异节点”,使用isDiffNode进行判断即可,如果一个属性为差异节点,则会仅包含以下属性:changeType表示修改的类型,值为"add"、"remove"或者"change"oldValue表示修改前的值,如果changeType为"add"则值恒定为undefinednewValue表示修改后的值,如果changeType为"remove"则值恒定为undefined快捷方式
update模块的默认导出是withDiff函数的快捷方式,仅返回更新后的对象,不提供差异对象,可用于函数内部更新对象等常用场景除此之外,本库还提供了一系列快捷函数,如
set、push、unshift、merge、defaults等,这些函数可用于快速更新对象的某个属性,可以通过API文档进行查阅链式调用
chain模块提供了链式更新一个对象的方法,使用方法如下:在使用
chain后得到的对象也有withDiff方法,可以同时获得更新后的对象和差异对象。chain后的对象每次调用对应的更新方法(如set、push等),都会得到一个新的对象,原有的对象不会受影响,比如:chain是延迟执行的,所以假设已经对foo进行了操作,再对着foo.bar(或更深层级的属性)进行操作,会出现不可预期的行为,如以下代码:这并不会给你预期的结果,所以在使用链式调用的时候要注意每个指令的路径。
差异合并
在一个完整的应用模型中,如果每一次对数据的操作都映射为后续的操作(如UI更新),则可能出现一些不可预期的问题:
所以在成熟的应用中,我们通常会在进行若干次数据变化后,根据整体的变化来进行后续的逻辑,这就要求每次变化产生的差异可以相互合并,生成一个最终的差异以供后续使用
diffy-update库提供了merge模块来支持差异的合并,我们可以使用mergeDiff函数将多次withDiff生成的差异进行合并:差异的合并是智能的,它包括:
"add"后再进行"change"则会合并为一个"add""add"后"remove",或者多次"change"导致最终值并没有变化foo.bar变化后再修改foo,则会变成foo的整体变化需要注意的是,差异合并本身是一个消耗资源的计算工作(虽然很快),因此在实现上并不追求输出最小的差异集,而是在性能和正确性之间取得一个折衷,在可接受的速度之下输出相对优化后的差异结果
应用场景
在使用
diffy-update后,我们可以制作一个非常简易的UI-数据绑定模型,其基本逻辑为:setImmediate等函数完成withDiff方法更新数据,同时记录最初的数据对象、每一次的差异以及更新后的新数据对象mergeDiff生成最终的差异对象当然一个完整的模型需要更多的细节考虑,但大致思路如上所示,
diffy-update在这一模型中作为底层的工具库,可以提供非常大的帮助API文档
更新历史
2.0.0
$change属性改名为changeType,现在应该使用isDiffNode函数判断一个对象是否为差异节点2.1.0
update模块下的isDiffNode函数已标记为废弃,请使用diffNode模块下的该函数2.2.0
dist目录,可以直接使用2.3.0
2.4.0
$splice指令以及对应的splice快捷函数2.5.0
chain模块提供链式调用