Redux的全家桶與最佳實踐

2019獨角獸企業重金招聘Python工程師標準>>> hot3.png

04203537_ak6W.png
image.png


Redux 的第一次代碼提交是在 2015 年 5 月底(也就是一年多前的樣子),那個時候 React 的最佳實踐還不是明晰,作為一個 View 層,有人會用 backbone 甚至是 angular 和它搭配,也有人覺得這層 View 功能已經足夠強大,簡單地搭配一些 utils 就直接上。后來便有了 FLUX 的演講,React 社區開始注意到這種新的類似函數式編程的理念,Redux 也作為 FLUX 的一種變體開始受到關注,再后來順理成章地得到 React 的『欽點』,作者也加入了 Facebook 從事 React 的開發。生態圈經過了這一年的成熟,現在很多第三方庫已經非常完善,所以這里想介紹一下目前 Redux 的一些最佳實踐。

1. 復習一下 Redux 的基本概念

首先我們復習一下 Redux 的基本概念, 如果你已經很熟悉了,就直接跳過這一章吧。

Redux 把界面視為一種狀態機,界面里的所有狀態、數據都可以由一個狀態樹來描述。所以對于界面的任何變更都簡化成了狀態機的變化:

(State, Input) => NewState

這其中切分成了三個階段:

  1. action
  2. reducer
  3. store

所謂的 action,就是用一個對象描述發生了什么,Redux 中一般使用一個純函數,即 actionCreator 來生成 action 對象。

// actionCreator => action
// 這是一個純函數,只是簡單地返回 action
function somethingHappened(data){return {type: 'foo',data: data}
}

隨后這個 action 對象和當前的狀態樹 state 會被傳入到 reducer 中,產生一個新的 state

//reducer(action, state) => newState
function reducer(action, state){switch(action.type){case 'foo':return { data: data };default:return state;}
}

store 的作用就是儲存 state,并且監聽其變化。
簡單地說就是你可以這樣產生一個 store :

import { createStore } from 'redux'
//這里的 reducer 就是剛才的 Reducer 函數
let store = createStore(reducer);

然后你可以通過 dispatch 一個 action 來讓它改變狀態:

store.getState();//{}
store.dispatch(somethingHappened('aaa'));
store.getState(); // { data: 'aaa'}

好了,這就是 Redux 的全部功能。對的,它就是如此簡單,以至于它本體只有 3KB 左右的代碼,因為它只是實現了一個簡單的狀態機而已,任何稍微有點編程能力的人都能很快寫出這個東西。至于和 React 的結合,則需要 react-redux 這個庫,這里我們就不講怎么用了。

2. Redux的一些痛點

大體上,Redux 的數據流是這樣的:

界面 => action => reducer => store => react => virtual dom => 界面

每一步都很純凈,看起來很美好對吧?對于一些小小的嘗試性質的 DEMO 來說確實很美好。但其實當應用變得越來越大的時候,這其中存在諸多問題:

  1. 如何優雅地寫異步代碼?(從簡單的數據請求到復雜的異步邏輯)
  2. 狀態樹的結構應該怎么設計?
  3. 如何避免重復冗余的 actionCreator?
  4. 狀態樹中的狀態越來越多,結構越來越復雜的時候,和 react 的組件映射如何避免混亂?
  5. 每次狀態的細微變化都會生成全新的 state 對象,其中大部分無變化的數據是不用重新克隆的,這里如何提高性能?

你以為我會在下面一一介紹這些問題是怎么解決的?還真不是,這里大部分問題的回答都可以在官方文檔中看到: 技巧 | Redux 中文文檔 ,文檔里講得已經足夠詳細(有些甚至詳細得有些啰嗦了)。所以下面只挑 Redux 生態圈里幾個比較成熟且流行的組件來講講。

3. Redux 異步控制

