在Angular中编写自定义管道时,可以指定是定义纯管道还是不纯管道:
@Pipe({ name: 'myCustomPipe', pure: false/true <----- here (default is `true`)})export class MyCustomPipe {}
Angular在管道上有一个非常好的文档,你可以在这里。 但是,正如文件经常发生的情况一样,没有明确的划分理由。 在这篇文章中,我想填补这个空白,并展示了函数式编程的前景,它展示了纯和不纯的管道来自何处。 除了学习差异,你会知道它是如何影响性能,这些知识将帮助你编写高效和高性能的管道。
A pure function
网上有很多关于函数式编程的信息,可能每个开发人员都知道纯函数是什么。 对于我自己,我将一个纯函数定义为一个没有内部状态的函数。 这意味着它执行的所有操作都不受状态的影响,给出相同的输入参数产生相同的确定性输出。
以下是添加数字的两个版本的函数。 第一个是纯的,第二个是不纯的:
const addPure = (v1, v2) => { return v1 + v2;};const addImpure = (() => { let state = 0; return (v) => { return state += v; }})();
如果我用相同的输入调用两个函数,比如说数字1,则第一个函数在每次调用时都会产生相同的输出2:
addPure(1, 1); // 2addPure(1, 1); // 2addPure(1, 1); // 2
而第二个产生不同的输出:
addImpure(1); // 1addImpure(1); // 2addImpure(1); // 3
所以这里的关键是即使输入不变,不纯的函数也会产生不同的输出。 这意味着我们不能使用输入值来确定输出是否会改变。
纯函数:
- 输入参数值决定了输出,所以如果输入参数不改变,输出不会改变
- 可以多次调用,而不会影响输出结果
非纯函数:
- 不能使用输入值来确定输出是否会改变
- 不能共用,因为内部状态会受到外界的影响
Applying that knowledge to Angular pipes
假设我们定义了一个自定义管道,纯管道:
@Pipe({ name: 'myCustomPipe', pure: true})export class MyCustomPipe {}
在组件模板中像这样使用它:
{ {v1 | customPipe}}{ {v2 | customPipe}}
因为管道是纯的,这意味着没有内部状态,管道可以被共享。Angular是怎样实现的?尽管模板中有,但它可以只创建一个管道实例,该实例可以在用法之间共享。
对于那些从我以前的文章中了解到组件工厂的人来说,这里有一个相关的编译代码,它只定义了一个管道定义:function View_AppComponent_0(_l) { return viewDef_1(0, [ pipeDef_2(0, ExponentialStrengthPipe_3, []), // node index 0 ...
这是在updateRenderer函数中共享的:
function(_ck,_v) { unwrapValue_7(_v,4,0,_ck(_v,5,0,nodeValue_8(_v, 0),...); ^^^ unwrapValue_7(_v,8,0,_ck(_v,9,0,nodeValue_8(_v, 0),...); ^^^
这里是unwrapValue函数用于通过调用transform来检索当前的管道值。 管道实例由nodeValue函数调用中的节点索引引用 - 在这种情况下为0。
但是,如果我们将管道定义为不纯,假设有一些内部状态:
@Pipe({ name: 'myCustomPipe', pure: false})export class MyCustomPipe {}
我们不希望第二次使用中的管道在第一次使用时受到其调用的影响,所以Angular会创建两个管道实例,每个实例都有自己的状态:
function View_AppComponent_0(_l) { return viewDef(0, [ ... pipeDef_2(0, ExponentialStrengthPipe, []) // node index 4 ... pipeDef_2(0, ExponentialStrengthPipe, []) // node index 8
并且它在updateRenderer函数中不共享:
function(_ck,_v) { unwrapValue_7(_v,4,0,_ck(_v,5,0,nodeValue_8(_v, 4),...); ^^^ unwrapValue_7(_v,8,0,_ck(_v,9,0,nodeValue_8(_v, 8),...); ^^^
你可以在这里看到,对于每个用法Angular使用不同的节点索引 - 4和8来代替节点索引0。
第一章总结的第二点是,用纯函数我们可以用输入值来确定输出是否会变化,而不纯的函数则不能保证。
在Angular中,我们将输入参数传递给管道,如下所示:
{ {v1 | customPipe:param1:param2}}
所以如果一个管道是纯的,我们知道它的输出(通过变换方法)是由输入参数严格确定的。 如果输入参数不改变,输出不会改变。 这个推理允许Angular只有在输入参数改变时才优化管道和调用变换方法。
但是,如果管道不纯并且具有内部状态,则相同的参数不能保证相同的输出。 这意味着Angular被迫在每个摘要上的管道实例上触发转换函数。
不纯管道的一个很好的例子是来自@ angular / common包的AsyncPipe。 此管道具有内部状态,该状态持有通过订阅传递给管道的observable作为参数创建的基础订阅。 由于Angular必须为每个管道使用创建一个新实例,以防止不同的可观察对象相互影响。 而且每个摘要上还必须调用变换方法,因为甚至认为可观察参数可能不会改变新的值,可能通过这个需要被变换检测处理的可观察到。
另外两个不纯的管子是JsonPipe和SlicePipe。 Angular对管道施加了额外的限制,被认为是纯粹的 - 管道的输入是不可变的。 如果输入是可变的,则需要在每个摘要上重新评估管道,因为可以在不更改对象引用(管道参数保持不变)的情况下改变输入对象。 这就是为什么尽管没有内部状态,JsonPipe和SlicePipe管道都不被认为是纯粹的。
其余的Angular默认管道是纯的。
Conclusion
所以,正如我们所看到的,如果不明智和谨慎使用不纯的管道,可能会有明显的性能下降。 性能受到影响的原因是Angular创建了不纯的管道的多个实例,并且在每个摘要循环中将其称为变换方法。
我希望通过阅读文章,您现在知道两种类型之间的区别,在设计和实现自定义管道时,Angular如何处理这两种类型,以及您应该使用哪种心智模型。