- Published on
Redux 非同步資料流 - Redux saga 基本用法
- Authors
- Name
- Penghua Chen(PH)
今天要學習的是在 Redux 中另一套可以用來處理非同步資料套件: Redux saga。
這套與 Redux thunk 都是目前算是蠻主流的套件,在使用上可以依據自己的喜好選擇使用
往下學習怎麼用之前,我們先對於 Redux-saga 再來多一點認識吧!
Redux-saga 的特色
依據文件提到,這裡條列出幾個重點:
- 可以在 Redux-saga 中處理 side effect 的行為,例如像是 fetching data 等,可以更有效率、簡單的管理。
- 作為 Redux 的 middleware ,可以在 Redux 中調度 Redux 的操作,例如在 middleware 中 dispatch 一個 action。
- 使用 ES6 的生成器(Generators)來處理非同步流。
- 相較於 redux-thunk,不會陷入到 回呼地獄(callback hell)。
以上為文件中提到關於 redux-saga 的特色,其中也提到了一個很重要的核心概念:生成器(Generators)。
所以接著我們要先學習一下關於生成器(Generators)的一些知識。
生成器(Generators)
生成器(Generators)是 ES6 出的功能,那為什麼 Redux-saga 會是透過它來實作出這個套件呢?
接著我們看一下關於生成器(Generators)的定義與一些關鍵字
什麼是生成器?
生成器函式會產生一連串的值,但並不是一次全部產生出來,而是對每次請求產生一個值。
在產生一個值之後,生成器函式不會像正常函式那樣結束執行,它只是先暫停,然後再請求另一個值時,從暫停的地方恢復運作。
生成器不同於標準函式,呼叫生成器不是執行生成器函式,而是建立出一個迭代器(iterator)的物件。
如何定義一個生成器函式?
定義一個生成器函式: 在 function 後面加上一個星號(*) 即可。
然後透過 yield
關鍵字來產生個別的值。
如何取得生成器產生的值?
呼叫迭代器(iterator)的方法: next()
,可以向迭代器請求一個值,當生成器遇到 yeild
關鍵字時,會產生一個物件的值作為結果回傳,然後就暫時暫停執行,讓頁面其他動作可以繼續,直到生成器再次透過 next()
方法呼叫時才會再次喚醒生成器。
而剛剛提到回傳的結果為一個物件,從這個物件的值我們可以判斷是否還有值可以產生出來。
例如物件的值長這樣的時候:
{value: "Katana", done: false}
由於 done
是 false
,代表還有值可以產生。
如果得到的值是 {value: undefined, done: true}
,當 done
是 true
,代表已經沒有值可以產生了。
大致了解了生成器(Generators)的定義、設定方式和如何取值之後,透過一個簡單的程式碼來幫助理解吧
function* FruitGenerator() {
yield "apple";
yield "pineapple";
yield "orange";
}
var fruitIterator = FruitGenerator();
console.log(fruitIterator.next());
console.log(fruitIterator.next());
console.log(fruitIterator.next());
console.log(fruitIterator.next());
上面程式碼建立了一個 FruitGenerator
的生成器,並且我們產生了一個 fruitIterator
的迭代器物件,而當我們一次次請求時,也就是透過連續呼叫 next()
方法時,會取得一連串的回傳值:
{value: "apple", done: false}
{value: "pineapple", done: false}
{value: "orange", done: false}
{value: undefined, done: true}
關於生成器(Generators)的學習大致先到這裡,接著我們看看怎麼使用 Redux-saga
Redux-saga 的使用
安裝 Redux-saga
安裝的部分相信大家都很熟悉了,可以透過 npm 或 yarn 的方式安裝
npm install --save redux-saga
yarn add redux-saga
然後再往下之前,我們先將流程順過一次
理解加入 Redux-saga 前後的流程差異
將 redux-saga 加入到 redux 之前,我們先把 redux 更新同步資料的流程再順過一次:
這裡把重點放在 React component dispatch 一個 action 到 reducer 中。
- 在 component 中透過
connect
連接到 redux store - 透過
mapDispatchToProps
dispatch
一個action
到reducer
中,並依照 action type 執行對應的state
更新流程。
而加入 redux-saga 之後的流程:
- 在 component 中透過
connect
連接到 redux store - 透過
mapDispatchToProps
dispatch
一個action
到 redux-saga 中,然後 在 redux-saga 中依照 action type 執行對應的非同步流程(例如: call api)後,再把資料往reducer
中觸發對應的state
更新流程。
上述流程的差異一定要先弄懂,才不會在實作的時候覺得卡住哦!
模擬一個非同步流程: 兩秒後變更卡片元件的內容
這裡為了可以更加了解完整整個 Redux 加入 redux-saga 的流程。
所以撰寫了一個範例,並且從建置 Redux 開始一一了解。
相關測試範例,點擊前往。
首先是這個範例的資料結構:
- components: 管理 Card 元件
- store:
- reducers: 管理 reducer,並依據 action type 更新 state
- sagas: 管理非同步流程,從 React component 中先 dispatch 到 middleware 中,處理完非同步流程後再 dispatch 到 reducer 中。
建立一個 Card 元件
建立元件相信大家都很熟悉了,所以不再多做贅述:
稍微需要提的是這裡透過前面篇幅學到的 styled componet 方式設定樣式的部分。
import React from "react";
import styled from "styled-components";
const CardDiv = styled.div`
width: 200px;
border: 1px solid #8d8d8d;
border-radius: 10px;
margin: 0 auto;
`;
const Card = (props) => (
<CardDiv>
<h3 className="name">{props.person.name}</h3>
<p className="age">{props.person.age}</p>
<p className="habbit">{props.person.habbit}</p>
</CardDiv>
);
export default Card;
接著,安裝、引入 Redux、建立 reducer
安裝 Redux 的部分不多提,這邊直接看看引入與建立 reducer 檔案:
首先先建立 reducer:
const initialState = {
person: {
name: "Bill",
age: 28,
habbit: "Read comic books"
}
};
export const rootReducer = (state = initialState, action) => {
switch (action.type) {
case "changeCardContent":
return {
...state,
person: action.payload
};
default:
return state;
}
};
接著引入 Redux,並將 reducer 傳入:
// store/index.js
import { rootReducer } from "./reducers/rootReducer";
import { createStore } from "redux";
export const store = createStore(rootReducer);
連接 Redux 與 React component,以 class-based component 為例:
import React, { Component } from "react";
import { connect } from "react-redux";
import "./styles.css";
import Card from "./components/Card";
class App extends Component {
render() {
return (
<div className="App">
<h2>使用 redux-saga 模擬非同步流程</h2>
<p>點擊按鈕後兩秒變更卡片的內容。</p>
<Card person={this.props.person} />
<button>Click Me!</button>
</div>
);
}
}
const mapStateToProps = (state) => {
return {
person: state.person
};
};
export default connect(mapStateToProps)(App);
到這裡沒意外的話,應該可以成功將 Redux 中的 person
的資料渲染到畫面上囉!
重頭戲上場,加入 redux-saga middleware
完成前置作業後,接著我們將 redux-saga 這個 middleware 加入到 Redux 中了!
還記得剛剛提到的關於流程變更的那個部分嗎?
「透過 mapDispatchToProps dispatch 一個 action 到 redux-saga 中,然後 在 redux-saga 中依照 action type 執行對應的非同步流程(例如: call api)後,再把資料往 reducer 中觸發對應的 state 更新流程。」
這裡我們先在 sagas 資料夾中建立一個 index.js 檔案,並設定好讓 React component 可以 dispatch 到 saga middleware 中的 action 與要 dipatch 到 reducer 中的 action
// sagas/index.js
import { delay, put, takeEvery, all } from "redux-saga/effects";
function* changeCardContentHandler() {
console.log("redux-saga works and execute the generator");
// 模擬 call api 兩秒後資料回來
yield delay(2000);
// disptach an action to reducer
yield put({
type: "changeCardContent",
payload: {
name: "Charlie",
age: 13,
habbit: "Play ball"
}
});
}
// receive an action from react component
function* watchChangeCardContentHandlerSaga() {
yield takeEvery("CHANGE_CARD_CONTENT", changeCardContentHandler);
}
export function* rootSaga() {
yield all([watchChangeCardContentHandlerSaga()]);
}
這裡有一些關於 redux-saga 中的方法相向會讓人一頭霧水,這部分稍後會提到,我們先繼續把流程建置好。
接著我們要在 redux 中引入 redux-saga,並運行起來:
import { rootReducer } from "./reducers/rootReducer";
import { createStore, applyMiddleware } from "redux";
// 引入 redux-saga
import createSagaMiddleware from "redux-saga";
import { rootSaga } from "./sagas";
// 建立 sagaMiddleware 並引入到 redux 中
const sagaMiddleware = createSagaMiddleware();
export const store = createStore(rootReducer, applyMiddleware(sagaMiddleware));
// 運行我們設定的 saga generator
sagaMiddleware.run(rootSaga);
到這裡已經完成 redux-saga 的建置,最後一步就是在 React component 中 dispatch
一個 action
到 middleware 中囉
import React, { Component } from "react";
import { connect } from "react-redux";
import "./styles.css";
import Card from "./components/Card";
class App extends Component {
render() {
return (
<div className="App">
<h2>使用 redux-saga 模擬非同步流程</h2>
<p>點擊按鈕後兩秒變更卡片的內容。</p>
<Card person={this.props.person} />
// 觸發事件
<button onClick={this.props.changeCardContentHandler}>Click Me!</button>
</div>
);
}
}
const mapStateToProps = (state) => {
return {
person: state.person
};
};
const mapDispatchToProps = (dispatch) => {
return {
changeCardContentHandler: () => {
// dipatch an action to redux-saga middleware
dispatch({
type: "CHANGE_CARD_CONTENT"
});
}
};
};
export default connect(mapStateToProps, mapDispatchToProps)(App);
順利的應該可以看到成功模擬非同步流程的結果了!
不過還記得剛剛在過程中使用到的那些方法嗎?
- Middleware API
createSagaMiddleware(options)
middleware.run(saga, ...args)
- Effect creators
put(action)
takeEvery(pattern, saga, ...args)
delay(ms, [val])
- Effect combinators
all([...effects])
這些是 redux-saga 提供的方法,透過這些方法我們才可以完成整個 middleware 的建置,接著一一來理解這些輔助函式的定義吧!
createSagaMiddleware(options)
Middleware API: 建立 Redux middleware 並將 sagas 與 Redux Store 做連接。
middleware.run(saga, ...args)
Middleware API: 運行 saga,只能在 applyMiddleware
後的階段運行才有效。
put(action)
Effect creators: 如同 Redux 中的 dispatch,會 disptach 一個 action 到 reducer 中執行後續的 state 更新
takeEvery(pattern, saga, ...args)
Effect creators: 讓 React component 可以 dispatch 一個 action 到 middleware 中,並直接符合 pattern 時的第二個參數,也就是要執行的函式。
delay(ms, [val])
Effect creators: 用於延遲程式流程執行,單位為毫秒(ms)。
all([...effects])
Effect combinators: 可同時運行多個 Effects,並等待這些 Effects 執行完成後才會繼續流程。
關於 redux-saga 的基礎使用學習就到這裡囉,明天見
鐵人賽文章與程式碼同步發佈於: