反转 Angular 2 *ngFor

92
<li *ngFor="#user of users ">
    {{ user.name }} is {{ user.age }} years old.
</li>

是否有可能颠倒 ngFor 的顺序,以使项目从底部添加?

14个回答

128
你可以直接在数组上使用 JavaScript 的 .reverse() 方法。不需要使用 Angular 特定的解决方案。
<li *ngFor="#user of users.slice().reverse() ">
   {{ user.name }} is {{ user.age }} years old.
</li>

查看更多内容: https://www.w3schools.com/jsref/jsref_reverse.asp


7
在使用 Angular v4.1 进行编译时,会出现一个错误,即数组在每次组件检查时都会被还原。建议使用管道进行修复。 - ant45de
9
这个 bug 也会在管道中出现,你需要使用 .slice().reverse() 来避免影响原始数组。(可以在管道或模板中使用) - Daniel Swiegers
正确获取数组的降序方法是使用管道。我用过这种方法,但有时它不起作用。 - ImFarhad
1
为什么要在这里使用slice()?如果我们不使用slice()会发生什么?(假设没有错误的话) - Satish Patro
2
为了避免在每个 Angular 变更检测周期中调用 slice()reverse() 方法,建议使用管道。 - Ploppy

106

你需要实现一个自定义管道,利用 JavaScript 数组的 reverse 方法:

import { Pipe, PipeTransform } from '@angular/core';

@Pipe({ name: 'reverse' })

export class ReversePipe implements PipeTransform {
  transform(value) {
    return value.slice().reverse();
  }
}

您可以这样使用:

<li *ngFor="let user of users | reverse">
    {{ user.name }} is {{ user.age }} years old.
</li>

别忘了在你的组件的pipes属性中添加管道。


54
谢谢,不需要一根管子。我只是在我的for语句中使用了.slice().reverse() - daniel
5
我认为您只想将内容翻转后进行显示,同时保持数据数组不变......这就是我回答的原因;-) - Thierry Templier
8
slice()方法返回一个新的数组,因此@zoidbergi的评论确保了在组件中数据数组保持不变,只影响视图。 - Mark Rajcok
1
小更新。ngFor语法现在使用“let”而不是井号。应该是ngFor =“let user of users | reverse”,而不是。 - Pat M
5
在模板中应避免使用方法调用,因为这种方式与变更检测不太兼容。换句话说,Angular 可能会调用 .slice().reverse() 的次数远超合理范围。在这里,管道是正确的选择,即使不使用管道也可以正常工作。 - Avius
显示剩余3条评论

18

这样应该就可以解决问题了

<li *ngFor="user of users?.reverse()">
  {{ user.name }} is {{ user.age }} years old.
</li>

10
相反,你应该使用 users.slice().reverse()(参见上面的答案)。如果没有 slice,则用户数组会在每次视图更新时反转,并且会出现意外行为! - bersling

16

令人惊讶的是,经过这么多年后,仍然没有一个解决方案被发布在这里,它没有严重的缺陷。这就是为什么在我的回答中,我将解释所有当前解决方案存在的缺点,并提出两种我认为更好的解决此问题的新方法。


当前解决方案的缺点

每个解决方案都有现场演示。请查看说明下的链接。

1. 纯管道与 slice().reverse() (目前被接受)

——由@Thierry Templier@rey_coder提出(自ngx-pipes 使用此方法以来)

@Pipe({ name: 'reverse' })
export class ReversePipe implements PipeTransform {
  transform(value) {
    return value.slice().reverse();
  }
}

管道默认为纯净, 因此Angular希望它们在给定相同输入时产生相同的输出。这就是为什么transform()方法不会在您更改数组时再次调用,除非您每次想要更新旧数组时都创建一个新数组,例如通过编写

this.users = this.users.concat([{ name, age }]);

替代

this.users.push({ name, age });

但仅因为您的管道需要它而这样做是一个坏主意。

因此,如果您不打算修改数组,则此解决方案仅适用。

在实时演示中,您会发现只有重置按钮起作用,因为单击它时会创建一个新的(空)数组,而添加按钮仅会改变现有数组。

实时演示

2. 不使用管道的slice().reverse()

— 由@André Góis, @Trilok Singh@WapShivam提供

<li *ngFor="let user of users.slice().reverse()">
   {{ user.name }} is {{ user.age }} years old.
</li>

