[Angular] ChangeDetection -- onPush

To understand how change detection can help us improve the proference, we need to understand when it works first.

There are some rules which can be applied when use change detection:

changeDetection: ChangeDetectionStrategy.OnPush

1. Change detection compares @Input value, so applied for dump components

Mostly change detection will be applied for dump component not smart component. Because if the data is getting from service, then change detection won‘t work.

<ul class="message-list" #list>
  <li class="message-list-item" *ngFor="let message of messages">
    <message [message]="message"></message>
  </li>
</ul>

For this code, <message> is a dump component:

@Component({
  selector: ‘message‘,
  templateUrl: ‘./message.component.html‘,
  styleUrls: [‘./message.component.css‘],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class MessageComponent {
  @Input() message: MessageVM;
}

2. Reducer: If the data is getting from the ‘store‘ (ngrx/store), then you need to be careful about how to write your reducer. We should keep AppState immutable and reuseable as much as possible.

For example:

function newMessagesReceivedAction(state: StoreData, action: NewMessagesReceivedAction) {
  const cloneState = cloneDeep(state);
  const newMessages = action.payload.unreadMessages,
    currentThreadId = action.payload.currentThreadId,
    currentUserId = action.payload.currentUserId;

  newMessages.forEach(message => {
    cloneState.messages[message.id] = message;
    cloneState.threads[message.threadId].messageIds.push(message.id);

    if(message.threadId !== currentThreadId) {
      cloneState.threads[message.threadId].participants[currentUserId] += 1;
    }
  });

  return cloneState;
}
export interface StoreData {
  participants: {
    [key: number]: Participant
  };
  threads: {
    [key: number]: Thread
  };
  messages: {
    [key: number]: Message
  };
}

As we can see that, the ‘state‘ is implements ‘StoreData‘ interface.

We did a deep clone of current state:

const cloneState = cloneDeep(state);

new every props in StateData interface will get a new reference. But this is not necessary, because in the code, we only modify ‘messages‘ & ‘threads‘ props, but not ‘participants‘.

Therefore it means we don‘t need to do a deep clone for all the props, so we can do:

function newMessagesReceivedAction(state: StoreData, action: NewMessagesReceivedAction) {
  const cloneState = {
    participants: state.participants, // no need to update this, since it won‘t change from here
    threads: Object.assign({}, state.threads),
    messages: Object.assign({}, state.messages)
  };
  const newMessages = action.payload.unreadMessages,
    currentThreadId = action.payload.currentThreadId,
    currentUserId = action.payload.currentUserId;

  newMessages.forEach(message => {
    cloneState.messages[message.id] = message;

    // First clone ‘cloneState.threads[message.threadId]‘,
    // create a new reference
    cloneState.threads[message.threadId] =
      Object.assign({}, state.threads[message.threadId]);

    // Then assign new reference to new variable
    const messageThread = cloneState.threads[message.threadId];

    messageThread.messageIds = [
      ...messageThread.messageIds,
      message.id
    ];

    if (message.threadId !== currentThreadId) {
      messageThread.participants = Object.assign({}, messageThread.participants);
      messageThread.participants[currentUserId] += 1;
    }
  });

  return cloneState;
}

So in the updated code, we didn‘t do a deep clone, instead, we using Object.assign() to do a shadow clone, but only for ‘messages‘ & ‘threads‘.

  const cloneState = {
    participants: state.participants, // no need to update this, since it won‘t change from here
    threads: Object.assign({}, state.threads),
    messages: Object.assign({}, state.messages)
  };

And BE CAREFUL here, since we use Object.assign, what it dose is just a shadow copy, if we still do:

cloneState.messages[message.id] = message;

It actually modify the origial state, instead what we should do is do a shadow copy of ‘state.messages‘, then modify the value based on new messages clone object:

    // First clone ‘cloneState.threads[message.threadId]‘,
    // create a new reference
    cloneState.threads[message.threadId] =
      Object.assign({}, state.threads[message.threadId]);

...

3. Selector: Using memoization to remember previous selector‘s data.

But only 1 & 2 are still not enough for Change Detection. Because the application state is what we get from BE, it is good to keep it immutable and reuse the old object reference as much as possible, but what we pass into component are not Application state, it is View model state. This will cause the whole list be re-render, if we set time interval 3s, to fetch new messages.

For example we smart component:

@Component({
  selector: ‘message-section‘,
  templateUrl: ‘./message-section.component.html‘,
  styleUrls: [‘./message-section.component.css‘]
})
export class MessageSectionComponent {

  participantNames$: Observable<string>;
  messages$: Observable<MessageVM[]>;
  uiState: UiState;

  constructor(private store: Store<AppState>) {
    this.participantNames$ = store.select(this.participantNamesSelector);
    this.messages$ = store.select(this.messageSelector.bind(this));
    store.subscribe(state => this.uiState = Object.assign({}, state.uiState));
  }

...

}

Event the reducers data is immutable, but everytime we actually receive a new ‘message$‘ which is Message view model, not the state model.

export interface MessageVM {
  id: number;
  text: string;
  participantName: string;
  timestamp: number;
}

And for view model:

  messageSelector(state: AppState): MessageVM[] {
    const {currentSelectedID} = state.uiState;
    if (!currentSelectedID) {
      return [];
    }
    const messageIds = state.storeData.threads[currentSelectedID].messageIds;
    const messages = messageIds.map(id => state.storeData.messages[id]);
    return messages.map((message) => this.mapMessageToMessageVM(message, state));
  }

  mapMessageToMessageVM(message, state): MessageVM {
    return {
      id: message.id,
      text: message.text,
      participantName: (state.storeData.participants[message.participantId].name || ‘‘),
      timestamp: message.timestamp
    }
  }

As we can see, everytime it map to a new message view model, but this is not what we want, in the mssages list component:

First, we don‘t want the whole message list been re-render every 3s. Because there is no new data come in. But becaseu we everytime create a new view model, the list is actually re-rendered. To prevent that, we need to update our selector code and using memoization to do it.

Install:

npm i --save reselect 
import {createSelector} from ‘reselect‘;
/*
export const messageSelector = (state: AppState): MessageVM[] => {
  const messages = _getMessagesFromCurrentThread(state);
  const participants = _getParticipants(state);
  return _mapMessagesToMessageVM(messages, participants);
};*/

export const messageSelector = createSelector(
  _getMessagesFromCurrentThread,
  _getParticipants,
  _mapMessagesToMessageVM
);
function _getMessagesFromCurrentThread(state: AppState): Message[] {
  const {currentSelectedID} = state.uiState;
  if(!currentSelectedID) {
    return [];
  }
  const currentThread = state.storeData.threads[currentSelectedID];
  return currentThread.messageIds.map(msgId => state.storeData.messages[msgId])
}

function _getParticipants(state: AppState): {[key: number]: Participant} {
  return state.storeData.participants;
}

function _mapMessagesToMessageVM(messages: Message[] = [], participants) {
  return messages.map((message) => _mapMessageToMessageVM(message, participants));
}

function _mapMessageToMessageVM(message: Message, participants: {[key: number]: Participant}): MessageVM {
  return {
    id: message.id,
    text: message.text,
    participantName: (participants[message.participantId].name || ‘‘),
    timestamp: message.timestamp
  }
}

‘createSelector‘ function takes getters methods and one mapping function. The advantage to using ‘createSelector‘ is that it can help to memoizate the data, if the input are the same, then output will be the same (take out from memory, not need to calculate again.) It means:

  _getMessagesFromCurrentThread,
  _getParticipants,

only when ‘_getMessagesFromCurrentThread‘ and ‘_getParticipants‘ outputs different result, then the function ‘_mapMessagesToMessageVM‘ will be run.

This can help to prevent the message list be rerendered each three seconds if there is no new message come in.

But this still not help if new message come in, only render the new message, not the whole list re-render. We still need to apply rule No.4 .

4. lodash--> memoize: Prevent the whole list of messages been re-rendered when new message come in.

function _mapMessagesToMessageVM(messages: Message[] = [], participants: {[key: number]: Participant}) {
  return messages.map((message) => {
    const participantNames = participants[message.participantId].name || ‘‘;
    return _mapMessageToMessageVM(message, participantNames);
  });
}

const _mapMessageToMessageVM = memoize((message: Message, participantName: string): MessageVM => {
  return {
    id: message.id,
    text: message.text,
    participantName: participantName,
    timestamp: message.timestamp
  }
}, (message, participantName) => message.id + participantName);

Now if new message come in, only new message will be rendered to the list, the existing message won‘t be re-rendered.

Github

时间: 2024-09-29 00:44:26

[Angular] ChangeDetection -- onPush的相关文章

Angular:OnPush变化检测策略介绍

在OnPush策略下,Angular不会运行变化检测(Change Detection ),除非组件的input接收到了新值.接收到新值的意思是,input的值或者引用发生了变化.这样听起来不好理解,看例子: 子组件接收一个balls(别想歪:))输入,然后在模板遍历这个balls数组并展示出来.初始化2秒后,往balls数组push一个new ball: //balls-list.component.ts @Component({ selector: 'balls-list', templat

.Net Core + Angular Cli 开发环境搭建

一.基础环境配置 1.安装VS 2017 v15.3或以上版本 2.安装VS Code最新版本 3.安装Node.js v6.9以上版本 4.重置全局npm源,修正为 淘宝的 NPM 镜像: npm install -g cnpm --registry=https://registry.npm.taobao.org 5.安装TypeScript cnpm install -g typescript typings 6.安装 AngularJS CLI cnpm install -g @angul

.Net Core+Angular Cli/Angular4开发环境搭建教程

一.基础环境配置1.安装VS2017v15.3或以上版本2.安装VSCode最新版本3.安装Node.jsv6.9以上版本4.重置全局npm源,修正为淘宝的NPM镜像:npminstall-gcnpm 一.基础环境配置 1.安装VS 2017 v15.3或以上版本2.安装VS Code最新版本3.安装Node.js v6.9以上版本4.重置全局npm源,修正为 淘宝的 NPM 镜像: npm install -g cnpm --registry=https://registry.npm.taob

项目配置分析

前不久学习了下angular2的基础知识,按照官网要求构造了一个quik项目,这个项目能满足基本的angular开发,但对于实际大型的项目有很多功能都没有实现或者是没有很好的封装和简化,因此在网上搜到了这个项目 NiceFish  .这个项目的结构和功能非常强大,能满足几乎所有的项目实际需要.在这里感谢 -------大漠穷秋  ,接下来我将从架构组织,项目管理配置,代码组织等方面来具体分析这个项目,希望以此来弄清楚一个真正的web现代项目. 如下是整个项目的结构: angular-cli.js

[Angular] Test component template

Component: import { Component, Input, ChangeDetectionStrategy, EventEmitter, Output } from '@angular/core'; @Component({ selector: 'stock-counter', changeDetection: ChangeDetectionStrategy.OnPush, template: ` <div class="stock-counter"> &l

[Angular] Testing @Input and @Output bindings

Component: import { Component, Input, ChangeDetectionStrategy, EventEmitter, Output } from '@angular/core'; @Component({ selector: 'stock-counter', changeDetection: ChangeDetectionStrategy.OnPush, template: ` <div class="stock-counter"> &l

[Angular] Update FormArray with patchValue

Currently, patchValue doesn't support update FormArray. The workarround is you need to empty the form array first, then add items back. import {ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, Output, SimpleChanges} from '@angular/

[Angular] Component architecture and Reactive Forms

It it recommeded that when deals with form component, we can create a container component to hold state, and then create a stateless component to enpower the form. For example: In the example has two components, one is container component 'meal.compo

angular 有关侦测组件变化的 ChangeDetectorRef 对象

我们知道,如果我们绑定了组件数据到视图,例如使用 <p>{{content}}</p>,如果我们在组件中改变了content的值,那么视图也会更新为对应的值. angular 会在我们的组件发生变化的时候,对我们的组件执行变化检测,如果检测到我们的数据发生了变化,就会执行某些操作,如修改绑定数据的时候更新视图. 这样一来,当我们的组件数据比较多的时候,angular就会有很多操作在静悄悄地进行,一个规避这个问题的方法是,设置某个组件的变化检测策略为 'OnPush'. 使用 OnP