先說結論
- Redux 是狀態管理庫,也是一種架構
- Redux 與 React 無關,但它是為了解決 React 元件中狀態無法共享而出的一種解決方案
- 單純的 Redux 只是一個狀態機, store 中存放了所有的狀態 state,要想改變裡面的狀態 state,只能 dispatch 一個動作
- 發出去的 action 需要用 reducer 來處理,傳入 state 和 action,返回新的 state
- subscribe 方法可以註冊回撥方法,當 dispatch action 的時候會執行裡面的回撥
- Redux 其實是一個釋出訂閱模式
- Redux 支援 enhancer,enhancer 其實就是一個裝飾器模式,傳入當前的 createStore,返回一個增強的 createStore
- Redux 使用 applyMiddleware 函式支援中介軟體,它的返回值其實就是一個 enhancer
- Redux 的中介軟體也是一個裝飾器模式,傳入當前的 dispatch,返回一個增強了的 dispatch
- 單純的 Redux 是沒有 View 層的
為什麼出現 Redux?
我們預設使用 React 技術棧,當頁面少且簡單時,完全沒必要使用 Redux。Redux 的出現,是為了應對複雜元件的情況。即當元件複雜到三層甚至四層時(如下圖),元件 4 想改變元件 1 的狀態
按照 React 的做法,狀態提升,將狀態提升至同一父元件(在圖中為祖父元件)。但層級一多,根元件要管理的 state 就很多了,不方便管理。
所以當初有了 context(React 0.14 確定引入),通過 context 能實現”遠房元件“的資料共享。但它也有缺點,使用 context 意味著所有的元件都可以修改 context 裡面的狀態,就像誰都可以修改共享狀態一樣,導致程式執行的不可預測,這不是我們想要的
facebook 提出了 Flux 解決方案,它引入了單向資料流的概念(沒錯,React 沒有單向資料流的概念,Redux 是整合了 Flux 的單向資料流理念),架構如下圖所示:
這裡不表 Flux。簡單理解,在 Flux 架構中,View 要通過 Action (動作)通知 Dispatcher(派發器),Dispatcher 來修改 Store,Store 再修改 View
Flux 的問題或者說缺點在哪?
store 之間存在依賴關係、難以進行伺服器端渲染、 stores 混雜了邏輯和狀態
筆者在學習的 React 技術棧時是 2018 年,那是已然流行 React + Redux 的解決方案,Flux 已經被淘汰了,瞭解 Flux 是為了引出 Redux
Redux 的出現
Redux 主要解決狀態共享問題
官網:Redux 是 JavaScript 狀態容器,它提供可預測的狀態管理
它的作者是 Dan Abramov
其架構為:
可以看得出,Redux 只是一個狀態機,沒有 View 層。其過程可以這樣描述:
- 自己寫一個 reducer(純函式,表示做什麼動作會返回什麼資料)
- 自己寫一個 initState(store 初始值,可寫可不寫)
通過 createStore 生成 store,此變數包含了三個重要的屬性
- store.getState:得到唯一值(使用了閉包老哥)
- store.dispatch:動作行為(改變 store 中資料的唯一指定屬性)
- store.subscribe:訂閱(釋出訂閱模式)
- 通過 store.dispatch 派發一個 action
- reducer 處理 action 返回一個新的 store
- 如果你訂閱過,當資料改變時,你會收到通知
按照行為過程,我們可手寫一個 Redux,下文在表,先說特點
三大原則
單一資料來源
- 整個應用的 全域性 state 被儲存在一棵 object tree 中,並且這個 object tree 只存在於唯一一個 store 中
State 是隻讀的
- 唯一改變 state 的方法就是觸發 action,action 是一個用於描述已發生時間的普通物件
使用純函式來執行修改
- 為了描述 action 如何改變 state tree,你需要編寫純的 reducers
三大原則是為了更好地開發,按照單向資料流的理念,行為變得可回溯
讓我們動手寫一個 Redux 吧
手寫 redux
按照行為過程和原則,我們要避免資料的隨意修改、行為的可回溯等問題
基礎版:23 行程式碼讓你使用 redux
export const createStore = (reducer, initState) => {
let state = initState
let listeners = []
const subscribe = (fn) => {
listeners.push(fn)
}
const dispatch = (action) => {
state = reducer(state, action)
listeners.forEach((fn) => fn())
}
const getState = () => {
return state
}
return {
getState,
dispatch,
subscribe,
}
}
搞個測試用例
import { createStore } from '../redux/index.js'
const initState = {
count: 0,
}
const reducer = (state, action) => {
switch (action.type) {
case 'INCREMENT':
return {
...state,
count: state.count + 1,
}
case 'DECREMENT':
return {
...state,
count: state.count - 1,
}
default:
return state
}
}
const store = createStore(reducer, initState)
store.subscribe(() => {
let state = store.getState()
console.log('state', state)
})
store.dispatch({
type: 'INCREMENT',
})
PS:俺是在 node 中使用 ES6 模組,需要升級 Node 版本至 13.2.0
第二版:難點突破:中介軟體
普通的 Redux 只能做最基礎地根據動作返回資料,dispatch 只是一個取資料的命令,例如:
dispatch({
type: 'INCREMENT',
})
// store 中的 count + 1
但在開發中,我們有時候要檢視日誌、非同步呼叫、記錄日常等
怎麼辦,做成外掛
在 Redux 中,類似的概念叫中介軟體
Redux 的 createStore 共有三個引數
createStore([reducer], [initial state], [enhancer]);
第三個引數為 enhancer,意為增強器。它的作用就是代替普通的 createStore,轉變成為附加上中介軟體的 createStore。打幾個比方:
- 託尼·斯塔克本來是一個普通有錢人,加上增強器(盔甲)後,成了鋼鐵俠
- 中央下發一筆救災款,加上增強器(大小官員的打點)後,到災民手上的錢只有一丟丟
- 路飛用武裝色打人,武裝色就是一箇中介軟體
enhancer 要做的就是:東西還是那個東西,只是經過了一些工序,加強了它。這些工序由 applyMiddleware 函式完成。按照行業術語,它是一個裝飾器模式。它的寫法大致是:
applyMiddleware(...middlewares)
// 結合 createStore,就是
const store = createStore(reudcer, initState, applyMiddleware(...middlewares))
所以我們需要先對 createStore 進行改造,判斷當有 enhancer 時,我們需傳值給中介軟體
export const createStore = (reducer, initState, enhancer) => {
if (enhancer) {
const newCreateStore = enhancer(createStore)
return newCreateStore(reducer, initState)
}
let state = initState;
let listeners = [];
...
}
如果有 enhancer 的話,先傳入 createStore 函式,生成的 newCreateStore 和原來的 createStore 一樣,會根據 reducer, initState 生成 store。可簡化為:
if (enhancer) {
return enhancer(createStore)(reducer, initState)
}
PS:為什麼要寫成這樣,因為 redux 是按照函式式寫法來寫的
為什麼 createStore 可以被傳值,因為函式也是物件,也可以作為引數傳遞(老鐵閉包了)
這樣我們的 applyMiddleware 自然就明確了
const applyMiddleware = (...middlewares) => {
return (oldCreateStore) => {
return (reducer, initState) => {
const store = oldCreateStore(reducer, initState)
...
}
}
}
這裡的 store 表示的是普通版中的 store,接下來我們要增強 store 中的屬性
我願稱之為:五行程式碼讓女人為我花了 18 萬
export const applyMiddleware = (...middlewares) => {
return (oldCreateStore) => {
return (reducer, initState) => {
const store = oldCreateStore(reducer, initState)
// 以下為新增
const chain = middlewares.map((middleware) => middleware(store))
// 獲得老 dispatch
let dispatch = store.dispatch
chain.reverse().map((middleware) => {
// 給每個中介軟體傳入原派發器,賦值中介軟體改造後的dispatch
dispatch = middleware(dispatch)
})
// 賦值給 store 上的 dispatch
store.dispatch = dispatch
return store
}
}
}
現在寫幾個中介軟體來測試一下
// 記錄日誌
export const loggerMiddleware = (store) => (next) => (action) => {
console.log('this.state', store.getState())
console.log('action', action)
next(action)
console.log('next state', store.getState())
}
// 記錄異常
export const exceptionMiddleware = (store) => (next) => (action) => {
try {
next(action)
} catch (error) {
console.log('錯誤報告', error)
}
}
// 時間戳
export const timeMiddleware = (store) => (next) => (action) => {
console.log('time', new Date().getTime())
next(action)
}
引入專案中,並執行
import { createStore, applyMiddleware } from '../redux/index.js'
import {
loggerMiddleware,
exceptionMiddleware,
timeMiddleware,
} from './middleware.js'
const initState = {
count: 0,
}
const reducer = (state, action) => {
switch (action.type) {
case 'INCREMENT':
return {
...state,
count: state.count + 1,
}
case 'DECREMENT':
return {
...state,
count: state.count - 1,
}
default:
return state
}
}
const store = createStore(
reducer,
initState,
applyMiddleware(loggerMiddleware, exceptionMiddleware, timeMiddleware),
)
store.subscribe(() => {
let state = store.getState()
console.log('state', state)
})
store.dispatch({
type: 'INCREMENT',
})
執行發現已經實現了 redux 最重要的功能——中介軟體
來分析下中介軟體的函數語言程式設計,以 loggerMiddleware 為例:
export const loggerMiddleware = (store) => (next) => (action) => {
console.log('this.state', store.getState())
console.log('action', action)
next(action)
console.log('next state', store.getState())
}
在 applyMiddleware 原始碼中,
const chain = middlewares.map((middleware) => middleware(store))
相當於給每個中介軟體傳值普通版的 store
let dispatch = store.dispatch
chain.reverse().map((middleware) => (dispatch = middleware(dispatch)))
相當於給每個中介軟體在傳入 store.dispatch,也就是 next,原 dispatch = next。這個時候的中介軟體已經本成品了,程式碼中的 (action) => {...}
就是函式 const dispatch = (action) => {}
。當你執行 dispatch({ type: XXX })
時執行中介軟體這段(action) => {...}
PS:柯里化一開始比較難理解,用多習慣就慢慢能懂
第三版:結構複雜化與拆分
中介軟體理解起來或許有些複雜,先看看其他的概念換換思路
一個應用做大後,單靠一個 JavaScript 檔案來維護程式碼顯然是不科學的,在 Redux 中,為避免這類情況,它提供了 combineReducers
來整個多個 reducer,使用方法如:
const reducer = combinReducers({
counter: counterReducer,
info: infoReducer,
})
在 combinReducers
中傳入一個物件,什麼樣的 state 對應什麼樣的 reducer。這就好了,那麼 combinReducers
怎麼實現呢?因為比較簡單,不做多分析,直接上原始碼:
export const combinReducers = (...reducers) => {
// 拿到 counter、info
const reducerKey = Object.keys(reducers)
// combinReducers 合併的是 reducer,返回的還是一個 reducer,所以返回一樣的傳參
return (state = {}, action) => {
const nextState = {}
// 迴圈 reducerKey,什麼樣的 state 對應什麼樣的 reducer
for (let i = 0; i < reducerKey.length; i++) {
const key = reducerKey[i]
const reducer = reducers[key]
const previousStateForKey = state[key]
const nextStateForKey = reducer(previousStateForKey, action)
nextState[key] = nextStateForKey
}
return nextState
}
}
同級目錄下新建一個 reducer 資料夾,並新建 reducer.js
、info.js
、index.js
// reducer.js
export default (state, action) => {
switch (action.type) {
case 'INCREMENT':
return {
count: state.count + 1,
}
case 'DECREMENT': {
return {
count: state.count - 1,
}
}
default:
return state
}
}
// info.js
export default (state, action) => {
switch (action.type) {
case 'SET_NAME':
return {
...state,
name: action.name,
}
case 'SET_DESCRIPTION':
return {
...state,
description: action.description,
}
default:
return state
}
}
合併匯出
import counterReducer from './counter.js'
import infoReducer from './info.js'
export { counterReducer, infoReducer }
我們現在測試一下
import { createStore, applyMiddleware, combinReducers } from '../redux/index.js'
import {
loggerMiddleware,
exceptionMiddleware,
timeMiddleware,
} from './middleware.js'
import { counterReducer, infoReducer } from './reducer/index.js'
const initState = {
counter: {
count: 0,
},
info: {
name: 'johan',
description: '前端之虎',
},
}
const reducer = combinReducers({
counter: counterReducer,
info: infoReducer,
})
const store = createStore(
reducer,
initState,
applyMiddleware(loggerMiddleware, exceptionMiddleware, timeMiddleware),
)
store.dispatch({
type: 'INCREMENT',
})
combinReducers
也完成了
既然拆分了 reducer,那麼 state 是否也能拆分,並且它是否需要傳,在我們平時的寫法中,一般都不傳 state。這裡需要兩點改造,一是每個 reducer 中包含了它的 state 和 reducer;二是改造 createStore,讓 initState 變得可傳可不傳,以及初始化資料
// counter.js 中寫入對應的 state 和 reducer
let initState = {
counter: {
count: 0,
},
}
export default (state, action) => {
if (!state) {
state = initState
}
switch (action.type) {
case 'INCREMENT':
return {
count: state.count + 1,
}
case 'DECREMENT': {
return {
count: state.count - 1,
}
}
default:
return state
}
}
// info.js
let initState = {
info: {
name: 'johan',
description: '前端之虎',
},
}
export default (state, action) => {
if (!state) {
state = initState
}
switch (action.type) {
case 'SET_NAME':
return {
...state,
name: action.name,
}
case 'SET_DESCRIPTION':
return {
...state,
description: action.description,
}
default:
return state
}
}
改造 createStore
export const createStore = (reducer, initState, enhancer) => {
if (typeof initState === 'function') {
enhancer = initState;
initState = undefined
}
...
const getState = () => {
return state
}
// 用一個不匹配任何動作來初始化store
dispatch({ type: Symbol() })
return {
getState,
dispatch,
subscribe
}
}
主檔案中
import { createStore, applyMiddleware, combinReducers } from './redux/index.js'
import {
loggerMiddleware,
exceptionMiddleware,
timeMiddleware,
} from './middleware.js'
import { counterReducer, infoReducer } from './reducer/index.js'
const reducer = combinReducers({
counter: counterReducer,
info: infoReducer,
})
const store = createStore(
reducer,
applyMiddleware(loggerMiddleware, exceptionMiddleware, timeMiddleware),
)
console.dir(store.getState())
到此為止,我們已經實現了一個七七八八的 redux 了
完整體的 Redux
退訂
const subscribe = (fn) => {
listeners.push(fn)
return () => {
const index = listeners.indexOf(listener)
listeners.splice(index, 1)
}
}
中介軟體拿到的 store
現在的中介軟體能拿到完整的 store,他甚至可以修改我們的 subscribe 方法。按照最小開放策略,我們只用給 getState 即可,修改下 applyMiddleware 中給中介軟體傳的 store
// const chain = middlewares.map(middleware => middleware(store))
const simpleStore = { getState: store.getState }
const chain = middlewares.map((middleware) => middleware(simpleStore))
compose
在我們的 applyMiddleware 中,把 [A, B, C] 轉換成 A(B(C(next))),效果是:
const chain = [A, B, C]
let dispatch = store.dispatch
chain.reverse().map((middleware) => {
dispatch = middleware(dispatch)
})
Redux 提供了一個 compose ,如下
const compose = (...funcs) => {
if (funcs.length === 0) {
return (args) => args
}
if (funcs.length === 1) {
return funcs[0]
}
return funcs.reduce((a, b) => (...args) => a(b(...args)))
}
2 行程式碼 replaceReducer
替換當前的 reudcer ,使用場景:
- 程式碼分割
- 動態載入
- 實時 reloading 機制
const replaceReducer = (nextReducer) => {
reducer = nextReducer
// 重新整理一次,廣播 reducer 已經替換,也同樣把預設值換成新的 reducer
dispatch({ type: Symbol() })
}
bindActionCreators
bindActionCreators 是做什麼的,他通過閉包,把 dispatch 和 actionCreator 隱藏起來,讓其他地方感知不到 redux 的存在。一般與 react-redux 的 connect 結合
這裡直接貼原始碼實現:
const bindActionCreator = (actionCreator, dispatch) => {
return function () {
return dispatch(actionCreator.apply(this, arguments))
}
}
export const bindActionCreators = (actionCreators, dispatch) => {
if (typeof actionCreators === 'function') {
return bindActionCreator(actionCreators, dispatch)
}
if (typeof actionCreators !== 'object' || actionCreators === null) {
throw new Error()
}
const keys = Object.keys(actionCreators)
const boundActionCreators = {}
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
const actionCreator = actionCreators[key]
if (typeof actionCreator === 'function') {
boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
}
}
return boundActionCreators
}
以上,我們就已經完成了 Redux 中所有的程式碼。大體上這裡 100 多行的程式碼就是 Redux 的全部,真 Redux 無非是加了些註釋和引數校驗
總結
我們把與 Redux 相關的名詞列出來,梳理它是做什麼的
createStore
- 建立 store 物件,包含 getState、dispatch、subscribe、replaceReducer
reducer
- 純函式,接受舊的 state、action,生成新的 state
action
- 動作,是一個物件,必須包括 type 欄位,表示 view 發出通知告訴 store 要改變
dispatch
- 派發,觸發 action ,生成新的 state。是 view 發出 action 的唯一方法
subscribe
- 訂閱,只有訂閱了,當派發時,會執行訂閱函式
combineReducers
- 合併 reducer 成一個 reducer
replaceReudcer
- 代替 reducer 的函式
middleware
- 中介軟體,擴充套件 dispatch 函式
磚家曾經畫過一張關於 Redux 的流程圖
換種思考方式理解
我們說過, Redux 只是一個狀態管理庫,它是由資料來驅動,發起 action,會引發 reducer 的資料更新,從而更新到最新的 store
與 React 結合
拿著剛做好的 Redux,放到 React 中,試試什麼叫 Redux + React 集合,注意,這裡我們先不使用 React-Redux,單拿這兩個結合
先建立專案
npx create-react-app demo-5-react
引入手寫的 redux 庫
在 App.js
中引入 createStore,並寫好初始資料和 reducer,在 useEffect 中監聽資料,監聽好之後當發起一個 action 時,資料就會改變,看程式碼:
import React, { useEffect, useState } from 'react'
import { createStore } from './redux'
import './App.css'
const initState = {
count: 0,
}
const reducer = (state, action) => {
switch (action.type) {
case 'INCREMENT':
return {
...state,
count: state.count + 1,
}
case 'DECREMENT':
return {
...state,
count: state.count - 1,
}
default:
return state
}
}
const store = createStore(reducer, initState)
function App() {
const [count, setCount] = useState(store.getState().count)
useEffect(() => {
const unsubscribe = store.subscribe(() => {
setCount(store.getState().count)
})
return () => {
if (unsubscribe) {
unsubscribe()
}
}
}, [])
const onHandle = () => {
store.dispatch({
type: 'INCREMENT',
})
console.log('store', store.getState().count)
}
return (
<div className="App">
<div>{count}</div>
<button onClick={onHandle}>add</button>
</div>
)
}
export default App
點選 button 後,資料跟著改變
PS:雖然我們可以用這種方式訂閱 store 和改變資料,但是訂閱的程式碼重複過多,我們可以用高階元件將他提取出去。這也是 React-Redux 所做的事情
與原生 JS+HTML 結合
我們說過,Redux 是個獨立於 Redux 的存在,它不僅可在 Redux 充當資料管理器,還可以在原生 JS + HTML 中充當起職位
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<div class="container">
<div id="count">1</div>
<button id="btn">add</button>
</div>
<script type="module">
import { createStore } from './redux/index.js'
const initState = {
count: 0,
}
const reducer = (state, action) => {
switch (action.type) {
case 'INCREMENT':
return {
...state,
count: state.count + 1,
}
case 'DECREMENT':
return {
...state,
count: state.count - 1,
}
default:
return state
}
}
const store = createStore(reducer, initState)
let count = document.getElementById('count')
let add = document.getElementById('btn')
add.onclick = function () {
store.dispatch({
type: 'INCREMENT',
})
}
// 渲染檢視
function render() {
count.innerHTML = store.getState().count
}
render()
// 監聽資料
store.subscribe(() => {
let state = store.getState()
console.log('state', state)
render()
})
</script>
</body>
</html>
效果如下:
狀態生態
我們從 Flux 說到 Redux,再從 Redux 說了各種中介軟體,其中 React-saga 就是為解決非同步行為而生的中介軟體,它主要採用 Generator(生成器)概念,比起 React-thunk 和 React-promise,它沒有像其他兩者將非同步行為放在 action creator 上,而是把所有的非同步操作看成“執行緒”,通過 action 觸發它,當操作完成後再次發出 action 作為輸出
function* helloWorldGenerator() {
yield 'hello'
yield 'world'
yield 'ending'
}
const helloWorld = helloWorldGenerator()
hewlloWorld.next() // { value: 'hello', done: false }
hewlloWorld.next() // { value: 'world', done: false }
hewlloWorld.next() // { value: 'ending', done: true }
hewlloWorld.next() // { value: undefined, done: true }
簡單來說:遇到 yield 表示式,就暫停執行後面的操作,並將緊跟 yield 後面的那個表示式的值,作為返回值 value,等著下一個呼叫 next 方法,再繼續往下執行
Dva
Dva 是什麼?
官網:Dva 首先是一個基於 Redux + Redux-saga 的資料流方案。為了簡化開發體驗,Dva 還額外內建了 react-router 和 fetch,所以可以理解為一個輕量級的應用框架
簡單來說,它是整合了現在最流行的資料流方案,即一個 React 技術棧:
dva = React-Router + Redux + Redux-saga + React-Redux
它的資料流圖為:
view dispatch 一個動作,改變 state(即 store),state 與 view 繫結,響應 view
其他不表,可去 Dva 官網檢視,這裡講講 Model ,它包含了 5 個屬性
namespace
- model 的名稱空間,同時也是他在全域性 state 上的屬性,只能用字串,不支援通過
.
的方式建立多層名稱空間
- model 的名稱空間,同時也是他在全域性 state 上的屬性,只能用字串,不支援通過
state
- 初始值
reducers
- 純函式,以 key/value 格式定義 reducer。用於處理同步擦做,唯一可以修改
state
的地方,由action
觸發 - 格式為:
(state, action) => newState
或[(state, action) => newState, enhancer]
- 純函式,以 key/value 格式定義 reducer。用於處理同步擦做,唯一可以修改
effects
- 處理非同步操作和業務邏輯,以 key/value 格式定義 effect
- 不直接修改 state。由 action 觸發
- call:執行非同步操作
- put:發出一個 Action,類似於 dispatch
subscriptions
- 訂閱
- 在
app.start()
時被執行,資料來源可以是當前的時間、伺服器的 websocket 連結、 keyboard 輸入、history 路由變化、geolocation 變化等等
Mobx
View 通過訂閱也好,監聽也好,不同的框架有不同的技術,總之 store 變化, view 也跟著變化
Mobx 使用的是響應式資料流方案。後續會單獨寫一篇,此篇太長,先不寫
補充:單向資料流
先介紹 React 中資料傳遞,即通訊問題
- 向子元件發訊息
- 向父元件發訊息
- 向其他元件發訊息
React 只提供了一種通訊方式:傳參。
即父傳值給子,子不能修改父傳的資料,props 具有不可修改性。子元件想把資料傳給父元件怎麼辦?通過 props 中的事件來傳值通知父元件
倉庫地址:https://github.com/johanazhu/...
本文參與了 SegmentFault 思否徵文「如何“反殺”面試官?」,歡迎正在閱讀的你也加入。