在这里,我们使用相同的数组方法,但不使用管道,这意味着Angular将在每个变更检测周期调用它们。数组的更改现在将反映在UI中,但问题在于slice()和reverse()可能会被过度调用,因为数组更改并不是唯一可以导致变更检测运行的事情。
请注意,如果我们修改先前解决方案的代码使管道变得不纯,则会获得相同的行为。我们可以通过在传递给Pipe装饰器的对象中设置pure:false来实现这一点。我在实时演示中这样做是为了能够在控制台中记录一些内容,每当slice()和reverse()将被调用时。我还添加了一个按钮,让您触发变更检测而不更改数组。点击后,您将在控制台中看到slice()和reverse()被调用,尽管数组没有被修改,这是我们不想要的理想解决方案。尽管如此,这个解决方案是当前所有解决方案中最好的。

实时演示

3. 通过缓存反转数组来提高性能

— 由@Günter Zöchbauer提供

@Pipe({ name: 'reverse', pure: false })
export class ReversePipe implements PipeTransform {
  constructor(private differs: IterableDiffers) {
    this.differ = this.differs.find([]).create();
  }

  transform(value) {
    if (this.differ.diff(value)) {
      this.cached = value.slice().reverse();
    }
    return this.cached;
  }
}

这是一个聪明的尝试来解决我在上一节中描述的问题。在这里,只有当数组内容实际上发生了改变时,才会调用slice()reverse()。如果它们没有改变,就会返回上次操作的缓存结果。
为了检查数组是否发生了改变,使用了Angular的IterableDiffer对象。这样的对象在Angular内部的变化检测过程中也被使用。同样,您可以在实时演示中检查控制台以查看它实际上是如何工作的。
然而,我认为这种解决方案并没有任何性能提升。相反,我认为这种解决方案使情况变得更糟。原因是它需要花费线性时间来发现数组没有改变,这与调用slice()reverse()一样糟糕。如果它已经改变,最坏情况下的时间复杂度仍然是线性的,因为在某些情况下,在检测到更改之前必须遍历整个数组。例如,当修改了数组的最后一个元素时就会发生这种情况。在这种情况下,整个数组将不得不被遍历两次:一次是调用区别函数,另一次是调用slice()reverse(),这太糟糕了。

演示实例

4. 仅使用reverse()

— 由@Prank100@Dan提供

<li *ngFor="let user of users.reverse()">
   {{ user.name }} is {{ user.age }} years old.
</li>

这个解决方案是绝对不可行的。原因在于reverse()会改变数组本身,而不是创建一个新的数组,这可能是不期望的,因为您可能需要在其他地方使用原始数组。如果您不需要,请在应用程序中收到数组后立即反转它,就不必再担心它了。
还有一个更严重的问题,尤其是在开发模式下看不到,非常危险。在生产模式下,您的数组将在每个变更检测周期中被反转,这意味着像[1,2,3]这样的数组将不断在[1,2,3][3,2,1]之间切换。你肯定不想要那样。
在开发模式下看不到它,因为在该模式下,Angular会执行每次变更检测运行后的额外检查,导致反转的数组再次反转,使其恢复到原始顺序。第二次检查的副作用没有反映在UI中,这就是为什么一切似乎正常工作,但实际上并非如此。

现场演示 (生产模式)


替代方案

1. 仅使用数组索引

// In the component:
userIdentity = i => this.users[this.users.length - 1 - i];

<li *ngFor="let _ of users; index as i; trackBy:userIdentity">
  {{ users[users.length - 1 - i].name }} is {{ users[users.length - 1 - i].age }} years old.
</li>

我们不是获取用户,而是只获取索引i并使用它来获取数组的第i个最后一个元素。(从技术上讲,我们也会获取用户,但我们使用变量名_表示我们不打算使用该变量。这是一个非常常见的约定。)

多次编写users[users.length - 1 - i]很麻烦。我们可以使用this trick来简化代码:

<li *ngFor="let _ of users; index as i; trackBy:userIdentity">
  <ng-container *ngIf="users[users.length - 1 - i] as user">
    {{ user.name }} is {{ user.age }} years old.
  </ng-container>
</li>

这样,我们创建了一个本地变量,可以在<ng-container>中使用。请注意,它只能正常工作,如果您的数组元素不是假值,因为我们在该变量中保存的实际上是*ngIf指令的条件。
还要注意,我们必须使用自定义的TrackByFunction,如果您不想花费大量时间进行调试,那么绝对是必需的,即使我们的简单示例似乎可以正常工作而无需它。 演示

2. 返回可迭代对象的纯管道

@Pipe({ name: 'reverseIterable' })
export class ReverseIterablePipe implements PipeTransform {
  transform<T>(value: T[]): Iterable<T> {
    return {
      *[Symbol.iterator]() {
        for (let i = value.length - 1; i >= 0; i--) {
          yield value[i];
        }
      }
    };
  }
}

