如何使用 changeDetectionStrategy.onPush 保持对象正确更新?

How to use changeDetectionStrategy.onPush to keep object correctly updated?

我正在使用 ngFor 和一个用于切换任务状态的复选框在待办事项中显示任务列表。 我正在使用来自 ngrx 的商店和以下状态界面:

export interface State {
    tasks: Task[];
    index: { [_id: string]: Task };
};

就是当状态改变时负责更新任务的reducer:

case TaskActions.UPDATE_TASK_SUCCESS: {
    const task = action.payload;
    state.index[task._id] = Object.assign(state.index[task._id], task);

    return state;
}

这里是将任务作为@Input 并将changeDetectionStrategy 设置为onPush 的任务组件:

import { Component, OnInit, Input, ChangeDetectionStrategy, DoCheck } from '@angular/core';

import {Store} from '@ngrx/store';
import { AppState } from '../../../../domain';

import { TaskActions } from '../../../../domain/actions';

@Component({
    selector: 'task',
    templateUrl: './task.component.html',
    changeDetection: ChangeDetectionStrategy.OnPush,
    styleUrls: ['./_task.component.scss']
})
export class TaskComponent implements OnInit, DoCheck {

    @Input() task;

    constructor(
        private store: Store<AppState>,
        private TaskActions: TaskActions
    ) { }

    ngOnInit() {
    }

    ngDoCheck() {
        console.warn('Check');
    }

    public toggleTaskStatus(_task) {
        var new_task = Object.assign({}, _task, {['status']: (_task.status == 'todo' ? 'done' : 'todo')});
        this.store.dispatch(this.TaskActions.updateTask(new_task));
    }

}

使用此配置,我第一次单击复选框时商店内的任务会更新,但 UI 不会反映任何更改。相反,从第二次开始,UI 开始切换其状态,但不根据商店状态(当商店中的任务状态为 'todo' 时,UI 为 'done')。此外,要更新 UI 不会等待减速器中的成功操作(该服务正在创建一个 1 秒的轻微超时的可观察对象)。

当 changeDetectionStrategy 设置为默认值时,一切似乎都正常工作,UI 根据存储状态进行更新。

我做错了什么?是关于状态的不变性还是组件内部的一些问题?

reducer 必须 是不可变的。

这里:

case TaskActions.UPDATE_TASK_SUCCESS: {
    const task = action.payload;
    state.index[task._id] = Object.assign(state.index[task._id], task);

    return state;
}

你正在改变状态。

这行代码changeDetection: ChangeDetectionStrategy.OnPush告诉Angular如果@Input在内存中有相同的地址(引用),它应该不被更新 (以提高性能)。

在你的例子中,你改变了状态和return相同的(状态)对象。
这就是当您删除行 changeDetection: ChangeDetectionStrategy.OnPush.

时它起作用的原因

要使其正常工作,您应该:

case TaskActions.UPDATE_TASK_SUCCESS: {
  const task = action.payload;

  // here, we return a new reference and ensure immutability
  return Object.assign(
    {},

    state,

    {
      index: {
        [task._id]: Object.assign(state.index[task._id], task)
      }
    }
  )
}

为避免改变状态,您可能需要设置 ngrx-store-freeze。如果发生状态突变,它将抛出错误。


稍微谈谈 ES7 语法,Typescript 现在支持对象上的扩展运算符,我们也可能有这种语法:

case TaskActions.UPDATE_TASK_SUCCESS: {
  const task = action.payload;

  return {
    {},

    ...state,

    {
      index: {
        [task._id]: { ...state.index[task._id], task }
      }
    }
  }
}

但是 Angular 目前不支持 Typescript 2.1.4,使用此版本会破坏您的应用程序。希望它很快就会得到支持。 CF即PR。 (感谢 semver,它应该在 2017 年 3 月登陆 Angular 4.0)。