Immutable & Redux in Angular Way

写在前面

AngularJS 1.x版本作为上一代MVVM的框架取得了巨大的成功,现在一提到Angular,哪怕是已经和1.x版本完全不兼容的Angular 2.x(目前最新的版本号为4.2.2),大家还是把其作为典型的MVVM框架,MVVM的优点Angular自然有,MVVM的缺点也变成了Angular的缺点一直被人诟病。

其实,从Angular 2开始,Angular的数据流动完全可以由开发者自由控制,因此无论是快速便捷的双向绑定,还是现在风头正盛的Redux,在Angular框架中其实都可以得到很好的支持。

Mutable

我们以最简单的计数器应用举例,在这个例子中,counter的数值可以由按钮进行加减控制。

counter.component.ts代码

import { Component,ChangeDetectionStrategy,Input } from '@angular/core';

@Component({
  selector       : 'app-counter',templateUrl    : './counter.component.html',styleUrls      : []
})
export class CounterComponent {
  @input()
  counter = {
    payload: 1
  };
  
  increment() {
    this.counter.payload++;
  }

  decrement() {
    this.counter.payload--;
  }

  reset() {
    this.counter.payload = 1;
  }

}

counter.component.HTML代码

<p>Counter: {{ counter.payload }}</p>
<button (click)="increment()">Increment</button>
<button (click)="decrement()">Decrement</button>
<button (click)="reset()">Reset</button>

现在我们增加一下需求,要求counter的初始值可以被修改,并且将修改后的counter值传出。在Angular中,数据的流入和流出分别由@Input和@Output来控制,我们分别定义counter component的输入和输出,将counter.component.ts修改为

import { Component,Input,Output,EventEmitter } from '@angular/core';
@Component({
  selector   : 'app-counter',templateUrl: './counter.component.html',styleUrls  : []
})
export class CounterComponent {
  @input() counter = {
    payload: 1
  };
  @Output() onCounterChange = new EventEmitter<any>();

  increment() {
    this.counter.payload++;
    this.onCounterChange.emit(this.counter);
  }

  decrement() {
    this.counter.payload--;
    this.onCounterChange.emit(this.counter);
  }

  reset() {
    this.counter.payload = 1;
    this.onCounterChange.emit(this.counter);
  }
}

当其他component需要使用counter时,app.component.HTML代码

<counter [counter]="initCounter" (onCounterChange)="onCounterChange($event)"></counter>

app.component.ts代码

import { Component } from '@angular/core';
@Component({
  selector   : 'app-root',templateUrl: './app.component.html',styleUrls  : [ './app.component.less' ]
})
export class AppComponent {
  initCounter = {
    payload: 1000
  }

  onCounterChange(counter) {
    console.log(counter);
  }
}

在这种情况下counter数据

  1. 会被当前counter component中的函数修改

  2. 也可能被initCounter修改

  3. 如果涉及到服务端数据,counter也可以被Service修改

  4. 在复杂的应用中,还可能在父component通过@ViewChild等方式获取后被修改

框架本身对此并没有进行限制,如果开发者对数据的修改没有进行合理的规划时,很容易导致数据的变更难以被追踪。

与AngularJs 1.x版本中在特定函数执行时进行脏值检查不同,Angular 2+使用了zone.js对所有的常用操作进行了monkey patch,有了zone.js的存在,Angular不再像之前一样需要使用特定的封装函数才能对数据的修改进行感知,例如ng-click或者$timeout等,只需要正常使用(click)或者setTimeout就可以了。

与此同时,数据在任意的地方可以被修改给使用者带来了便利的同时也带来了性能的降低,由于无法预判脏值产生的时机,Angular需要在每个浏览器事件后去检查更新template中绑定数值的变化,虽然Angular做了大量的优化来保证性能,并且成果显著(目前主流前端框架的跑分对比),但是Angular也提供了另一种开发方式。

Immutable & ChangeDetection

在Angular开发中,可以通过将component的changeDetection定义为ChangeDetectionStrategy.OnPush从而改变Angular的脏值检查策略,在使用OnPush模式时,Angular从时刻进行脏值检查的状态改变为仅在两种情况下进行脏值检查,分别是

  1. 当前component的@Input输入值发生更换

  2. 当前component或子component产生事件

反过来说就是当@Input对象mutate时,Angular将不再进行自动脏值检测,这个时候需要保证@Input的数据为Immutable

将counter.component.ts修改为

