Angular2 之 @ngrx/store

RxJs驱动Angular应用程序的状态管理,其灵感来自于Redux。

有人这样说…

如果说RxJS是Angular2开发中的倚天剑,那么Redux就是屠龙刀了。而且这两种神兵利器都是不依赖于平台的,左手倚天右手屠龙……

什么是Redux?

Redux是为了解决应用程序状态(State)管理而提出的一种解决方案。那么什么是状态呢?简单来说对于应用开发来讲,UI上显示的数据、控件状态、登陆状态、数据加载画面的不同状态等等全部可以看作状态。

如果应用程序中没有明确的状态的管理,很多项目随着需求的迭代,代码规模逐渐扩大、团队人员水平参差不齐就会遇到各种状态管理极其混乱,导致代码的可维护性和扩展性降低。

那么Redux是怎么解决这个问题的呢?它提出了几个概念:Reducer、Action、Store。

Store

可以把Store想象成一个数据库,就像我们平时在开发过程中使用的Oracle数据库一样,Store是一个你应用内的数据(状态)中心。Store在Redux中有一个基本原则:它是一个唯一的、状态不可修改的树,状态的更新只能通过显性定义的Action发送后触发。

Store中一般负责:保存应用状态、提供访问状态的方法、派发Action的方法以及对于状态订阅者的注册和取消等。

遵守这个约定的话,任何时间点的Store的快照都可以提供一个完整当时的应用状态。

Reducer

我一直觉得Reducer这个单词不好理解,它的英文解释有:减速器、还原剂还有一个是异形接头。这几个单词的意思,我都不能直接拿过来用。

还好今天看到这边文章,文章中如下描述:

其二是我看了Redux的作者的一段视频,里面他用数组的reduce方法来做类比,而我之前对reduce的理解是reduce就是对数组元素进行累加计算成为一个值。

我觉得这个描述非常好,因为数组的reduce操作就是给出不断的用序列中的值经过累加器计算得到新的值,这和旧状态进入reducer经处理返回新状态是一样的。

很多网上的文章说可以把Reducer想象成数据库中的表,也就是Store是数据库,而一个reducer就是其中一张表。我其实觉得Reducer不太像表,我还是觉得这个累加器我比较好理解。

Reducer其实就是用来维护状态的

Reducer是一个纯javascript函数,接收2个参数:第一个是处理之前的状态,第二个是一个可能携带数据的动作(Action)。

export interface Reducer<T> { 
  (state: T, action: Action): T;
}

那么纯函数是意味着什么呢?意味着我们理论上可以把reducer移植到所有支持Redux的框架上,不用做改动。

什么是纯函数呢?

  • 纯函数具有幂等性,也就是给出同样的参数值,该函数总是求出同样的结果。
  • 结果的求值不会促使任何可语义上可观察的副作用或输出,例如易变对象的变化或输出到I/O装置。

下面我们来看一段简单的代码:

export const counter: Reducer<number> = (state = 0, action) => { 
  switch(action.type){ 
      case 'INCREMENT': return state + action.payload;          

      case 'DECREMENT': return state - action.payload;     

      default: return state; }};

上面的代码定义了一个计数器的Reducer,一开始的状态初始值为0((state = 0, action) 中的 state=0 给state赋了一个初始状态值)根据Action类型的不同返回不同的状态。这段代码就是非常简单的javascript,不依赖任何框架,可以在React中使用,也可以在接下来的我们要学习的Angular2中使用。

Action

Store中存储了我们的应用状态,Reducer接收之前的状态并输出新状态,但是我们如何让Reducer和Store之间通信呢?这就是Action的职责所在。在Redux规范中,所有的会引发状态更新的交互行为都必须通过一个显性定义的Action来进行。

下面的示意图描述了如果使用上面代码的Reducer,显性定义一个Action{type: ‘INCREMENT’, payload: 2}并且dispatch这个Action后的流程。

显性定义的Action触发Reducer产生新的状态

比如说之前的计数器状态是1,我们派送这个Action后,reducer接收到之前的状态1作为第一个参数,这个Action作为第二个参数。在Switch分支中走的是INCRMENT这个流程,也就是state+action.payload,输出的新状态为3.这个状态保存到Store中。

注意:

export interface Action { 
  type: string; 
  payload?: any; // 这个值可有可没有
}

为什么要在Angular2中使用?

angular1.x中没有一个统一的状态管理机制,所以造成状态很混乱。

混乱的Angular状态管理.png

所以我们要在Angular2中使用状态管理。

在Angular 2中使用Redux

