Functional Reactive Programming với RxJs và Angular 2

Trong bài này chúng ta sẽ đi qua các khái niệm về Functional Reactive Programming(FRP) từ góc nhìn của một lập trình viên Angular 2. Hãy cùng nhau tìm hiểu các chủ đề sau:

  • Khái niệm lập trình bất đồng bộ với stream
  • Kiểu dữ liệu mới: Observables
  • Functional Reactive Programming và RxJs
  • Cốt lõi cơ chế hoạt động của Observables
  • Các toán tử thông dụng: map, filter, reduce, scan
  • Các hình thức sử dụng RxJs phổ biến trong Angular 2: Forms và Http
  • Hot vs Cold Observables
  • Kết luận

Functional Programming trong thế giới Frontend

Mặc dù Functional Programming (FP) được biết tới trong nhiều năm gần đây, nhưng việc nó được áp dụng vào quá trình phát triển chính của một ứng dụng vẫn chưa phổ biến.

Lập trình Frontend vốn không đồng bộ, và dường như là ko đủ cho việc xây dựng giao diện theo cách lập trình funtional

Khái niệm lập trình bất đồng bộ với stream

Stream: Một khái niệm không thế thiểu và là trung tâm của FP. Một stream hay còn được hiểu là một chuỗi các giá trị trong dòng thời gian. Ví dụ ta có stream của các giá trị số, trong đó mỗi giá trị được đưa ra mỗi giây:

0, 1, 2, 3, 4

Một ví dụ khác của stream là một chuỗi các sự kiện click chuột, với x và y là tọa độ của các cú click chuột:

(100, 200), (110, 300), (400, 50) ...

Mọi việc xảy ra trong trình duyệt có thể được nhìn thấy như là stream: chuỗi các sự kiện của trình duyệt được kích hoạt khi người dùng tương tác với các trang web, dữ liệu đến từ máy chủ, thời gian chờ việc kích hoạt...

Streams là một mô hình tốt để mô tả làm thế nào một chương trình frontend thực sự hoạt động.

Kiểu dữ liệu mới: Observables

Để các khái niệm về stream có ích trong việc xây dựng một chương trình, chúng ta cần một cách để tạo ra stream, subcribe, react và combine stream thành 1 stream mới,...

Chẳng hạn với một numeric stream như sau:

[0, 1, 2, 3, 4]

Trông rất giống một mảng trong Javascript!

Mảng là cấu trúc dữ liệu mà trong nó thực sự dễ dàng để thao tác và kết hợp để tạo mảng mới, nhờ các API mở rộng của nó.

Sự kết hợp của một stream với một tập hợp các functional operators để chuyển đổi nó, đưa chúng ta đến một khái niệm mới: Observable.

Bạn có thể sử dụng Observable để định nghĩa 1 stream và subcribe cũng như transform nó. Một điều quan trọng cần ghi nhớ là Observable không phải là stream.

Functional Reactive Programming và RxJs

RxJs là viết tắt của Reactive Extensions for Javascript.

Dưới đây là một numeric stream chúng ta đề cập ở trên, nhưng được định nghĩa bằng RxJs:

var obs = Rx.Observable.interval(1000).take(5);

Khi đã có khái niệm về Stream và Observable, chúng ta sẽ bắt đầu với các khái niệm về FRP khác.

Functional Reactive Programming (FRP) là một mô hình phát triển phần mềm mà toàn bộ các chương trình có thể được xây dựng xoay quanh khái niệm về stream. Không chỉ tồn tại ở frontend mà còn là toàn bộ program nói chung.

Khi phát triển trong mô hình này, chúng ta cần quan tâm tới việc xác định stream, kết hợp chúng lại với nhau, cuối cùng là lắng nghe và tạo ra 1 stream mới.

Mục tiêu của FRP

Ý tưởng chính của FRP là xây dựng chương trình theo cách mà chỉ khai báo và định nghĩa đâu là stream, làm cách nào mà chúng được liên kết với nhau và những gì sẽ xảy ra nếu một giá trị mới xuất hiện trong stream theo thời gian.

Các chương trình như này có thể được xây dựng với rất ít hoặc không có các biến trạng thái. Nói cách khác: Các ứng dụng kiểu này không có state không có nhà nước, nhưng các state này thường được lưu trữ trên các stream nhất định hoặc trong DOM, không phải trên các mã ứng dụng riêng của chương trình.

Cốt lõi cơ chế hoạt động của Observables

Hãy quay trở lại dãy số đơn giản định nghĩa bởi Observable trình bày trước, và thêm một effect với nó:

var obs = Rx.Observable.interval(500)
            .take(5)
            .do(i => console.log(i) );

Nếu chúng ta chạy chương trình này, bạn có thể ngạc nhiên bởi thực tế là không có gì được in ra trong console log! Tuy nhiên đây lại là một trong những thuộc tính chính của Observable.

Observables are either hot or cold

Nếu Observable không có một subscribers nào → nó sẽ không được kích hoạt!

Các Observable được cho là cold vì nó không tạo ra giá trị mới nếu không tồn tại một subscribers nào. Do đó để in các giá trị số trong console, chúng ta cần phải subscribe tới Observable:

obs.subscribe();

Bằng cách thêm đoạn code này chúng ta nhận được các giá trị số được in trong console. Nhưng điều gì sẽ xảy ra nếu chúng ta thêm subscribe cho observable này:

var obs = Rx.Observable.interval(500).take(5)
            .do(i => console.log("obs value "+ i) );

obs.subscribe(value => console.log("observer 1 received " + value));

obs.subscribe(value => console.log("observer 2 received " + value));

Những gì đang xảy ra ở đây là các tên obs sẽ được thực thi như sau:

  • In các giá trị trên console qua do() operator
  • Sau khi 2 subscribes được add vào obs, mỗi subscribes sẽ print giá trị kết quả ra console.
obs value 0
observer 1 received 0
obs value 0
observer 2 received 0

obs value 1
observer 1 received 1
obs value 1
observer 2 received 1

Hình như các các side-effect được gọi tới hai lần! ?? Điều này dẫn đến một thuộc tính quan trọng thứ hai của Observables.

Observables are not shared by default

Khi chúng ta tạo một subscriber, và thiết lập một dây quy trình xử lý mới. Biến obs chỉ đơn giản là một định nghĩa, một kế hoạch chi tiết về cách thức một quy trình xử lý được thiết lập từ khi source của event khởi động tới khi kết thúc.

Có nhiều cách để xác định các loại Observables nơi các side-effect sẽ chỉ được gọi một lần. Điều quan trọng là chúng ta nên giữ hai điều trong tâm trí mọi lúc khi xử lý với observables:

  • is the observable hot or cold?
  • is the observable shared or not?

Các toán tử thông dụng của RxJs operators trong Angular 2

How does Angular 2 use Observables

Angular 2 sử dung RxJs Observables theo 2 cách khác biệt sau:

  • Implementation trong core logic: EventEmitter
  • Một phần trong public API, ví dụ: Forms và HTTP module

Map operator

var obs = Rx.Observable.interval(500).take(5)
            .map(i => 2 * i );

Map và filter sử dụng trong form validation

<form [ngFormModel]="form" (ngSubmit)="onSubmit()">
   <p>
        <label>First Name:</label>
        <input type="text" ngControl="firstName">
   </p>
</form>

Chúng ta truy cập tới form observable thông qua form.valueChanges.

this.form.valueChanges
         .map((value) => {
            value.firstName = value.firstName.toUpperCase();
            return value;
         })
         .filter((value) => this.form.valid)
         .subscribe(validValue => ...);

Reduce operator

var obs = Rx.Observable.interval(500).take(5);

var reduced = obs.reduce((state, value) => state + value , 0);

reduced.subscribe(total => console.log("total =" + total));

Điều gì sẽ xảy ra ? Khi bạn tạo tới observable thứ 2 và đặt tên là reduced. Reduce phát ra một giá trị khi stream obs closes và giá trị này chính là tổng các giá trị trong stream. Khi đó Output trong console sẽ là:

total = 10

Scan operator

var obs = Rx.Observable.interval(500).take(5);

var scanObs = obs.scan((state, value) => state + value , 0);

scanObs.subscribe(total => console.log(total));

Kết quả trong console:

0
1
3
6
10

Share operator

Một tính chất quan trọng mà chúng ta đã bắt gặp từ đầu bài viết này. Share operator cho phép chia sẻ một single subscription trong quá trình xử lý chuỗi với một subscriber khác. Hãy xem ví dụ bên dưới:

var obs = Rx.Observable.interval(500).take(5)
            .do(i => console.log("obs value "+ i) )
            .share();

obs.subscribe(value => console.log("observer 1 received " + value));

obs.subscribe(value => console.log("observer 2 received " + value));

Chúng ta có output:

obs value 0
observer 1 received 0
observer 2 received 0

obs value 1
observer 1 received 1
observer 2 received 1

Rõ ràng side effect bên trong block lệnh do chỉ được gọi một lần thay vì 2 lần.

Kết luận

RxJSFRP là những khái niệm thực sự mạnh mẽ được tiếp xúc trong một số phần của Angular 2 API, và có thể được sử dụng để cấu trúc các ứng dụng được xây dựng theo một cách rất khác so với trước đây.

Có rất nhiều lựa chọn cho việc xây dựng cấu trúc một ứng dụng Angular 2. Một là sử dụng đầy đủ reactive RxJs, hoặc một tùy chọn khác là giữ cho ứng dụng đơn giản và chỉ sử dụng RxJs thông qua các Angular 2 API có sẵn (ví dụ như Forms và Http).

References


All Rights Reserved