Immutable & Redux in Angular Way

Immutable & Redux in Angular Way

写在前面

AngularJS 1.x版本做为上一代MVVM的框架取得了巨大的成功,如今一提到Angular,哪怕是已经和1.x版本彻底不兼容的Angular 2.x(目前最新的版本号为4.2.2),你们仍是把其做为典型的MVVM框架,MVVM的优势Angular天然有,MVVM的缺点也变成了Angular的缺点一直被人诟病。javascript

其实,从Angular 2开始,Angular的数据流动彻底能够由开发者自由控制,所以不管是快速便捷的双向绑定,仍是如今风头正盛的Redux,在Angular框架中其实均可以获得很好的支持。css

Mutable

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

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代码java

<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修改成git

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代码github

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

app.component.ts代码web

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数据typescript

  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, Input, Output, EventEmitter, ChangeDetectionStrategy } from '@angular/core';
@Component({
  selector       : 'app-counter',
  changeDetection: ChangeDetectionStrategy.OnPush,
  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);
  }
}

将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);
  }

  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',
  templateUrl: './app.component.html',
  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, INCREMENT, 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

相关文章
相关标签/搜索