import { Component,EventEmitter,ChangeDetectionStrategy } from '@angular/core';
@Component({
  selector       : 'app-counter',changeDetection: ChangeDetectionStrategy.OnPush,styleUrls      : []
})
export class CounterComponent {
  @input() counter = {
    payload: 1
  };
  @Output() onCounterChange = new EventEmitter<any>();

  increment() {
    this.counter.payload++;
    this.onCounterChange.emit(this.counter);
  }

  decrement() {
    this.counter.payload--;
    this.onCounterChange.emit(this.counter);
  }

  reset() {
    this.counter.payload = 1;
    this.onCounterChange.emit(this.counter);
  }
}

将app.component.ts修改为

import { Component } from '@angular/core';
@Component({
  selector   : 'app-root',styleUrls  : [ './app.component.less' ]
})
export class AppComponent {
  initCounter = {
    payload: 1000
  }

  onCounterChange(counter) {
    console.log(counter);
  }

  changeData() {
    this.initCounter.payload = 1;
  }
}

将app.component.html修改为

<app-counter [counter]="initCounter" (onCounterChange)="onCounterChange($event)"></app-counter>
<button (click)="changeData()">change</button>

这个时候点击change发现counter的值不会发生变化。

将app.component.ts中changeData修改为

changeData() {
  this.initCounter = {
    ...this.initCounter,payload: 1
  }
}

counter值的变化一切正常,以上的代码使用了Typescript 2.1开始支持的 Object Spread,和以下代码是等价的

changeData() {
  this.initCounter = Object.assign({},this.initCounter,{ payload: 1 });
}

在ChangeDetectionStrategy.OnPush时,可以通过ChangeDetectorRef.markForCheck()进行脏值检查,官网范点击此处,手动markForCheck可以减少Angular进行脏值检查的次数,但是不仅繁琐,而且也不能解决数据变更难以被追踪的问题。

通过保证@Input的输入Immutable可以提升Angular的性能,但是counter数据在counter component中并不是Immutable,数据的修改同样难以被追踪,下一节我们来介绍使用Redux思想来构建Angular应用。

Redux & Ngrx Way

Redux来源于React社区,时至今日已经基本成为React的标配了。Angular社区实现Redux思想最流行的第三方库是ngrx,借用官方的话来说RxJS poweredinspired by Redux,靠谱。

如果你对RxJS有进一步了解的兴趣,请访问https://rxjs-cn.github.io/rxj...

基本概念

和Redux一样,ngrx也有着相同View、Action、Middleware、dispatcher、Store、Reducer、State的概念。使用ngrx构建Angular应用需要舍弃Angular官方提供的@Input和@Output的数据双向流动的概念。改用Component->Action->Reducer->Store->Component的单向数据流动。

以下部分代码来源于CounterNgrx和这篇文章

我们使用ngrx构建同样的counter应用,与之前不同的是这次需要依赖@ngrx/core@ngrx/store

Component

app.module.ts代码,将counterReducer通过StoreModule import

import {browserModule} from '@angular/platform-browser';
import {NgModule} from '@angular/core';
import {FormsModule} from '@angular/forms';
import {HttpModule} from '@angular/http';

import {AppComponent} from './app.component';
import {StoreModule} from '@ngrx/store';
import {counterReducer} from './stores/counter/counter.reducer';

@NgModule({
  declarations: [
    AppComponent
  ],imports: [
    browserModule,FormsModule,HttpModule,StoreModule.provideStore(counterReducer),],providers: [],bootstrap: [AppComponent]
})
export class AppModule {
}

在NgModule中使用ngrx提供的StoreModule将我们的counterReducer传入

app.component.html

<p>Counter: {{ counter | async }}</p>
<button (click)="increment()">Increment</button>
<button (click)="decrement()">Decrement</button>
<button (click)="reset()">Reset</button>

注意多出来的async的pipe,async管道将自动subscribe Observable或Promise的最新数据,当Component销毁时,async管道会自动unsubscribe。

app.component.ts

import {Component} from '@angular/core';
import {CounterState} from './stores/counter/counter.store';
import {Observable} from 'rxjs/observable';
import {Store} from '@ngrx/store';
import {DECREMENT,INCREMENT,RESET} from './stores/counter/counter.action';

@Component({
  selector: 'app-root',styleUrls: ['./app.component.css']
})
export class AppComponent {
  counter: Observable<number>;

  constructor(private store: Store<CounterState>) {
    this.counter = store.select('counter');
  }