*ngFor 循环内部使用 可迭代对象,所以我们只需使用管道构造一个满足我们需求的对象。我们对象的 [Symbol.iterator] 方法是一个 生成器函数,它返回迭代器,让我们能够以相反的顺序遍历数组。如果您第一次听到可迭代对象或者对星号语法和 yield 关键字不熟悉,请阅读 本指南

这种方法的优点在于,我们只需要创建一个可迭代对象来处理数组。如果我们对数组进行更改,应用程序仍将正常工作,因为生成器函数每次调用时都会返回一个新的迭代器。这意味着我们只需要在创建新数组时创建一个新的可迭代对象,因此纯管道就足够了,在使用 slice()reverse() 的解决方案中,这使得该解决方案更好。
我认为这比以前的解决方案更加优雅,因为我们不必在 *ngFor 循环中使用数组索引。实际上,除了添加管道之外,我们不需要更改组件中的任何内容,因为整个功能都包含在其中。
<li *ngFor="let user of users | reverseIterable">
  {{ user.name }} is {{ user.age }} years old.
</li>

此外,不要忘记在您的模块中导入和声明管道。请注意,这次我将管道命名为不同的名称,以表明它仅适用于足够使用可迭代对象的上下文中。如果其他管道期望其输出为数组,则不能将其应用于其输出,并且不能像使用普通数组一样通过在模板中编写{{ users | reverseIterable }}来打印反转的数组,因为可迭代对象没有实现toString()方法(尽管如果在您的用例中有意义,则可以实现它)。这是与具有slice()和reverse()的管道相比的劣势,但如果您想要在*ngFor循环中使用它,那么它绝对更好。可能可以通过一个有状态的纯管道进行优化。 在线演示

可能的优化 - 使用带有状态的纯管道

"有状态的纯管道"听起来像是个自相矛盾的词汇,但如果我们使用Angular的相对较新的编译和渲染引擎Ivy,实际上就可以创建这样的管道。在引入Ivy之前,即使我们在模板中多次使用一个纯管道,Angular也只会创建一个实例。这意味着transform()方法总是会在同一个对象上被调用。因此,我们不能依赖于存储在该对象中的任何状态,因为它在模板中所有出现的管道之间是共享的。"

然而,Ivy采用了一种不同的方法,为每个出现的实例创建一个单独的管道实例,这意味着我们现在可以使纯管道具有状态。为什么我们要这样做呢?嗯,我们当前解决方案的问题是,虽然我们在修改数组时不必创建新的可迭代对象,但在创建新的可迭代对象时仍然必须这样做。我们可以通过将数组存储在管道实例的属性中,并使可迭代对象依赖于该属性而不是传递给transform()方法的数组来解决这个问题。因此,每当调用transform()时,我们只需要将新数组分配给现有属性,然后返回现有的可迭代对象即可。

@Pipe({ name: 'reverseIterable' })
export class ReverseIterablePipe implements PipeTransform {
  private array: any[] = [];
  private reverseIterable: Iterable<any>;

  constructor() {
    this.reverseIterable = {
      [Symbol.iterator]: function*(this: ReverseIterablePipe) {
        for (let i = this.array.length - 1; i >= 0; i--) {
          yield this.array[i];
        }
      }.bind(this)
    };
  }

  transform<T>(value: T[]): Iterable<T> {
    this.array = value;
    return this.reverseIterable;
  }
}

请注意,我们必须将生成器函数绑定this,否则在其他上下文中this可能具有不同的含义。通常我们可以使用箭头函数来解决这个问题,但是对于生成器函数并没有这样的语法,所以我们必须显式地绑定它们。
从Angular 9版本开始,默认启用Ivy,但我在实时演示中禁用了它,以便向您展示为什么我们不能在没有它的情况下使纯管道具有状态。为了使应用程序正常工作,您需要在tsconfig.json文件中将angularCompilerOptions.enableIvy设置为true实时演示

16

所选答案存在两个问题:

首先,除非在管道的属性中添加pure: false,否则管道将无法注意到源数组的修改。

其次,管道不支持双向绑定,因为它复制的是数组的副本而非数组本身。

最终代码如下:

    import {Pipe} from 'angular2/core';

    @Pipe({
      name: 'reverse',
      pure: false
    })
    export class ReversePipe {
      transform (values) {
        if (values) {
          return values.reverse();
        }
      }
    }

Plunker

http://plnkr.co/edit/8aYdcv9QZJk6ZB8LZePC?p=preview


