Angular Pipe 在 嵌套对象上的非预期行为分析
场景
在工作中,存在一个嵌套对象,需要展示嵌套对象内层的一些信息,于是写了个Pipe
来处理,但是发现当嵌套的对象发生变化时,pipe
不会重新执行。例如有下面一个数据。
var feer = {
name: 'joe',
skills: [
{
name:'js'
},
{
name: 'ts'
}
]
}
我们想要的结果是把skills
里面的name
全部展示出来,以,
分割。
// component
export class AppComponent {
private skills = ['css', 'html', 'java', 'gulp']
name = 'Angular 6';
feer = {
name: 'joe',
skills: [
{
name: 'js'
},
{
name: 'ts'
}
]
}
add() {
const skill = this.skills.shift();
if (skill) {
this.feer.skills.push({
name: skill
})
}
}
}
// html
{{ feer.skills | defaultPure }}
// Pipe
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'defaultPure'
})
export class DefaultPurePipe implements PipeTransform {
transform(feer:any): string {
return feer.skills.map((v)=>v.name).join(',');
}
}
在这种情况下,如果调用add
引起 skills
发生变化,pipe
不会重新计算,显示的还是初始值 js,ts
。
问题
这里存在一个问题,对于上面👆例子中存在的嵌套结构,在 skills
变化的时候 ,transform
并没有重新执行。这里推测可能是 内部检测时候并不会做 deep-change-detection
(也就是不会关注 skills
里的变化),最终导致了上面的问题: skills
发生了变化,但 pipe
并没有重新执行计算变更输出结果。
下面是摘自官网的一段话:
Angular executes a pure pipe only when it detects a pure change to the input value. A pure change is either a change to a primitive input value (String
,Number
,Boolean
,Symbol
) or a changed object reference (Date
,Array
,Function
,Object
).Angular ignores changes within (composite) objects. It won't call a pure pipe if you change an input month, add to an input array, or update an input object property.
pipe
忽略了 对象内部复合对象的变动(如例子中的 skills
), angular
不会做深度检查,当我们调用 add
方法时候,往 skills
数组里 push
里一个对象,对于 angular
来说, skills
是“未”发生变化的,因为引用是一样的。
解决方案
在@Pipe
的 decorator 中除了name
还有一个叫 pure
类型为 boolean
的 metadata
,官方解释如下
If Pipe is pure (its output depends only on its input.) Normally pipe'stransform
method is only invoked when the inputs to pipe'stransform
method change. If the pipe has internal state (it's result are dependant on state other than its arguments) than setpure
tofalse
so that the pipe is invoked on each change-detection even if the arguments to the pipe do not change.
大意是 pipe
仅在输入发生改变的时候会再次执行 transform
方法。但是如果 pipe
有一个内部状态,并且输出依赖于这个内部状态,那么将 pure
设置为 false
以便在每次 change-detection
(数据更新检测) 时候执行 transform
即使输入的数据并没有改变。
对于pure
, 默认值为 true
,如果设置为 false
,则可以将上面的功能实现出来。但是同时要注意一个问题,一个 impure
的 pipe
(即 pure
= false ) 会经常被调用,如果有大量运算,则可能影响用户体验 。
同样看一段官网的解释:
Angular executes an impure pipe during every component change detection cycle. An impure pipe is called often, as often as every keystroke or mouse-move.
对于 impure
的 pipe
,angular
会在每次 component
的 变化检测周期里调用执行,甚至一次按键、一次鼠标事件都会触发 pipe
的执行。
最终,对于上述的问题,只需修改 pipe
的代码即可。
@Pipe({
name: 'defaultPure',
pure: false
})
export class DefaultPurePipe implements PipeTransform {
transform(feer:any): string {
return feer.skills.map((v)=>v.name).join(',');
}
}
完结
对于上述问题,表面上来说加一下 pure
即可,对于深层次来说,涉及到了 angular
内部的一些工作原理。但是话说回来,目前仅仅研究到这种方案来处理,实际上应该还会有其他的解决方案,大胆猜测可以利用 change-detection
的一些钩子来处理,待研究好了再记录一下。
之前在遇到这个问题的时候,也是一脸懵逼的找答案,殊不知答案就在文档上写的清清楚楚。 🤷♀️~~~
BTW,其实在 vue
中也存在了一个类似的问题, 为了性能等的考虑,不会对复合对象做深度变更检测。而 Vue
的做法则简单粗暴一些:重新装饰数组方法。 具体可以查看 Vue
的 源代码 。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。