资讯专栏INFORMATION COLUMN

Immutable & Redux in Angular Way

lunaticf / 797人阅读

摘要:来源于社区,时至今日已经基本成为的标配了。部分很简单,要根据传入的执行不同的操作。当性能遇到瓶颈时基本不会遇到,可以更改,保证传入数据来提升性能。当不再能满足程序开发的要求时,可以尝试使用进行函数式编程。

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

Counter: {{ counter.payload }}

现在我们增加一下需求,要求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();

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

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

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

也可能被initCounter修改

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

在复杂的应用中,还可能在父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从时刻进行脏值检查的状态改变为仅在两种情况下进行脏值检查,分别是

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

当前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();

  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修改为


这个时候点击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

Counter: {{ counter | async }}

注意多出来的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;

  constructor(private store: Store) {
    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其实提供了不同的开发模式

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

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

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

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

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

数据单向流动,便于追踪

Ngrx的缺点也很明显

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

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

参考资料

Immutability vs Encapsulation in Angular Applications

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

Angular 也走 Redux 風 (使用 Ngrx)

Building a Redux application with Angular 2

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/83692.html

相关文章

  • 2017-09-23 前端日报

    摘要:知乎专栏前端给不了解前端的同学讲前端掘金前端够得到安全跨站请求伪造掘金前端面试问题持续更新掘金向核心贡献代码的六个步骤基于的仿音乐移动端个人文章用构建组件网易严选感受开发已完结掘金英文 2017-09-23 前端日报 精选 [译] 网络现状:性能提升指南前端够得到Web安全3--点击劫持/UI-覆盖攻击React, Jest, Flow, Immutable.js将改用MIT开源协议N...

    BingqiChen 评论0 收藏0
  • (译 & 转载) 2016 JavaScript 后起之秀

    摘要:在年成为最大赢家,赢得了实现的风暴之战。和他的竞争者位列第二没有前端开发者可以忽视和它的生态系统。他的杀手级特性是探测功能,通过检查任何用户的功能,以直观的方式让开发人员检查所有端点。 2016 JavaScript 后起之秀 本文转载自:众成翻译译者:zxhycxq链接:http://www.zcfy.cc/article/2410原文:https://risingstars2016...

    darry 评论0 收藏0
  • 关于前端数据&逻辑的思考

    摘要:这里引出了一个概念,就是数据流这个概念,在项目中我将所有数据的操作都成为数据的流动。 最近重构了一个项目,一个基于redux模型的react-native项目,目标是在混乱的代码中梳理出一个清晰的结构来,为了实现这个目标,首先需要对项目的结构做分层处理,将各个逻辑分离出来,这里我是基于典型的MVC模型,那么为了将现有代码重构为理想的模型,我需要做以下几步: 拆分组件 逻辑处理 抽象、...

    alin 评论0 收藏0
  • 2017-06-23 前端日报

    摘要:前端日报精选大前端公共知识梳理这些知识你都掌握了吗以及在项目中的实践深入贯彻闭包思想,全面理解闭包形成过程重温核心概念和基本用法前端学习笔记自定义元素教程阮一峰的网络日志中文译回调是什么鬼掘金译年,一个开发者的好习惯知乎专 2017-06-23 前端日报 精选 大前端公共知识梳理:这些知识你都掌握了吗?Immutable.js 以及在 react+redux 项目中的实践深入贯彻闭包思...

    Vixb 评论0 收藏0
  • React-redux进阶之Immutable.js

    摘要:的优势保证不可变每次通过操作的对象都会返回一个新的对象丰富的性能好通过字典树对数据结构的共享的问题与原生交互不友好通过生成的对象在操作上与原生不同,如访问属性,。 Immutable.js Immutable的优势 1. 保证不可变(每次通过Immutable.js操作的对象都会返回一个新的对象) 2. 丰富的API 3. 性能好 (通过字典树对数据结构的共享) Immutab...

    孙淑建 评论0 收藏0

发表评论

0条评论

lunaticf

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<