[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] += ;
}
}); 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.
[Angular] ChangeDetection -- onPush的更多相关文章
- Angular:OnPush变化检测策略介绍
在OnPush策略下,Angular不会运行变化检测(Change Detection ),除非组件的input接收到了新值.接收到新值的意思是,input的值或者引用发生了变化.这样听起来不好理解, ...
- angular变化检测OnPush策略需要注意的几个问题
OnPush组件内部触发的事件(包括viewChild)会引起组件的一次markForCheck Detached组件内部触发的事件不会引起组件的变化检测 OnPush组件的contentChild依 ...
- .Net Core + Angular Cli / Angular4 开发环境搭建
一.基础环境配置 1.安装VS 2017 v15.3或以上版本 2.安装VS Code最新版本 3.安装Node.js v6.9以上版本 4.重置全局npm源,修正为 淘宝的 NPM 镜像: npm ...
- .Net Core+Angular Cli/Angular4开发环境搭建教程
一.基础环境配置1.安装VS2017v15.3或以上版本2.安装VSCode最新版本3.安装Node.jsv6.9以上版本4.重置全局npm源,修正为淘宝的NPM镜像:npminstall-gcnpm ...
- AngularCLI介绍及配置文件主要参数含义解析
使用Angular CLI可以快速,简单的搭建一个angular2或angular4项目,是只要掌握几行命令就能构建出前端架构的最佳实践,它本质也是使用了webpack来编译,打包,压缩等构建的事情, ...
- angular-cli.json常见配置
{ "project": { "name": "ng-admin", //项目名称 "ejected": false / ...
- angular-cli.json配置参数解释,以及依稀常用命令的通用关键参数解释
一. angular-cli.json常见配置 { "project": { "name": "ng-admin", //项目名称 &quo ...
- angular-cli.json配置参数解析,常用命令解析
1.angular-cli.json配置参数解析 { "project": { "name": "ng-admin", //项目名称 &qu ...
- 使用OnPush和immutable.js来提升angular的性能
angular里面变化检测是非常频繁的发生的,如果你像下面这样写代码 <div> {{hello()}} </div> 则每次变化检测都会执行hello函数,如果hello函数 ...
随机推荐
- Writing buffer overflow exploits - a tutorial for beginners
Buffer overflows in user input dependent buffers have become one of the biggest security hazards on ...
- 学习笔记(一):offset
很多初学者对于JavaScript中的offset.scroll.client一直弄不明白,虽然网上到处都可以看一张图(图1),但这张图太多太杂,并且由于浏览器差异性,图示也不完全正确. 图一 不知道 ...
- python 序列排序 排序后返回相应的索引
https://blog.csdn.net/longwei92/article/details/83098289 https://blog.csdn.net/u013731339/article/de ...
- Excel数据比对-批量数据比对
1.导出现场的Excel收费规则2.有专门的代码写的测试收费规则的工具(开发自己开发的)3.在这个工具上选择,导出的收费规则Excel,点击导出按钮(导出按钮里面有计算每一列的计费结果4.Excel里 ...
- GO语言学习(十九)Go 错误处理
Go 错误处理 Go 语言通过内置的错误接口提供了非常简单的错误处理机制. error类型是一个接口类型,这是它的定义: type error interface { Error() string } ...
- 国内计算机类期刊 SCI收录:
国内计算机类期刊 SCI收录: JOURNAL OF COMPUTER SCIENCE AND TECHNOLOGY,计算机科学与技术,英文,双月刊, SCIE 国内计算机类期刊 EI收录: 核心类 ...
- 16.用Spring Boot颠覆Java应用开发
转自:https://www.cnblogs.com/aishangJava/p/5971288.html Java开发概述: 使用Java做Web应用开发已经有近20年的历史了,从最初的Servle ...
- Codeforces Round #445 Div. 1 C Maximum Element (dp + 组合数学)
题目链接: http://codeforces.com/contest/889/problem/C 题意: 给你 \(n\)和 \(k\). 让你找一种全排列长度为\(n\)的 \(p\),满足存在下 ...
- JavaScript系列--JavaScript数组高阶函数reduce()方法详解及奇淫技巧
一.前言 reduce() 方法接收一个函数作为累加器,数组中的每个值(从左到右)开始缩减,最终计算为一个值. reduce() 可以作为一个高阶函数,用于函数的 compose. reduce()方 ...
- cmake配置c++可调用的文件路径参数
一.目的 在程序中使用一个路径配置,因为在svn服务器的测试数据,测试数据成为了本地路径,在程序中使用了绝对路径来处理文件的输入,这个令人头疼啊. 每次下完代码,我得挨个地方去找,谁在用本地路径,有点 ...