[Angular] ChangeDetection -- onPush
2017-01-31 01:16
302 查看
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:
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.
For this code, <message> is a dump component:
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:
As we can see that, the 'state' is implements 'StoreData' interface.
We did a deep clone of current 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:
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'.
And BE CAREFUL here, since we use Object.assign, what it dose is just a shadow copy, if we still do:
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:
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:
Event the reducers data is immutable, but everytime we actually receive a new 'message$' which is Message view model, not the state model.
And for view model:
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:
'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:
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.
Now if new message come in, only new message will be rendered to the list, the existing message won't be re-rendered.
Github
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
相关文章推荐
- Angular2源码解读之ChangeDetection
- Angular 依赖性注入 changeDetection 可拖拽的属性型指令
- Angular实现一个简单的多选复选框的弹出框指令
- angular+做一个日程表,可以添加内容然后可以隐藏显示
- Using TemplateRef to create a tooltip/popover directive in Angular 2
- angular自定义过滤器
- AngularJs directive详解及示例代码
- Angular2官网项目 第二天
- AngularJs Understanding the Model Component
- 基于MVC+jQuery+Angularjs的Echarts的初步实现
- Angular.js简介
- 再相见 —— Angular
- .net版本angular-file-upload
- angular过滤器
- angularjs controller控制器注入对象顺序
- highcharts 在angular中的使用示例代码
- 动态添加的dom方法,调用angular中$scope方法
- angular.js常用内置指令
- Angular2 gotchas: Double binding within Form
- 【Angular】Angular基础(3)