[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函数 ...
随机推荐
- 非常不错的canvas效果,线随心动
非常不错的canvas效果,下面是html代码. <!DOCTYPE html> <html> <head> <meta charset="utf- ...
- Qt源码分析之信号和槽机制(QMetaObject是一个内部struct)
Qt的信号和槽机制是Qt的一大特点,实际上这是和MFC中的消息映射机制相似的东西,要完成的事情也差不多,就是发送一个消息然后让其它窗口响应,当然,这里的消息是广义的说法,简单点说就是如何在一个类的一个 ...
- 使用ILMerge将所有引用的DLL和exe文件打成一个exe文件
今天做了一个IM自动更新的软件,里面牵扯到了文件的解压和接口签名加密,使用了2个第三方的dll,想发布的时候才发现调用的类没几个,就像把它们都跟EXE文件打包在一起,以后复制去别的地方用也方便,于是上 ...
- seaJS注意点:
1.require 是同步往下执行,require.async 则是异步回调执行.require.async 一般用来加载可延迟异步加载的模块.
- jquery点击完一个按钮,并且触发另一个按钮
$a.click(function(){ $b.trigger('click'); });
- PythonNET网络编程3
IO IO input output 在内存中存在数据交换的操作都可以认为是IO操作 和终端交互 : input print 和磁盘交互 : read write 和网络交互 : recv send ...
- 使用H5 formData对象上传图片和视频的文件时,必填的属性
async : false,cache : false,contentType : false,// 告诉jQuery不要去设置Content-Type请求头processData : false,/ ...
- 百度2019校招Web前端工程师笔试卷(9月14日)
8月27日晚,在实习公司加班.当时正在调试页面,偶然打开百度首页console,发现彩蛋,于是投了简历. 9月14日晚,七点-九点,在公司笔试. 笔试题型(有出入): 一.单选20道 1.难度不难,考 ...
- JS学习笔记 - fgm练习 - 鼠标移入/移出div样式改变
思路: div的默认样式正常设置. 鼠标移入时,发生改变的样式有3个,即 边框颜色,div背景色,字体颜色. 把这三个css改变设置在一个类名下,再通过js给div动态 添加/去除这个类名,实现div ...
- Fiddler--功能简介
Fiddler的基本介绍 Fiddler的官方网站: www.fiddler2.com Fiddler官方网站提供了大量的帮助文档和视频教程, 这是学习Fiddler的最好资料. Fiddler是最 ...