本篇文章主要內容是介紹 Redux 的歷史背景、實戰和概念,目標讀者設定爲 redux 初級玩家。javascript
既然定位爲初級玩家,那麼就不會講源碼、實現、設計原則這些東西。我會帶你站在歷史的角度俯覽 redux 的傳奇一輩子,並經過代碼示例掌握基本用法,經過圖示把道理捋明白。html
redux 官方自述是 A Predictable State Container for JS Apps,通俗理解,是一個用於 JavaScript 的狀態管理庫。注意:它特別強調了 Predictable State(可預測狀態)。前端
這個很好理解,萬物皆有狀態,能夠把人類簡單的分紅清醒/睡眠/昏迷等狀態。還能夠繼續細化,好比情緒,分爲生氣/冷靜/憤怒/開心等狀態。身體的某個部位,好比眼睛,失明/睜開/閉眼等狀態。vue
爲了更加容易理解狀態,這裏拿最典型的 TodoList 舉例。java
最原始的 web 應用,在沒有狀態的狀況下,應該怎麼作呢?react
<!-- todolist 原始版 -->
<div id="todo-list">
<div>
學英語<span> 未完成 </span><button onclick="complete(this)">完成</button>
</div>
<div>
學數學<span> 已完成 </span ><button onclick="complete(this, true)">取消完成</button>
</div>
<div>
學語文<span> 未完成 </span><button onclick="complete(this)">完成</button>
</div>
</div>
<script> function complete(target, cancel = false) { if (cancel) { target.textContent = "完成"; target.onclick = () => complete(target); target.previousElementSibling.textContent = " 未完成 "; } else { target.textContent = "取消完成"; target.onclick = () => complete(target, true); target.previousElementSibling.textContent = " 已完成 "; } } </script>
複製代碼
能夠看到,代碼比較簡單,這種代碼在四五年前是很是流行的,可是如今已經不多有人這麼寫代碼了。git
若是加入狀態的概念,應該怎麼作?github
<!-- todolist 狀態版 -->
<div id="todo-list"></div>
<script> var todoEl = document.getElementById("todo-list"); var state = { todoList: [ { id: 1, name: "學英語", complete: false }, { id: 2, name: "學數學", complete: true }, { id: 3, name: "學語文", complete: false } ] }; function render() { const todoHtml = state.todoList .map( item => `<div>${item.name} ${ item.complete ? `已完成 <button onclick='complete(${item.id}, true)'>取消完成</button>` : `未完成 <button onclick='complete(${item.id})'>完成</button>` }</div>` ) .join(""); todoEl.innerHTML = todoHtml; } function complete(id, cancel = false) { state.todoList.find(item => item.id === id).complete = !cancel; render(); } render(); </script>
複製代碼
能夠看到,比起原始版實現,狀態版的代碼好像更多。web
兩份代碼的顯示效果是徹底一致的,那麼,到底哪一個版本更好呢?數據庫
若是功能肯定下來,只有目前的這麼點功能,而且不會有任何需求的變更,那麼無疑,第一種實現是更優選擇。但這種狀況仍是在少數,更多狀況下,應用老是會存在各類不肯定性,隨時均可能變更。
之前的 web 應用,不能稱之爲應用,只能叫網頁,大一點的叫網站。如今爲何叫 web 應用了?由於這麼叫高大上嗎?並非,而是如今的 web 應用更大,更復雜了。
你能夠仔細觀察,原始版 button 的文字和 click 綁定的函數,與前面的「學數學」、「學英語」徹底無關。若是要給頁面添加新的元素,好比何時完成的,點擊每一條 item 會彈出詳情等。這樣擴展下去,應用會愈來愈混亂和複雜。
狀態版的實現,比原始版多了一個 state
變量和一個 render
函數。
其中,state
就是應用的狀態,當頁面發生操做時,修改 state
。當應用的 state
發生變化時,就會調用 render
方法,從新渲染頁面。
這就是有狀態和無狀態的區別。
是否是有點 react 和 vue 的味道了?
react 和 vue 都有 state
和 render
。react 的 state
,vue 的 data
,都是能夠響應數據變化而自動重繪頁面的。只不過實現方式不一樣。react 是在 this.setState
後發生重繪,vue 是經過對對象和數組進行數據劫持實現的,但它們同時也帶來了新的問題,好比 react 中異步的渲染,vue 在 data
聲明後新添加的屬性沒法自動響應等。更爲細節的部分很少說,不在該文章範圍之內。
如今已經明白了狀態的做用,那麼,既然 react 和 vue 自身都有狀態系統,爲何還須要狀態管理庫呢?
緣由是由於在組件化的探索中出現了問題。
一個正常的組件樹像下面這張圖。
使用組件化的應用,可能存在幾百上千個組件,若是不使用狀態管理庫,狀態會散落在每一個組件內部。一些須要共享的狀態,可能要傳給父組件,祖輩組件,也可能要傳給子組件、子孫組件,還可能要傳給兄弟組件,祖輩的兄弟組件等等。這些場景雖然仍能經過 props 的機制完成,可是很是不直觀,會讓人感到錯綜複雜,這好像又找到了以直接修改 DOM 的方式來編寫代碼的感受,這很危險。
爲此,react 沒有提供什麼特殊的方案,它建議直接使用 flux 模式來解決。而 vue 沒有放棄,它提供了不少種解決方案,v-model
、sync
、$attrs
、$listeners
等等,但沒有什麼實質性的改變。
跨組件通訊,是一個必需要解決的問題。
真正有實質性變化的,是 react 的 context API 和 vue 的 vue event bus 模式。
它們很像,又有所區別。它們存在一樣的問題,改變狀態的過程不夠直觀,雖然能夠跨組件修改某個狀態,但很難對這個操做進行跟蹤、定位和預測。
這樣雖然給咱們極大的自由任意操做全局狀態,但讓咱們難以快速找到到底是誰在何時改變了某個全局狀態。
因此,還須要更加規範細緻的解決方案,能夠追蹤數據什麼時候變化的狀態管理庫,即本節開頭所講的可預測狀態,這也是 redux 一再強調的特性。
解決問題的路上老是會出現更多新的問題,這個過程就像是俄羅斯套娃,一步步無限接近真理,可世界上根本沒有真理。
redux 在 github 上的第一次提交記錄是 2015 年的 5 月 31 日,提交者是 gaearon,名字翻譯過來你可能很是熟悉,叫蓋倫。這個蓋倫就是大名鼎鼎的 Facebook 工程師 Dan Abramov,國內俗稱 Dan 大神。dan 大神是一個很是真實的人,不掩飾問題,不故做高深。不少人都很是喜歡他,這裏是他的博客。
redux 並非憑空出現的,在它以前,facebook 還有一個叫作 flux 的庫,flux 是 2014 年 7 月 24 日開源的。對於如今的前端開發者來說,flux 可能比較陌生。由於在它那個時代,前端領域尚未特別重視應用的狀態,因此 flux 在當時並不流行。2014 年是什麼時代?要知道如今所謂的前端三大框架資歷最老的 Angular 出現的時間也纔是 2014 年 9 月 19 日,比 flux 還要晚出現 2 個月。那個時代,仍是 jQuery 和 Bootstarp 橫行的時代。flux 在最開始的一段時間裏,不少人不解和困惑,甚至有人提出 flux 是事件編程的倒退。其實當時不少人沒有正確地看到 flux 想作什麼,只是停留在表面的 API 的用法上,沒體會到 flux 真正的核心是一個單向數據流的狀態機。通過一段時間,flux 逐漸被人理解和承認。不巧的是以後不到一年的時間裏,mobx 和 redux 相繼出現,它們都在 flux 的基礎上作了大量改進,因此它們比 flux 更加優秀。flux 沒有機會大放異彩就被埋沒在了歷史的長河中,屬於一個曇花一現的庫。如今出現最頻繁的地方,大概就是相似我這篇文章同樣介紹 redux 歷史的文章或書中。
因此,前端的應用狀態這一律念在業界成型的時間大概是 2013 年到 2014 年左右。
若是以一個過來人的身份回答,redux 很簡單,它不難。
畢竟它的 js 源碼僅有 712 行,包括註釋和換行符,若是願意認真讀的話,半天時間就能讀完一遍。
但是若是把時間回放到幾年前我剛開始接觸 redux 的時候,我也是很懵的。
如今讓我以一個初學者的身份來回答這個問題的話,應該是這樣,redux 自己很是簡單,但學習它有些難度。
昨天看阮一峯老師最新寫的《科技愛好者週刊:第 99 期》中說了這麼一件事。
兩天前,ZDNet 發表了新文章《認識 iPad:提升你生產力的 10 個應用》。這一類的科普文章,每週都會出現,這難道不是一件很奇怪的事情嗎?
iPad 已經發布 10 年了,但是人們還必須看這種文章,說明你們還沒找到辦法,到底怎樣才能在 iPad 上進行實際工做!
這讓我想到了如今的 redux。其實到如今,仍是有不少人在寫關於 redux 的文章,也有不少人在問關於 redux 的疑惑。這說明你們須要 redux,但至今仍未找到學習 redux 的最好方式。因此我嘗試把我這幾年使用 Redux 的心得體會寫一寫,或許會對你們有所幫助。
dan 大神在 redux 發佈 3 年後的某一天,提交了一條commit。
標題是「Remove "Redux itself is very simple"」,意思是刪除了一段文字,「Redux 自己很是簡單」。
同時,dan 大神還在該條 commit 中提到:
Reflecting a few years later this was a bit of a silly thing to write in the docs. Of course it's not simple to people learning it.
翻譯成中文的意思是:
幾年後,仍在文檔中強調「react 自己很是簡單」是一件很愚蠢的事情。 固然,要學習它並不容易。
因而可知,Redux 對新手而言確實不怎麼友好。
至於怎麼學習,推薦三條路。
第一條,英語好的同窗,去看官方文檔,這是最佳學習方法。也能夠看一些優秀的資源。好比dan 大神的博客、dan 大神的視頻、Redux 官方推薦學習資源等。
第二條,技術很是強的同窗,大致翻閱下文檔,寫兩個 demo,而後去讀源碼吧。
第三條,技術通常,英語也挺差的同窗。看一些中文資料也不錯,好比如今你正在看的這篇文章。
學習這件事,儘可能仍是要去源頭看看。「取乎其上,得乎其中。取之於中,而求之於下。「。
但也不用過分強求,總之學會纔是目的,具體怎麼學,仍是要看你習慣哪一種方式。
redux 和相似的框架都在解決 web 應用中狀態難以管理的問題。
在早期,facebook 的 web 網站常常會碰到數據和視圖不一致的現象。好比消息圖標莫名其妙的亮起,當點擊圖標後,又發現沒有消息。facebook 的工程師們不止一次地解決這個 BUG,但每次修復後的一段段時間裏都會重複出現。
形成這個現象的緣由是數據和視圖的複雜關係。數據的流向很難預測,因此也很難理清它們之間具體的關係是怎樣的。
借用一張網圖來看一下 jQuery 時代的應用數據流向。
這很是糟糕。
facebook 的工程師在探索這個問題時,給出的第一個答案就是 flux。
flux 不只僅是一種庫或框架,更是一種模式或架構。這種模式或架構的名字也叫做單向數據流。
flux 很是好理解。
好比頁面初始化加載的這個動做,是一個 Action, dispatcher 會把 action 傳遞給 store,dispatcher 會修改應用的 store,store 的改變會重繪視圖 view。一個界面就加載出來了,很是簡單的原理。
視圖 view 上有一個按鈕,點擊按鈕的動做,又是一個 Action, action 又會告訴 dispatcher 該去通知 store 了,而後 store 會發生改變,重繪 view。如此循環往復,愈來愈簡單了。
從上面兩張圖中能夠看出,不管應用程序多麼複雜,數據變化的流向老是一致的。
若是再加上 api 的調用,流程是這樣的。
注意:這是 flux 的數據流向圖,redux 和它有所區別。但不用在乎,這裏只是大概演示下流程。
事實證實,flux 是對的。
在以後的探索中,facebook 又作出了更讓人滿意的答案,redux 和 mobx。尤爲是 redux。
雖然 flux 和 react 在設計原則和思想的細節上有較大的差別,但解決的問題是相同的。
react 解決的問題就是經過單向數據流的架構方式使應用的狀態按照必定的模式來變化,從而可以預測應用的狀態。
Redux 自己是徹底獨立運行的庫,不會基於某個庫或框架、也不會依附於某個庫或框架。因此,react 雖然能夠直接使用 redux,可是沒法和自身的響應式結合。
爲了解決這一問題,facebook 又開發了 react-redux。
二者是有區別的,redux 的責任是單純的狀態管理,react-redux 更像一個膠水,把 react 應用程序和 redux 狀態倉庫粘在一塊兒。讓 redux 中數據的變化能夠觸發 react 中的數據響應視圖。
下面這段話是來自於 redux 官網:
Keep in mind that Redux is only concerned with managing the state. In a real app, you'll also want to use UI bindings like react-redux.
翻譯成中文意思是:
請記住,Redux 僅與管理狀態有關。在真正的應用程序中,您還須要使用 UI 綁定的庫,例如react-redux。
不少人在學習和理解 redux 時,常常會出現概念混淆的問題,我以爲這是學習 redux 的一大屏障。事實上,概念越多的庫或框架,越難學習,好比 rx.js。
我認爲先學習用法,再去理解概念相對更友好一些。由於這樣更加直觀。
我一直在強調 redux 是能夠獨立運行的,從某種程度上,redux 和 react 沒有任何瓜葛,記住這一點,這很重要。
下面用代碼演示如何在原生 js 中使用 redux,仍然是那個 todolist 示例,拿以前寫的狀態版進行重構。
<!-- todolist redux版 -->
<script src="https://unpkg.com/redux@4.0.5/dist/redux.js"></script>
<div id="todo-list"></div>
<script> let todoEl = document.getElementById("todo-list"); // 1. 定義 action types,它描述了你的應用程序有幾種改變數據的操做 let COMPLETE = "COMPLETE"; let CANCEL_COMPLETE = "CANCEL_COMPLETE"; // 2. 定義 reducers // reducer 默認會有 2 個參數,第一個是初始狀態,第 2 個是 dispatch 傳遞進來的 action let initialState = [ { _id: 1, name: "學英語", complete: false }, { _id: 2, name: "學數學", complete: true }, { _id: 3, name: "學語文", complete: false } ]; function todoReducer(state = initialState, action) { // 經過判斷 action 的 type 屬性,來進行不一樣的 state 變化。 switch (action.type) { case COMPLETE: state.find(item => (item.id = action.id)).complete = true; return state; case CANCEL_COMPLETE: state.find(item => (item.id = action.id)).complete = false; default: return state; } } // 3. 調用 createStore 建立 store,todoReducer 是必傳參數 let store = Redux.createStore(todoReducer); // 4. 定義 actions creator,它們是一個函數,返回一個簡單對象 let completeAction = id => ({ type: COMPLETE, id }); let cancelCompleteAction = id => ({ type: CANCEL_COMPLETE, id }); function render() { // 5. 使用狀態時,調用 store 的 getState 方法能夠獲取最新的狀態 const todoHtml = store .getState() .map( item => `<div>${item.name} ${ item.complete ? `已完成 <button onclick='complete(${item._id}, true)'>取消完成</button>` : `未完成 <button onclick='complete(${item._id})'>完成</button>` }</div>` ) .join(""); todoEl.innerHTML = todoHtml; } function complete(id, cancel = false) { // 6. complete 函數再也不直接修改 state 中的數據,而是調用 store 對象的 dispatch 方法傳遞 action 的方式來建立新的 state store.dispatch(cancel ? cancelCompleteAction(id) : completeAction(id)); // render(); 再也不這裏重繪,而是使用 store 的 subscribe } // 7. 使用 store 的 subscribe 監聽 state 的變化,它的參數是一個回調函數,每次 state 變化,都會自動調用該函數 store.subscribe(render); render(); </script>
複製代碼
代碼中有詳盡的註釋,這幾乎是一個 redux 應用的最簡版本。看明白這個例子,就搞懂了 redux 最基本的使用。
雖然代碼的註釋中標註了各個步驟的序號,但你能夠不按照這個順序來寫代碼。標註只是爲了方便理解。
如今來回顧一下,上面的代碼都作了什麼。
首先要有一個 store,建立 store 須要調用 Redux.createStore()。 createStore 接受一個 reducer 函數做爲參數。reducer 默認有 2 個參數,第 1 個是 state,它是當前狀態樹,第 2 個是 action,這個參數其實就是 store 對象的 dispatch 方法傳遞的參數 action。
action 是一個結構簡單的對象,它有一個 type 屬性,用於標記這個 action 是作什麼的,與之對應的 reducer 函數會經過 switch 來處理這個 action。
建立 action 對象的函數叫作 action creator,它也很是簡單,就是返回一個 action 對象。
reducer 函數是處理數據變動的地方,它會返回一個新的對象,這個對象就是新的狀態。這和 Array 的 reduce 的運行機制很是相像。
store 的 getState 方法用於獲取當前狀態樹對象 state;subscribe 方法用於監聽 state 的變化,它接受一個函數做爲參數,每次數據發生變化時,調用改回調函數。
不少人在剛開始學習 redux 時,被各個概念和它們之間的關係弄的雲裏霧裏,我認爲只要把這些概念之間的關係梳理清楚,學習 redux 的一大門檻就算跨過去了,爲此我特地畫了一張簡單的關係圖。
若是你歷來沒有使用過 redux,那你必定會以爲這裏面的各類參數傳來傳去,函數調來調去,就像變戲法同樣。爲何不直接修改 state 呢?state 的本質不就是一個全局對象嗎?
確實是這樣,state 就是一個全局對象。
若是直接修改 state 會有幾個問題。
當一個數據沒達到預期時,很難找到究竟是在哪裏修改了這個數據。
雖然你可使用註釋來在必定程度上解決這個問題。
JavaScript 的對象是很是鬆散的,你能夠隨意修改,也能夠把它弄丟。好比在某個不起眼的角落,寫了一行 state = null;
處理數據最規範的手段就是經過某種模式來變動它們,而不是直接用=來修改。最典型的例子是數據庫。
在 2017 年 8 月份,有一篇文章曾經很是火爆,shape your store like your database。像數據庫同樣設計你的 Redux,你能夠讀一下。
redux 是一個 JavaScript 數據容器,其實它更像一個數據庫。
而咱們所作的一切和 redux 中那些看似繁瑣的 API 都是爲了讓數據的更新是可預測可追蹤的,若是使用 redux 的方式來處理數據,你能夠立刻找到此次狀態變動是由於什麼,是在哪一個地方讓數據發生了變化。這是 redux 的惟一好處。
再來思考一個問題,一個簡單的 todoList 應用把代碼弄的這麼複雜,有必要嗎?
事實上,不管如何都找不到任何須須使用 redux 的理由。
redux 帶給咱們的不只僅是學習成本,還會讓咱們多寫不少代碼。
這是付出,同時還要看收益。正常狀況下,收益要高於付出,至少也要持平,咱們纔會考慮付出。沒有人會傻到本身給本身刨坑吧?
不少初學者在學一門框架或庫時就想把全家桶全用上,這是絕對不可取的。
redux 的開發動機在官網上寫的很明白,就一句話:our code must manage more state than ever before.(咱們的代碼變必須管理比以往更多的狀態)
換句話說,咱們的應用中存在大量狀態時,才應該考慮使用 redux,而不是在一開始就優先考慮使用 redux。
上面的例子使用了大部分 redux 的核心 API,但沒介紹combineReducers
、applyMiddleware
、bindActionCreators
、compose
這幾個更高級的 API,由於它們都不是最核心的 API,而是爲了解決某項更高級的問題而存在的。這些不會在這裏講,但會在下一篇文章中提到。
dan 大神在 2018 年曾經發表過一篇文章,you might not need redux(你可能不須要 redux),你能夠讀一讀。而後認真思考,到底需不須要 redux。我所指的不是到底需不須要學習 redux,而是在你的應用程序中需不須要使用 redux。redux 是一個優秀的庫,做爲前端工程師,不管怎樣老是要見識一下的。
雖然 redux 能夠在任何環境下使用,但 facebook 開發它的最初目的仍是爲了解決大型 react 應用的狀態管理問題。
在 react 中使用 redux,通常都會用到 react-redux 這個庫。文章前面有提到,react-redux 自己就像是一個膠水,並不複雜。
它的用法大概是這樣。
首先導出一個叫作 Provider 的組件,而後在 Provider 組件中注入 store。再用 Provider 把應用的根組件包裹起來。這樣就可使用 store 了。
import React from "react";
import ReactDOM from "react-dom";
import { Provider } from "react-redux";
import store from "./store";
import App from "./App";
const rootElement = document.getElementById("root");
ReactDOM.render(
<Provider store={store}> <App /> </Provider>,
rootElement
);
複製代碼
react-redux 是基於 react 中的 context 來實現的,因此這一步是必須的。
須要使用 store state 的組件,使用 connect 函數將 store 和 react 的組件鏈接起來。
import { connect } from "react-redux";
import { increment, decrement, reset } from "./actionCreators";
const Counter = props => <div> {props.counter} </div>;
const mapStateToProps = (state /*, ownProps*/) => {
return {
counter: state.counter
};
};
const mapDispatchToProps = { increment, decrement, reset };
export default connect(mapStateToProps, mapDispatchToProps)(Counter);
複製代碼
若是不須要 store 的組件,在寫法遵循 react 的正常寫法便可,不須要變更。
代碼中多了兩個新的概念,mapStateToProps 和 mapDispatchToPorps,其實它們很是好理解。
mapStateToProps 是將 redux 中 state 映射到 react 組件的 props 中,其實就是 getState 的做用。
mapDispatchToProps 是將 redux 中的 dispatch 映射到 react 組件的 props 中,這樣就可使用props.increment
來調用 dispatch。
react-redux 的原理就是將數據提高至最高組件,而後在組件中經過 props 層層傳遞。
react-redux 的使用就這麼簡單,是的,很是簡單。
什麼是 hooks?
hooks 是 react 16.8 推出的新特性,一個替換 component 組件的方案,react 將來的發展方向。
在 hooks 出現以後,咱們再也不須要 connect。
react-redux 最經常使用的 hooks 有 3 個,useSelector、useDispatch 和 useStore。
useSelector 取代的是 mapStateToProps,useDispatch 取代的是 mapDispatchToProps。useStore 是對 store 的引用。
一樣是上面那段計數器代碼,用 hooks 會這樣寫。
import { useSelector, useDispatch } from 'react-redux'
import { increment, decrement, reset } from './actionCreators'
export default const Counter = () => {
const counter = useSelector(state=>state.counter);
const dispatch = useDispatch();
// 若是你要調用 dispatch
// dispatch(increment());
return (
<div> {counter} </div>
)
}
複製代碼
能夠看到,使用 hooks 後,代碼變得很是優雅。
hooks 已經出現 3 年,如今很是穩定,若是還認爲 hooks 是新特性,那真是有點跟不上時代的節奏了。我很是推薦使用 hooks,如今我開發的 react 項目中幾乎所有都是函數式組件和 hooks。
須要注意的是,hooks 不能在 class 組件中使用,它只能在函數組件中使用,並且只能在函數的最外層中使用,這些都取決於 hooks 的實現方式。
經過這篇文章的學習,你應該已經掌握了 react 最基本的使用,若是文中所講述的東西你都可以掌握並理解,那麼恭喜你,已經成爲一個合格的 redux 初級玩家了!
不過,遊戲纔剛剛開始。
接下來我會再寫兩篇關於 Redux 的文章,讀者羣體定位分別是中級玩家和高級玩家,敬請期待。
若是這篇文章對你有所幫助,而且你也喜歡個人文章風格,請關注個人微信公衆號。