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] += ;
}
}); 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] += ;
}
}); 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

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

  1. Angular:OnPush变化检测策略介绍

    在OnPush策略下,Angular不会运行变化检测(Change Detection ),除非组件的input接收到了新值.接收到新值的意思是,input的值或者引用发生了变化.这样听起来不好理解, ...

  2. angular变化检测OnPush策略需要注意的几个问题

    OnPush组件内部触发的事件(包括viewChild)会引起组件的一次markForCheck Detached组件内部触发的事件不会引起组件的变化检测 OnPush组件的contentChild依 ...

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

    一.基础环境配置 1.安装VS 2017 v15.3或以上版本 2.安装VS Code最新版本 3.安装Node.js v6.9以上版本 4.重置全局npm源,修正为 淘宝的 NPM 镜像: npm  ...

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

    一.基础环境配置1.安装VS2017v15.3或以上版本2.安装VSCode最新版本3.安装Node.jsv6.9以上版本4.重置全局npm源,修正为淘宝的NPM镜像:npminstall-gcnpm ...

  5. AngularCLI介绍及配置文件主要参数含义解析

    使用Angular CLI可以快速,简单的搭建一个angular2或angular4项目,是只要掌握几行命令就能构建出前端架构的最佳实践,它本质也是使用了webpack来编译,打包,压缩等构建的事情, ...

  6. angular-cli.json常见配置

    { "project": { "name": "ng-admin", //项目名称 "ejected": false / ...

  7. angular-cli.json配置参数解释,以及依稀常用命令的通用关键参数解释

    一. angular-cli.json常见配置 { "project": { "name": "ng-admin", //项目名称 &quo ...

  8. angular-cli.json配置参数解析,常用命令解析

    1.angular-cli.json配置参数解析 { "project": { "name": "ng-admin", //项目名称 &qu ...

  9. 使用OnPush和immutable.js来提升angular的性能

    angular里面变化检测是非常频繁的发生的,如果你像下面这样写代码 <div> {{hello()}} </div> 则每次变化检测都会执行hello函数,如果hello函数 ...

随机推荐

  1. django 简单会议室预约(5)

    再来看看views.py的后半部分,对数据库的增删改查 #获取学院列表 def get_acad_list(): room_list = ConfeRoom.objects.all() #对数据库的操 ...

  2. 《WPF》Expander控件简单美化

    示例图: Expander控件功能很常见, 一般用于系统左侧的菜单收缩面板. 1.主要的组成 一个头部(header) 和 一个 内容(content) 组成. <Expander Expand ...

  3. Leetcode:populating_next_right_pointers_in_each_node题解

    一.     题目 对于结构体:struct TreeLinkNode { TreeLinkNode *left; TreeLinkNode *right; TreeLinkNode *next; } ...

  4. 开源课程管理系统(CMS):Moodle

    开源课程管理系统(CMS):Moodle 一.总结 1.php开发的cms,可借鉴参考用 二.Moodle(百度) Moodle(Modular Object-Oriented Dynamic Lea ...

  5. php编译参数注释

    1. 指定安装路径 --prefix=PREFIX 2. 指定运行用户 --with-fpm-user=nginx 3. 指定运行组 --with-fpm-group=nginx 3.与'--pref ...

  6. 利用formdata对象上传文件时,需要添加的参数

    function doUpload() { var formData = new FormData($( "#uploadForm" )[0]); $.ajax({ url: 'h ...

  7. python基础--数值类型和序列类型

    Python中数值类型:int(整数),float(浮点数),True/False(布尔值,首字母必须大写) int:1    #任意整数 float:2.3   #小数 python赋值: a = ...

  8. js进阶 13-9/10 jquery如何实现三级列表

    js进阶 13-9/10 jquery如何实现三级列表 一.总结 一句话总结:用的是定位,父标签相对定位,子标签就可以绝对定位了,绝对定位的孩子还是可以设置绝对定位.用toggle设置子菜单显示和隐藏 ...

  9. echarts+百度地图+vue 填坑记(一)(百度地图、鼠标移入移出标注,信息框会产生闪烁)

    大概七月底开始实习,到现在经历了两个完整的项目(c2b). 因为开发时间紧,任务重,所以在开发过程踩到的坑都没时间去记录. 现在在开发一个某链运输监控系统,到了收尾阶段,有时间写博客了!开心! 一.鼠 ...

  10. Day1:字符编码

    一.ASCII码 ASCII(American Standard Code for Information Interchange,美国标准信息交换代码),8位,一个字节,最多只能表示255个符号. ...