  increment() {
    this.store.dispatch({
      type: INCREMENT,payload: {
        value: 1
      }
    });
  }

  decrement() {
    this.store.dispatch({
      type: DECREMENT,payload: {
        value: 1
      }
    });
  }

  reset() {
    this.store.dispatch({type: RESET});
  }
}

在Component中可以通过依赖注入ngrx的Store,通过Store select获取到的counter是一个Observable的对象,自然可以通过async pipe显示在template中。

dispatch方法传入的内容包括typepayload两部分, reducer会根据typepayload生成不同的state,注意这里的store其实也是个Observable对象,如果你熟悉Subject,你可以暂时按照Subject的概念来理解它,store也有一个next方法,和dispatch的作用完全相同。

Action

counter.action.ts

export const INCREMENT = 'INCREMENT';
export const DECREMENT = 'DECREMENT';
export const RESET     = 'RESET';

Action部分很简单,reducer要根据dispath传入的action执行不同的操作。

Reducer

counter.reducer.ts

import {CounterState,INITIAL_COUNTER_STATE} from './counter.store';
import {DECREMENT,RESET} from './counter.action';
import {Action} from '@ngrx/store';

export function counterReducer(state: CounterState = INITIAL_COUNTER_STATE,action: Action): CounterState {
  const {type,payload} = action;

  switch (type) {
    case INCREMENT:
      return {...state,counter: state.counter + payload.value};

    case DECREMENT:
      return {...state,counter: state.counter - payload.value};

    case RESET:
      return INITIAL_COUNTER_STATE;

    default:
      return state;
  }
}

Reducer函数接收两个参数,分别是state和action,根据Redux的思想,reducer必须为纯函数(Pure Function),注意这里再次用到了上文提到的Object Spread。

Store

counter.store.ts

export interface CounterState {
  counter: number;
}

export const INITIAL_COUNTER_STATE: CounterState = {
  counter: 0
};

Store部分其实也很简单,定义了couter的Interface和初始化state。

以上就完成了Component->Action->Reducer->Store->Component的单向数据流动,当counter发生变更的时候,component会根据counter数值的变化自动变更。

总结

同样一个计数器应用,Angular其实提供了不同的开发模式

  1. Angular默认的数据流和脏值检查方式其实适用于绝大部分的开发场景。

  2. 当性能遇到瓶颈时(基本不会遇到),可以更改ChangeDetection,保证传入数据Immutable来提升性能。

  3. 当MVVM不再能满足程序开发的要求时,可以尝试使用Ngrx进行函数式编程。

这篇文章总结了很多Ngrx优缺点,其中我觉得比较Ngrx显著的优点是

  1. 数据层不仅相对于component独立,也相对于框架独立,便于移植到其他框架

  2. 数据单向流动,便于追踪

Ngrx的缺点也很明显

  1. 实现同样功能,代码量更大,对于简单程序而言使用Immutable过度设计,降低开发效率

  2. FP思维和OOP思维不同,开发难度更高

参考资料

  1. Immutability vs Encapsulation in Angular Applications

  2. whats-the-difference-between-markforcheck-and-detectchanges

  3. Angular 也走 Redux 風 (使用 Ngrx)

  4. Building a Redux application with Angular 2

Immutable & Redux in Angular Way的更多相关文章

  1. HTML5 播放 RTSP 视频的实例代码

    目前大多数网络摄像头都是通过 RTSP 协议传输视频流的,但是 HTML 并不标准支持 RTSP 流。本文重点给大家介绍HTML5 播放 RTSP 视频的实例代码,需要的朋友参考下吧

  2. HTML5 input新增type属性color颜色拾取器的实例代码

    type 属性规定 input 元素的类型。本文较详细的给大家介绍了HTML5 input新增type属性color颜色拾取器的实例代码,感兴趣的朋友跟随脚本之家小编一起看看吧

  3. 利用Node实现HTML5离线存储的方法

    这篇文章主要介绍了利用Node实现HTML5离线存储的方法,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下

  4. amazeui模态框弹出后立马消失并刷新页面

    这篇文章主要介绍了amazeui模态框弹出后立马消失并刷新页面,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下

  5. 移动HTML5前端框架—MUI的使用

    这篇文章主要介绍了移动HTML5前端框架—MUI的使用的相关资料,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧

  6. 详解如何通过H5(浏览器/WebView/其他)唤起本地app

    这篇文章主要介绍了详解如何通过H5(浏览器/WebView/其他)唤起本地app的相关资料,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧

  7. H5混合开发app如何升级的方法

    本篇文章主要介绍了H5混合开发app如何升级的方法,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧

  8. 使用placeholder属性设置input文本框的提示信息

    这篇文章主要介绍了使用placeholder属性设置input文本框的提示信息,本文给大家介绍的非常详细,具有一定的参考借鉴价值,需要的朋友可以参考下

  9. Bootstrap File Input文件上传组件

    这篇文章主要介绍了Bootstrap File Input文件上传组件,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下

  10. AmazeUI 折叠面板的实现代码

    这篇文章主要介绍了AmazeUI 折叠面板的实例代码,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下