官方文檔里介紹了一種很樸素的異步控制中間件 redux-thunk (如果你還不了解中間件的話請看 Middleware | Redux 中文文檔 ,事實上 redux-thunk 的代碼很簡單,簡單到只有幾行代碼:

function createThunkMiddleware(extraArgument) {return ({ dispatch, getState }) => next => action => {if (typeof action === 'function') {return action(dispatch, getState, extraArgument);}return next(action);};
}

它其實只干了一件事情,判斷 actionCreator 返回的是不是一個函數,如果不是的話,就很普通地傳給下一個中間件(或者 reducer);如果是的話,那么把 dispatchgetStateextraArgument 作為參數傳入這個函數里,實現異步控制。

比如我們可以這樣寫:

//普通action
function foo(){return {type: 'foo',data: 123}
}//異步action
function fooAsync(){return dispatch => {setTimeout(_ => dispatch(123), 3000);}
}

但這種簡單的異步解決方法在應用變得復雜的時候,并不能滿足需求,反而會使 action 變得十分混亂。

舉個比較簡單的例子,我們現在要實現『圖片上傳』功能,用戶點擊開始上傳之后,顯示出加載效果,上傳完畢之后,隱藏加載效果,并顯示出預覽圖;如果發生錯誤,那么顯示出錯誤信息,并且在2秒后消失。

用普通的 redux-thunk 是這樣寫的:

function upload(data){return dispatch => {// 顯示出加載效果dispatch({ type: 'SHOW_WAITING_MODAL' });// 開始上傳api.upload(data).then(res => {// 成功,隱藏加載效果,并顯示出預覽圖dispatch({ type: 'PRELOAD_IMAGES', data: res.images });dispatch({ type: 'HIDE_WAITING_MODAL' });}).catch(err => {// 錯誤,隱藏加載效果,顯示出錯誤信息,2秒后消失dispatch({ type: 'SHOW_ERROR', data: err });dispatch({ type: 'HIDE_WAITING_MODAL' });setTimeout(_ => dispatch({ type: 'HIDE_ERROR' }), 2000);})}
}

這里的問題在于,一個異步的 upload action 執行過程中會產生好幾個新的 action,更可怕的是這些新的 action 也是包含邏輯的(比如要判斷是否錯誤),這直接導致異步代碼中到處都是 dispatch(action) ,是很不可控的情況。如果還要進一步考慮取消、超時、隊列的情況,就更加混亂了。

所以我們需要更強大的異步流控制,這就是 GitHub - yelouafi/redux-saga: An alternative side effect model for Redux apps 。下面我們來看看如果換成 redux-saga 的話會怎么樣:

import { take, put, call, delay } from 'redux-saga/effects'
// 上傳的異步流
function *uploadFlow(action) {// 顯示出加載效果yield put({ type: 'SHOW_WAITING_MODAL' });// 簡單的 try-catchtry{const response = yield call(api.upload, action.data);yield put({ type: 'PRELOAD_IMAGES', data: response.images });yield put({ type: 'HIDE_WAITING_MODAL' });}catch(err){yield put({ type: 'SHOW_ERROR', data: err });yield put({ type: 'HIDE_WAITING_MODAL' });yield delay(2000);yield put({ type: 'HIDE_ERROR' });}     
}function* watchUpload() {yield* takeEvery('BEGIN_REQUEST', uploadFlow)
}

是不是規整很多呢?redux-saga 允許我們使用簡單的 try-catch 來進行錯誤處理,更神奇的是竟然可以直接使用 delay 來替代 setTimeout 這種會造成回調和嵌套的不優雅的方法。

本質上講,redux-sage 提供了一系列的『副作用(side-effects)方法』,比如以下幾個:

  1. put (產生一個 action)
  2. call (阻塞地調用一個函數)
  3. fork (非阻塞地調用一個函數)
  4. take (監聽且只監聽一次 action)
  5. delay (延遲)
  6. race (只處理最先完成的任務)

并且通過 Generator 實現對于這些副作用的管理,讓我們可以用同步的邏輯寫一個邏輯復雜的異步流。

下面這個例子出自于 官方文檔 ,實現了一個對于請求的隊列,即讓程序同一時刻只會進行一個請求,其它請求則排隊等待,直到前一個請求結束:

import { buffers } from 'redux-saga';
import { take, actionChannel, call, ... } from 'redux-saga/effects';function* watchRequests() {// 1- 創建一個針對請求事件的 channelconst requestChan = yield actionChannel('REQUEST');while (true) {// 2- 從 channel 中拿出一個事件const {payload} = yield take(requestChan);// 3- 注意這里我們使用的是阻塞的函數調用yield call(handleRequest, payload);}
}function* handleRequest(payload) { ... }

更多關于 redux-saga 的內容,請參考 Read Me | redux-saga (中文文檔: 自述 | Redux-saga 中文文檔 )。

4. 提高 selector 的性能

把 react 與 redux 結合的時候,react-redux 提供了一個極其重要的方法: connect ,它的作用就是選取 redux store 中的需要的 state 與 dispatch , 交由 connect 去綁定到 react 組件的 props 中:

import { connect } from 'react-redux';
import { toggleTodo } from '../actions'
import TodoList from '../components/TodoList'// 我們需要向 TodoList 中注入一個名為 todos 的 prop
// 它通過以下這個函數從 state 中提取出來:
const mapStateToProps = (state) => {// 下面這個函數就是所謂的selectortodos: state.todos.filter(i => i.completed)// 其它props...
}const mapDispatchToProps = (dispatch) => {onTodoClick: (id) => {dispatch(toggleTodo(id))}
}// 綁定到組件上
const VisibleTodoList = connect(mapStateToProps,mapDispatchToProps
)(TodoList)export default VisibleTodoList

在這里需要指定哪些 state 屬性被注入到 component 的 props 中,這是通過一個叫 selector 的函數完成的。

上面這個例子存在一個明顯的性能問題,每當組件有任何更新時都會調用一次 state.todos.filter 來計算 todos ,但我們實際上只需要在 state.todos 變化時重新計算即可,每次更新都重算一遍是非常不合適的做法。下面介紹的這個 reselect 就能幫你省去這些沒必要的重新計算。

你可能會注意到, selector 實際上就是一個『 純函數』

selector(state) => some props

而純函數是具有可緩存性的,即對于同樣的輸入參數,永遠會得到相同的輸出值 (如果對這個不太熟悉的同學可以參考 JavaScript函數式編程 ,reselect 的原理就是如此,每次調用 selector 函數之前,它會判斷參數與之前緩存的是否有差異,若無差異,則直接返回緩存的結果,反之則重新計算:

import { createSelector } from 'reselect';var state = {a: 100
}var naiveSelector = state => state.a;// mySelector 會緩存輸入 a 對應的輸出值
var mySelector = createSelector(naiveSelector, a => {console.log('做一次乘法!!!');return a * a;}
)console.log(mySelector(state));    // 第一次計算,需要做一次乘法
console.log(mySelector(state));    // 輸入值未變化,直接返回緩存的結果
console.log(mySelector(state));    // 同上
state.a = 5;                            // 改變 a 的值
console.log(mySelector(state));    // 輸入值改變,做一次乘法
console.log(mySelector(state));    // 輸入值未變化,直接返回緩存的結果
console.log(mySelector(state));    // 同上

上面的輸出值是:

做一次乘法!!!
10000
10000
10000
做一次乘法!!!
25
25
25

之前那個關于 todos 的范例可以這樣改,就可以避免 todos 數組被重復計算的性能問題:

import { createSelector } from 'reselect';
import { connect } from 'react-redux';
import { toggleTodo } from '../actions'
import TodoList from '../components/TodoList'const todoSelector = createSelector(state => state.todos,todos => todos.filter(i => i.completed)
)const mapStateToProps = (state) => {todos: todoSelector// 其它props...
}const mapDispatchToProps = (dispatch) => {onTodoClick: (id) => {dispatch(toggleTodo(id))}
}// 綁定到組件上
const VisibleTodoList = connect(mapStateToProps,mapDispatchToProps
)(TodoList)export default VisibleTodoList

更多可以參考 GitHub - reactjs/reselect: Selector library for Redux

5. 減少冗余代碼

redux 中的 action 一般都類似這樣寫:

function foo(data){return {type: 'FOO',data: data}
}//或者es6寫法:
var foo = data => ({ type: 'FOO', data})

當應用越來越大之后,action 的數量也會大大增加,為每個 action 對象顯式地寫上 type 和 data 或者其它屬性會造成大量的代碼冗余,這一塊是完全可以優化的。

比如我們可以寫一個最簡單的 actionCreator:

function actionCreator(type){return function(data){return {type: type,data: data}}
}var foo = actionCreator('FOO');
foo(123); // {type: 'FOO', data: 123}

redux-actions 就可以為我們做這樣的事情,除了上面這種樸素的做法,它還有其它比較好用的功能,比如它提供的 createActions 方法可以接受不同類型的參數,以產生不同效果的 actionCreator,下面這個范例來自官方文檔:

import { createActions } from 'redux-actions';const { actionOne, actionTwo, actionThree } = createActions({// 函數類型ACTION_ONE: (key, value) => ({ [key]: value }),// 數組類型ACTION_TWO: [(first) => first,               // payload(first, second) => ({ second }) // meta],// 最簡單的字符串類型
}, 'ACTION_THREE');actionOne('key', 1));
//=>
//{
//  type: 'ACTION_ONE',
//  payload: { key: 1 }
//}actionTwo('Die! Die! Die!', 'It\'s highnoon~');
//=>
//{
//  type: 'ACTION_TWO',
//  payload: ['Die! Die! Die!'],
//  meta: { second: 'It\'s highnoon~' }
//}actionThree(76);
//=>
//{
//  type: 'ACTION_THREE',
//  payload: 76,
//}

更多可以參考 GitHub - acdlite/redux-actions: Flux Standard Action utilities for Redux.

轉載于:https://my.oschina.net/cllgeek/blog/1584693

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/539926.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/539926.shtml
英文地址,請注明出處:http://en.pswp.cn/news/539926.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

php二分查找算法時間復雜度,一個運用二分查找算法的程序的時間復雜度是什么...

一個運用二分查找算法的程序的時間復雜度是“對數級別”。二分查找是一種效率較高的查找方法,算法復雜度即是while循環的次數,時間復雜度可以表示“O(h)O(log2n)”。本教程操作環境:windows7系統、Dell G3電腦。一個運用二分查找算法的程序的…

Android MediaPlayer使用方法簡單介紹

1)如何獲得MediaPlayer實例: 可以使用直接new的方式:MediaPlayer mp new MediaPlayer();也可以使用create的方式,如:MediaPlayer mp MediaPlayer.create(this, R.raw.test);//這時就不用調用setDataSource了* 需要在…

oracle基本的操作命令,oracle命令基本操作

--創建表空間create tablespace TBS_OTHERS datafile G:\APP\ORCL\ORADATA\ORCL\TBS_OTHERS01.dbf size 1000m;-- 創建用戶create user C##JHGL identified by jhgl default tablespace TBS_OTHERScreate user C##YJYJHGL identified by jhgl default tablespace TBS_OTHERScre…

將不確定變為確定~頭壓縮是否有必要,MVC如何實現頭壓縮

網頁的頭部壓縮在頁面體積大的情況下非常有必要做,它會使頁面體積有一個明顯的減小,同時加到網頁從服務端下載到客戶端的速度,以下是我做的一個測試: 沒有使用頭壓縮時: 使用了頭壓縮后: 我們可以看到&…

android .9.png ”點九” 圖片制作方法

“點九”是andriod平臺的應用軟件開發里的一種特殊的圖片形式,文件擴展名為:.9.png 智能手機中有自動橫屏的功能,同一幅界面會在隨著手機(或平板電腦)中的方向傳感器的參數不同而改變顯示的方向,在界面改變方向后,界面上的圖形會因為長寬的變化而產生拉伸…

servlet3.0異步處理

Servlet3是Tomcat7出現的新特性,所以需要先安裝tomcat7 微信企業號使用回調模式時: 假如企業無法保證在五秒內處理并回復,可以直接回復空串,企業號不會對此作任何處理,并且不會發起重試。這種情況下,可以…

使用svn diff的-r參數的來比較任意兩個版本的差異

1 svn diff的用法1.1 對比當前本地的工作拷貝文件(working copy)和緩存在.svn下的版本庫文件的區別[plain] view plaincopyprint? svn diff 1.2 對比當前本地的工作拷貝文件(working copy)和任意版本A的差異[plain] view plaincopyprint? svn diff -rA 比如,以下…

深入理解HTTP Session

session在web開發中是一個非常重要的概念,這個概念很抽象,很難定義,也是最讓人迷惑的一個名詞,也是最多被濫用的名字之一,在不同的場合,session一次的含義也很不相同。這里只探討HTTP Session。為了說明問題…

Hibernate的懶加載session丟失解決方法

在web.xml加入spring提供的過濾器&#xff0c;延長session的生命周期 <!--Hibernate的懶加載session丟失解決方法 --><filter><filter-name>openSessionInView</filter-name><filter-class>org.springframework.orm.hibernate4.support.OpenSess…

Linux訪問其他進程空間,Linux環境進程間通信系列(五):共享內存

共享內存可以說是最有用的進程間通信方式&#xff0c;也是最快的IPC形式。兩個不同進程A、B共享內存的意思是&#xff0c;同一塊物理內存被映射到進程A、B各自的進程地址空間。進程A可以即時看到進程B對共享內存中數據的更新&#xff0c;反之亦然。由于多個進程共享同一塊內存區…

沖刺NO.8

Alpha沖刺第八天 站立式會議 項目進展 項目穩步進行&#xff0c;項目的基礎部分如基本信息管理&#xff0c;信用信息管理等部分已相對比較完善。 問題困難 技術困難在短期內很難發生質的變化&#xff0c;而本項目由于選擇了隊員不太熟悉的程序框架&#xff0c;所以所以項目的交…

linux由眾多微內核組成,什么是linux

大家對Linux這個詞比較陌生吧&#xff0c;那么Linux是什么呢&#xff1f;Linux是什么Linux是一種自由和開放源碼的類Unix操作系統。目前存在著許多不同的Linux&#xff0c;但它們都使用了Linux內核。Linux可安裝在各種計算機硬件設備中&#xff0c;從手機、平板電腦、路由器和視…

淺析jQuery中常用的元素查找方法總結

$("#myELement") 選擇id值等于myElement的元素&#xff0c;id值不能重復在文檔中只能有一個id值是myElement所以得到的是唯一的元素 $("div") 選擇所有的div標簽元素&#xff0c;返回div元素數組 $(".myClass") 選擇使用myClass類的css的所有…

右擊菜單一鍵優化(增加新建office2003、新建reg和bat,刪除新建公文包、新建wps、新建rar)...

右擊菜單一鍵優化&#xff08;增加新建office2003、新建reg和bat&#xff0c;刪除新建公文包、新建wps、新建rar&#xff09; Windows Registry Editor Version 5.00 [HKEY_CLASSES_ROOT\.doc]"Word.Document.8""Content Type""application/msword&qu…

jquery獲取select選擇的顯示值

轉載自&#xff1a;http://blog.csdn.net/a5489888/article/details/8611703 本來以為jQuery("#select1").val();是取得選中的值&#xff0c; 那么jQuery("#select1").text();就是取得的文本。 這是不正確的&#xff0c;正確做法是&#xff1a; jQuery(&qu…

克隆整個linux系統環境的軟件,開源的系統克隆工具 Clonezilla(再生龍)linux、UBUNTU備份不用愁...

Clonezilla是一個很好的系統克隆工具,它基于Partimage,吸取了Norton Ghost和Partition Image的優點。即不僅支持對整個系統進行克隆,而且也可以克隆單個的分區,這種靈活性可能更能適應備份者的需要。支持GNU/Linux的文件系統 ext2、ext3、reiserfs、xfs、jfs和Windows的FAT、FA…

SqlServer2008備份與還原(完整圖示版)

一、備份 1、在需要備份的數據庫上&#xff0c;右鍵——任務——備份&#xff0c;如下&#xff1a; 2、選擇備份到哪個路徑和備份名字&#xff1a; 點擊“添加”&#xff0c;如下&#xff0c; 3、上面點擊“確定”后&#xff0c;回到第一個頁面&#xff0c;選中剛才添加的路徑和…

Jquery mobile問題總匯

轉載&#xff1a;http://www.wglong.com/main/artical!details?id4#q6 1頁面縮放顯示問題 問題描述&#xff1a; 頁面似乎被縮小了&#xff0c;屏幕太寬了。 解決辦法&#xff1a; 在head標簽內加入&#xff1a; <meta name"viewport" content"widthdevice…

Linux通過文件大小查找,linux 根據文件大小查找文件

linux下的find命令用來查找文件&#xff0c;通過man find就知道它是無所不能的。所以按照文件大小來查找文件就不在話下。從man find搜索size&#xff0c;可以看到如下信息&#xff1a;-size n[cwbkMG]File uses n units of space. The following suffixes can be used:b for 5…

DBCP連接池介紹

DBCP連接池介紹 ----------------------------- 目前 DBCP 有兩個版本分別是 1.3 和 1.4。 DBCP 1.3 版本需要運行于 JDK 1.4-1.5 &#xff0c;支持 JDBC 3。 DBCP 1.4 版本需要運行于 JDK 1.6 &#xff0c;支持 JDBC 4。 1.3和1.4基于同一套源代碼&#xff0c;含有所有的bug修…