ReactiveX流式程式設計ReactiveX
來自微軟,它是一種針對非同步資料流的程式設計。簡單來說,它將一切資料,包括HTTP請求,DOM事件或者普通資料等包裝成流的形式,然後用強大豐富的操作符對流進行處理,使你能以同步程式設計的方式處理非同步資料,並組合不同的操作符來輕鬆優雅的實現你所需要的功能。
為什麼從xstream講起
xstream的作者也是rxjs的深度使用者,但是作者基於一些實踐中考慮而開發這個庫,作者的解釋:WHY WE BUILT XSTREAM
- xstream只有26個核心操作符和工廠函式
- 只支援
熱
模式流 - 只有
stream
、listener
、producer
三個概念,比較好理解 - 壓縮後只有30kb大小,日常開發可以輕鬆整合代替部分繁瑣邏輯
xstream簡單上手
import xs from 'xstream'
// Tick every second incremental numbers,
// only pass even numbers, then map them to their square,
// and stop after 5 seconds has passed
var stream = xs.periodic(1000)
.filter(i => i % 2 === 0)
.map(i => i * i)
.endWhen(xs.periodic(5000).take(1))
// So far, the stream is idle.
// As soon as it gets its first listener, it starts executing.
stream.addListener({
next: i => console.log(i),
error: err => console.error(err),
complete: () => console.log('completed'),
})
複製程式碼
核心概念
Stream
流
代表從事件發生、處理、監聽的一條管道,每個stream都有很多operator類似:map
, filter
, fold
, take
;
每次呼叫operator都返回一個新的stream;
一般說來stream中的資料是由producer生產的,但是你可以呼叫shamefullySend*
系列函式手動發射事件,但是這種方法是反reactive
的,作者強烈不推薦使用;按照我的理解,這些方法在多個stream聯合工作的,用來mock某些流的資料時候會比較有用
Listener
監聽者
,stream的出口,消費管道最終產物;包含有3個方法
next
:stream裡每次有管道里產生的資料到流入到這個next方法裡接收error
:stream資料流轉中有異常情況時呼叫complete
:生產者呼叫了stop方法後呼叫
var listener = {
next: (value) => {
console.log('The Stream gave me a value: ', value);
},
error: (err) => {
console.error('The Stream gave me an error: ', err);
},
complete: () => {
console.log('The Stream told me it is done.');
},
}
複製程式碼
Producer
生產者,stream的入口,用來持續產生流的輸入
var producer = {
start: function (listener) {
this.id = setInterval(() => listener.next('yo'), 1000)
},
stop: function () {
clearInterval(this.id)
},
id: 0,
}
// This fellow delivers a 'yo' next event every 1 second
var stream = xs.create(producer)
複製程式碼
MemoryStream
記憶流
普通stream的記憶版:它會記住最後一次傳送給listener的next方法的資料,這樣後來addListener新增的監聽者能收到記住的這個資料; 這個特性是很有用的,能夠用來儲存應用執行過程中的一些臨時狀態。
Stream的構造-Factories
create
標準的通過producer構造, create(producer)
createWithMemory
標準的通過producer構造memorystream, createWithMemory(producer)
from
從Array|PromiseLike|Observable建立一個stream
of
從字面量建立一個stream,這樣建立的stream會立刻發射所有的引數,並觸發completed
fromPromise
從promise建立一個stream
merge
合併兩個stream成為一個stream,合併的後的資料按照原本的時間線繼續輸出(如下圖)
combine
這個單純用文字不太好解釋,請看下圖(借用的rxjs裡的combineLatest圖,功能是類似的)
另外,rxjs中還有個一個類似的zip操作符(xstream中不存在,自己實現),看下圖仔細體會和xstream的combine的不同
常用的操作符-Operators
map
mapTo
filter
take
drop
圖片借用的rx裡的skip,是一樣的效果
fold
圖片借用的rx裡的scan,是一樣的效果
flatten
這個是操作符就有點複雜了,涉及到了分流的情況,主要功能是將主stream裡返回的支流直接打平,輸出支流裡的資料;整個xstream標準operators(extra下有擴充套件的)裡只有這個操作符有涉及到分流的處理,彈珠(Marble)圖如下
這裡解釋一下,為什麼b輸出之後,主流程走到第二個tick,開始輸出第二個支流,這是第一個支流的後續輸出都會被廢棄;
實踐一個TODO List
流式思考
假如現在需要我們寫一個簡單的todolist:有一個 input
和一個 button
當我在input輸入內容之後,點選 button
就往todolist集合裡新增一條資料,每條todo行前面有個 checkbox
用來勾選todo的完成狀態,每條todo行後面有一個 del
按鈕,用來刪除這條todo
ok,讓我們開始之前先用 流
式的方式思考一下這個問題, 流
式的方式是基於時間線的演進系統動態變化的一個抽象,那麼基於此我們可以很簡單抽閒出 3
條時間線:
基於此,可以很容易寫出3條stream的程式碼如下:
// 工具函式,方便的建立dom事件流
import fromEvent from 'xstream/extra/fromEvent';
// 從新增按鈕建立的stream
const addTodoBtn$ = fromEvent(addBtnEl, 'click').map(() => inputEl.value).filter(v => v && v !== '');
// 從刪除按鈕觸發的stream
const delTodoBtn$ = fromEvent(document.body, 'click').map((e: Event) => e.target).filter((target: HTMLElement) => target.classList.contains('delTodo')).map((target: HTMLElement) => parseInt(target.dataset.index));
// 從標記完成選項觸發的stream
const toggleTodoInput$ = fromEvent(document.body, 'change').map((e: Event) => e.target).filter((target: HTMLElement) => target.classList.contains('toggleTodo')).map((target: HTMLInputElement) => ({ checked: target.checked, index: parseInt(target.dataset.index) }));
複製程式碼
好了,現在我們有了3條stream:
- addTodoBtn$在時間線上持續收集
button
的點選,並判斷input框裡是否有輸入有效內容,如果有的話就將輸入的內容作為stream的資料發射出去 - delTodoBtn$在時間線上持續收集
del
的點選,並將繫結的data-index數值發射出去 - toggleTodoInput$在時間線上持續收集
checkbox
的點選,並將當前checkbox的選中狀態和data-index一起發射出去
儲存狀態
現在我們有了3條stream,那麼該如何將這些stream與dom的操作對應起來呢?同時還有另外一個問題:傳統的開發過程中,我們需要有一個外部變數類似state這樣用來儲存每次操作後最新的todolist資料集合(副作用); 但是ReactiveX提倡的方式就是要消除副作用,我們需要一點兒技巧來處理這個狀況;
這裡我們思考一下整個操作分兩部分:增量資料、減量資料、更新資料, 而減量資料和更新資料都是基於增量資料來源的基礎上操作的;那麼我們需要定義一個增量資料來源,並且需要能持續保持這個資料來源最後的資料狀態。
讓我們翻翻上面的operators,看看有哪個操作符是可以用來持久儲存區域性變數的? 沒錯,就是 fold
操作符:
// 資料來源
const startUp$ = addTodoBtn$.fold((todos: Todo[], inputValue) => {
const todo:Todo = { text: inputValue, completed: false };
todos.push(todo);
return todos;
}, []);// 初始化空資料列表
複製程式碼
由於這裡的todolist的增量來源只有 button
一個(如果有多個,可以看看 combine
操作符,這裡不展開);
分支流和flatten應用
有了增量資料來源,那麼我們在增量資料來源的每個tick上分出一個減量、更新的 支流
(參看上面的flatten操作符),這樣支流執行的時候拿到的都是資料來源的最新資料;
// 監聽資料來源,並觸發刪除的stream
const delTodos$ = startUp$.map(todos => delTodoBtn$.map(index => {
console.log(index);
todos.splice(index, 1);
return todos;
})).flatten();
// 監聽資料來源,並觸發選中的stream
const toggleTodos$ = startUp$.map(todos => toggleTodoInput$.map(({ checked, index }) => {
console.log(checked, index);
if (todos[index]) {
todos[index].completed = checked;
}
return todos;
})).flatten();
複製程式碼
組裝stream
ok,現在我們的資料來源是多個,輸出的都是todolist最新的資料集合狀態,讓我們把這些stream管道組裝起來:
// 組合起來
const todos$ = xs.merge(startUp$, delTodos$, toggleTodos$);
todos$.addListener({
next: function(todos: Todo[]) {
console.log(todos);
renderTodos(todos);
},
error: function(e) {
console.error(e);
},
complete: function() {
}
})
複製程式碼
副作用
在定義完清晰的stream後,我們的實際業務程式碼就是這麼"簡單",由於stream的出口一直都是最新的todolist集合我們實現了類似react的全量渲染;哈哈,實際上這裡還有個不怎麼簡單的副作用方法:
const inputEl = document.getElementById('input') as HTMLInputElement;
const addBtnEl = document.getElementById('addBtn') as HTMLButtonElement;
const todoListEl = document.getElementById('lists');
const initTodos = [];
const renderTodos = function(todos: Todo[]) {
if (!todos) {
return;
}
todoListEl.innerHTML = '';
const fragement = document.createDocumentFragment();
todos.forEach((todo, index) => {
const liEL = document.createElement('li');
if (todo.completed) {
liEL.className = 'completed';
}
liEL.innerHTML = `<input type='checkbox' class="toggleTodo" name="toggleTodo" data-index="${index}" ${todo.completed ? 'checked' : null} />
<span>${todo.text}</span>
<a href='javascript:void(0);' class='delTodo' data-index="${index}">x</a>`;
fragement.appendChild(liEL);
});
todoListEl.appendChild(fragement);
}
複製程式碼
最終效果
總結
通過實際例子可以看到這裡有一大段不怎麼優雅的副作用方法,用來操作dom元素,由於我們基於全量渲染的思想,並沒有使用傳統的同步增刪改dom的方式,否則副作用程式碼會更多; 同時由於沒有vdom的加持,此段渲染程式碼純粹只能用來demo展示一下; 至於更優雅的副作用處理和vdom能力,可以期待我後續關於cycle.js的介紹?
專欄其他文章
FE One