也测试过 Angular 4。 - Dan
1
抛出异常。在修改数组之前,必须将其复制,就像Thierry的答案中一样。 - zamber
拥有一个纯管道的目的是为了让Angular不会在每个变更检测周期中都执行它。这并不意味着源代码的更改不会被检测到,而是意味着Angular可以信任,如果源代码是稳定的,那么管道的输出也是稳定的。其次,双向绑定?我不建议管道直接修改源对象。除非你只使用值类型,否则修改数组中的项(例如通过传递给子组件等方式)应该按预期工作。 - Serge

9

更新

@Pipe({
  name: 'reverse',
  pure: false
})
export class ReversePipe implements PipeTransform {
  constructor(private differs: IterableDiffers) {
    this.differ = this.differs.find([]).create();
  }

  transform(value) {
    const changes = this.differ.diff(value);
    if (changes) {
      this.cached = value.slice().reverse();
    }
    return this.cached;    
  }
}

默认情况下,Angular只有在数组引用被更改时才调用管道的transform()方法,因此当向数组添加或删除项目时,它不会被调用,这意味着对数组的更改不会反映在UI中。
为了让管道在每次变更检测调用时都得到调用,我将管道设置为不纯的。不纯的管道将经常被调用,因此它的工作效率非常重要。创建数组的副本(甚至是大型数组),然后颠倒其顺序是相当昂贵的。
因此,区别器只在识别到一些更改时才执行实际工作,否则返回上一次调用的缓存结果。
您可以创建一个自定义管道,返回逆序排列的数组,或者直接提供以相反顺序排列的数据。
另请参见: - angular 2 sort and filter - https://developer.mozilla.org/de/docs/Web/JavaScript/Reference/Global_Objects/Array/reverse

我在编写测试规范方面遇到了麻烦。希望您能够帮助回答我关于此事的问题?https://dev59.com/GKPia4cB1Zd3GeqPtyRv - Jonathon Oates
@JonathonOates 我没有使用TS,也不太了解TS测试相关的内容。我相信其他人会能够提供帮助的。 - Günter Zöchbauer
1
在构造函数中,变量 value 未定义。 - Ashok Koyi
1
另外,IterableDiffer 现在是泛型的了。需要类型参数。 - Ashok Koyi
@Kalinga 我自己不活跃地使用 TS。我不知道在哪里为 diff 添加类型参数。无论如何,感谢你的提示。 - Günter Zöchbauer
我不确定缓存数组是否会提高性能,实际上我认为它甚至会使性能变差。原因是查找数组是否已更改需要与调用slice()一样的时间,这需要线性时间。如果数组已更改,则在最坏情况下必须遍历整个数组,然后才能检测到更改。最坏情况可能发生在数组的最后一个元素被更改时。在这种情况下,整个数组将不得不被遍历两次:一次在调用差异函数时,一次在调用slice()时,这非常糟糕。 - aweebit

7
可以使用 ngx-pipes npm install ngx-pipes --save 模块
import {NgPipesModule} from 'ngx-pipes';

@NgModule({
 // ...
 imports: [
   // ...
   NgPipesModule
 ]
})

组件
@Component({
  // ..
  providers: [ReversePipe]
})
export class AppComponent {
  constructor(private reversePipe: ReversePipe) {
  }
}

然后在模板中使用:<div *ngFor="let user of users | reverse">

7
尝试这个:
<ul>
  <li *ngFor="let item of items.slice().reverse()">{{item}}</li>
</ul>

1
好主意。我不想为一个使用场景创建一个管道。 - Aaron

4

您可以使用JavaScript函数来实现。

<li *ngFor="user of users?.slice()?.reverse()">
    {{ user.name }} is {{ user.age }} years old.
</li>

使用 ? 在切片后面没有意义。 - Matheus Ribeiro

2
使用模板中的.slice().reverse()方法解决方案简单而有效。然而,在使用迭代索引时,您应该注意。这是因为.slice()方法会为视图返回一个新数组,以便我们的初始数组不会改变。问题在于反转后的数组也具有反转的索引。因此,如果要将其用作变量,则还应反转索引。例如:
<ul>
  <li *ngFor="let item of items.slice().reverse(); index as i" (click)="myFunction(items.length - 1 - i)">{{item}}</li>
</ul>

在上面的代码中,我们使用反转后的items.length - 1 - i代替了i,这样点击的项目就对应于我们最初数组中的相同项目。
请查看此stackblitz: https://stackblitz.com/edit/angular-vxasr6?file=src%2Fapp%2Fapp.component.html

1
感谢您提醒我关于索引的问题,这可能会在下周当我尝试获取索引并且出现“什么鬼...这不是该索引的元素”时,为我节省大约20个小时。 - Cooper Scott

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接