[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函数 ...
随机推荐
- eclipse中修改了代码编译后执行不起作用
在网上试了好多方法都没解决 后来,删了eclipse里的服务器,重新创建了一下,重新运行自己的工程发现修改的代码起作用了.
- BZOJ3238: [Ahoi2013]差异(后缀数组)
Description Input 一行,一个字符串S Output 一行,一个整数,表示所求值 Sample Input cacao Sample Output 54 解题思路: 看到lcp,想到了 ...
- x64系统下,InpOutx64有数字签名而WinIO3.0无数字签名
参考文档 http://www.highrez.co.uk/Downloads/InpOut32/ //可以下载InpOutx64的驱动程序及DLL,还有驱动主板硬件IO的例程 https://www ...
- POJ 3009 Curling 2.0 {深度优先搜索}
原题 $On Planet MM-21, after their Olympic games this year, curling is getting popular. But the rules ...
- Android时间戳与字符串相互转换
import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; public cl ...
- 12. ZooKeeper之Java客户端API使用—创建会话。
转自:https://blog.csdn.net/en_joker/article/details/78686649 客户端可以通过创建一个ZooKeeper(org.apache.zookeeper ...
- Android Studio com.android.dex.DexException: Multiple dex files define(重复引用包)
如果你用Android Studio开发,并且要用其他项目作为library,这个问题是很容易出现的.出现这个问题的原因是包的重复引用,意思就是在你自己的项目中引用了某个包,而被你作为library的 ...
- 利用ServiceWorker实现页面的快速加载和离线访问
Service workers 本质上充当Web应用程序与浏览器之间的代理服务器,也可以在网络可用时作为浏览器和网络间的代理.它们旨在(除其他之外)使得能够创建有效的离线体验,拦截网络请求并基于网络是 ...
- Java Web学习总结(16)——JSP的九个内置对象
一.JSP运行原理 每个JSP 页面在第一次被访问时,WEB容器都会把请求交给JSP引擎(即一个Java程序)去处理.JSP引擎先将JSP翻译成一个_jspServlet(实质上也是一个servlet ...
- js进阶 14-4 $.get()方法和$.post()方法如何使用
js进阶 14-4 $.get()方法和$.post()方法如何使用 一.总结 一句话总结:$.get(URL,callback); $.post(URL,data,callback); callba ...