随机推荐

  1. Angular2 innerHtml删除样式

    我正在使用innerHtml并在我的cms中设置html,响应似乎没问题,如果我这样打印:{{poi.content}}它给了我正确的内容:``但是当我使用[innerHtml]=“poi.content”时,它会给我这个html:当我使用[innerHtml]时,有谁知道为什么它会剥离我的样式Angular2清理动态添加的HTML,样式,……

  2. 为Angular根组件/模块指定@Input()参数

    我有3个根组件,由根AppModule引导.你如何为其中一个组件指定@input()参数?也不由AppModalComponent获取:它是未定义的.据我所知,你不能将@input()传递给bootstraped组件.但您可以使用其他方法来做到这一点–将值作为属性传递.index.html:app.component.ts:

  3. angular-ui-bootstrap – 如何为angular ui-bootstrap tabs指令指定href参数

    我正在使用角度ui-bootstrap库,但我不知道如何为每个选项卡指定自定义href.在角度ui-bootstrap文档中,指定了一个可选参数select(),但我不知道如何使用它来自定义每个选项卡的链接另一种重新定义问题的方法是如何使用带有角度ui-bootstrap选项卡的路由我希望现在还不算太晚,但我今天遇到了同样的问题.你可以通过以下方式实现:1)在控制器中定义选项卡href:2)声明一个函数来改变控制器中的散列:3)使用以下标记:我不确定这是否是最好的方法,我很乐意听取别人的意见.

  4. 离子框架 – 标签内部的ng-click不起作用

    >为什么标签标签内的按钮不起作用?>但是标签外的按钮(登陆)工作正常,为什么?>请帮我解决这个问题.我需要在点击时做出回复按钮workingdemo解决方案就是不要为物品使用标签.而只是使用divHTML

  5. Angular 2:将值传递给路由数据解析

    我正在尝试编写一个DataResolver服务,允许Angular2路由器在初始化组件之前预加载数据.解析器需要调用不同的API端点来获取适合于正在加载的路由的数据.我正在构建一个通用解析器,而不是为我的许多组件中的每个组件设置一个解析器.因此,我想在路由定义中传递指向正确端点的自定义输入.例如,考虑以下路线:app.routes.ts在第一个实例中,解析器需要调用/path/to/resourc

  6. angularjs – 解释ngModel管道,解析器,格式化程序,viewChangeListeners和$watchers的顺序

    换句话说:如果在模型更新之前触发了“ng-change”,我可以理解,但是我很难理解在更新模型之后以及在完成填充更改之前触发函数绑定属性.如果您读到这里:祝贺并感谢您的耐心等待!

  7. 角度5模板形式检测形式有效性状态的变化

    为了拥有一个可以监听其包含的表单的有效性状态的变化的组件并执行某些组件的方法,是reactiveforms的方法吗?

  8. Angular 2 CSV文件下载

    我在springboot应用程序中有我的后端,从那里我返回一个.csv文件WheniamhittingtheURLinbrowsercsvfileisgettingdownloaded.现在我试图从我的角度2应用程序中点击此URL,代码是这样的:零件:服务:我正在下载文件,但它像ActuallyitshouldbeBook.csv请指导我缺少的东西.有一种解决方法,但您需要创建一个页面上的元

  9. angularjs – Angular UI-Grid:过滤后如何获取总项数

    提前致谢:)你应该避免使用jQuery并与API进行交互.首先需要在网格创建事件中保存对API的引用.您应该已经知道总行数.您可以使用以下命令获取可见/已过滤行数:要么您可以使用以下命令获取所选行的数量:

  10. angularjs – 迁移gulp进程以包含typescript

    或者我应该使用tsc作为我的主要构建工具,让它解决依赖关系,创建映射文件并制作捆绑包?

返回
顶部