ngrx是一套利用RxJS的类库,其中的@ngrx/store
(https://github.com/ngrx/store) 就是基于Redux规范制定的Angular2框架。

简介

@ngrx/store是一个旨在提高写性能的控制状态的容器,在angular的应用程序上是一致的。核心原理有:

  • 状态是一个不可变的数据结构
  • 行为描述状态的改变
  • 成为还原剂的纯函数拿到下一个状态和之前的状态来计算一个新的状态。
  • 状态访问store,一个可观测状态和一个观察者的行为

这些核心原则支持构建组件,这些组件可以使用OnPush变化检测策略给你聪明,性能变化检测在您的应用程序。

We will use @ngrx/store to provide use with a store for us to well… store our state.

我们使用@ngrx/store来提供我们使用一个store,来存储我们的状态。

要重申,redux中最重要的一个概念是应用程序的整个状态都集中在一个JavaScript对象树中。

因为我们的对象状态树是只读的,我们对每个动作的响应必须返回一个新的状态对象,而不改变先前的状态对象。在实现redux时,在reducer中实现不可变性是至关重要的,因此将逐步介绍下面的每个操作,并讨论如何完成这一任务。

知识点

  • 状态和组件的关系:

    • 状态驱动组件进行渲染
    • 组件发action来改变状态。
  • 是一个单向数据流

单项数据流

  • 要执行一个动作,我们称之为this.store.dispatch

  • 状态下降
    redux的另一个基石是状态总是向下流。

ngrx-state-down-concrete-resized.png

  • 事件向上
    “状态向下”的另一面是事件总是向上流动的。比如点击了item中的一个删除按钮,当前item组件中没有删除操作,那么就会向上去找删除方法。

ngrx-events-up-concrete-resized.png
* 在@ngrx/store实现使用observables允许我们使用异步和管道来实现我们的模块。
* We created our reducers which are simple functions that take an action and state object and returns a new state object.
* A store is basically a key value value map with some mechanisms(机制) to handle events and emit(发布) state.
* We broadcast(广播) our events using store.emit.
* We subscribe(订阅) to data using store.select.
* With asynchronous calls(异步调用), we pass our results through an observable sequence(可观察序列) and then emit the event to the reducer on completion.

一个简单的使用例子

在您的应用程序创建一个Reducer函数为每个数据类型。这些Reducer将使您的应用程序的组合状态:

// counter.ts
import { ActionReducer, Action } from '@ngrx/store';

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

export function counterReducer(state: number = 0, action: Action) {
    switch (action.type) {
        case INCREMENT:
            return state + 1;

        case DECREMENT:
            return state - 1;

        case RESET:
            return 0;
        // 这是一个例子:添加一个新的元素
        case 'ADD_TODO': 
           return [ ...state, action.payload ];
        default:
            return state;
    }
}
  • 第一个参数是state,就像我们在组件或服务中自己维护了一个内存数组一样,我们的Todo状态其实也是一个number,我们还赋了一个0的初始值(避免出现undefined错误)。
  • 第二个参数是一个有type和payload两个属性的对象,其实就是Action。也就是说我们其实可以不用定义Action,直接给出构造的对象形式即可。内部的话其实reducer就是一个大的switch语句,根据不同的Action类型决定返回什么样的状态。默认状态下我们直接将之前状态返回即可。Reducer就是这么单纯的一个函数。

  • 现在我们来考虑其中一个动作,增加一个Todo,我们需要发送一个Action,这个Action的type是 ’ADD_TODO’ ,payload就是新增加的这个Todo。

逻辑其实就是列表数组增加一个元素,用数组的push方法直接做是不是就行了呢?不行,因为Redux的约定是必须返回一个新状态,而不是更新原来的状态。而push方法其实是更新原来的数组,而我们需要返回一个新的数组。感谢ES7的Object Spread操作符,它可以让我们非常方便的返回一个新的数组。

在APP的主模块中,导入reducers、使用StoreModule.provideStore(reducers)方法去给angular注入提供商:(*In your app’s main module, import those reducers and use the StoreModule.provideStore(reducers)
function to provide them to Angular’s injector:*)

import { NgModule } from '@angular/core';
import { StoreModule } from '@ngrx/store';
import { counterReducer } from './counter';
@NgModule({ 
  imports: [ 
      BrowserModule, 
      StoreModule.provideStore({ counter: counterReducer })         
]})

export class AppModule {}

你可以在你的组件中注入Storeservice,使用store.select方法去获取状态,当数据改变后,由于store.select是可观察类型的,所以可以多次返回值,like this:counter的值会自动更新。

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

interface AppState {
  counter: number;
}

@Component({
  selector: 'my-app',
  template: `
        <button (click)="increment()">Increment</button>
        <div>Current Count: {{ counter | async }}</div>
        <button (click)="decrement()">Decrement</button>

        <button (click)="reset()">Reset Counter</button>
    `
})
export class CounterComponent {
  counter: Observable<number>;

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

  increment() {
    this.store.dispatch({ type: INCREMENT });
  }

  decrement() {
    this.store.dispatch({ type: DECREMENT });
  }

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

参考:

写在后面

GitHub上集大家之力搞了一个前端面试题的项目,里面都是大家面试时所遇到的题以及一些学习资料,有兴趣的话可以关注一下。如果你也有兴趣加入我们的话,请在项目中留言。项目同时也可以在gitbook上查看。

InterviewLibrary-GitHub
InterviewLibrary-gitbook

©️2020 CSDN 皮肤主题: 大白 设计师: CSDN官方博客 返回首页
实付0元
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值