rudux
redux
執行流程圖:
簡單概述:click -> store.dispatch(action) -> reduer -> newState -> viewUpdate
react-readux 中 通過 connect 連結元件和 redux , this.props.dispatch() 呼叫
後面將會講到...
redux
依賴包也是十分的簡潔
demo
const redux = require('redux')
const createStore = redux.createStore
const types = {
UPDATE_NAME: 'UPDATE_NAME'
}
const defaultStore = {
user: 'tom'
}
/**
* reducer 純函式 接收一個state,返回一個新的state
* @param {Object} state
* @param {Object} action [type] 必選引數
* @return newState
* */
function getUser(state = defaultStore, action) {
const { type, payload } = action
let res = Object.assign({}, defaultStore)
switch (type) {
case types.UPDATE_NAME:
res.user = payload.name
break
default:
return res
}
return res
}
const store = createStore(getUser)
/**
* listener
* */
store.subscribe(() => {
console.log(store.getState())
})
/**
* dispatch(action) action
* */
store.dispatch({
type: types.UPDATE_NAME,
payload: {
name: '大帥哥'
}
})
//@log { name: '大帥哥' }
複製程式碼
- 使用者發出
action
【store.dispatch(action)
】 Store
自動呼叫Reducer
, 返回新的state
【let nextState = getUser(previousState, action)
】State
一旦有變化,Store
就會呼叫監聽函式 【store.subscribe(listener)
】
執行過程如下:
store
Store
就是儲存資料的地方,你可以把它看成一個容器。整個應用只能有一個 Store
常用方法:
- store.dispatch() :分發 action 較為常用
- store.subscribe() : state 發生變化後立即執行
- store.getState() : 獲取 store 中存著的 state
createStore
createStore 如其名,建立 store
下面是該方法的部分原始碼:
/**
* @param {Function} reducer 函式
* @param {any} [preloadedState] The initial state
* @param {Function} [enhancer] The store enhancer
* @returns {Store}
* */
export default function createStore(reducer, preloadedState, enhancer) {
if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
enhancer = preloadedState
preloadedState = undefined
}
if (typeof enhancer !== 'undefined') {
if (typeof enhancer !== 'function') {
throw new Error('Expected the enhancer to be a function.')
}
return enhancer(createStore)(reducer, preloadedState)
}
// ...
return {
dispatch, // 分發 action
subscribe, // 監聽器
getState, // 獲取 store 的 state 值
replaceReducer,
[$$observable]: observable // 供Redux內部使用
}
}
複製程式碼
preloadedState
: 初始化的initialState
,第二個引數不是Object
,而是Function
,createStore
會認為你忽略了preloadedState
而傳入了一個enhancer
createStore
會返回enhancer(createStore)(reducer, preloadedState)
的呼叫結果,這是常見高階函式的呼叫方式。在這個呼叫中enhancer
接受createStore
作為引數,對createStore
的能力進行增強,並返回增強後的createStore
dispatch(action)
diapatch
是 store 物件的方法,主要用來分發 action
,
redux 規定 action 一定要包含一個 type 屬性,且 type 屬性也要唯一
dispatch 是 store 非常核心的一個方法,也是我們在應用中最常使用的方法,下面是 dispatch 的原始碼 :
function dispatch(action) {
if (!isPlainObject(action)) {
// 校驗了action是否為一個原生js物件
throw new Error('Actions must be plain objects. ' + 'Use custom middleware for async actions.')
}
if (typeof action.type === 'undefined') {
// action物件是否包含了必要的type欄位
throw new Error('Actions may not have an undefined "type" property. ' + 'Have you misspelled a constant?')
}
if (isDispatching) {
// 判斷當前是否處於某個action分發過程中, 主要是為了避免在reducer中分發action
throw new Error('Reducers may not dispatch actions.')
}
try {
isDispatching = true
currentState = currentReducer(currentState, action)
} finally {
isDispatching = false
}
const listeners = (currentListeners = nextListeners)
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i]
listener()
}
// 在一系列檢查完畢後,若均沒有問題,將當前的狀態和action傳給當前reducer,用於生成新的state
return action
}
複製程式碼
reducer && store.replaceReducer
Redux 中負責響應 action 並修改資料的角色就是reducer
,reducer
的本質實際上是一個函式
replaceReducer:
/**
* @desc 替換當前的reducer的函式
* @param {Function}
* @return {void}
*/
function replaceReducer(nextReducer) {
if (typeof nextReducer !== 'function') {
throw new Error('Expected the nextReducer to be a function.')
}
currentReducer = nextReducer
dispatch({ type: ActionTypes.REPLACE })
}
複製程式碼
replaceReducer 使用場景:
- 當你的程式要進行程式碼分割的時候
- 當你要動態的載入不同的 reducer 的時候
- 當你要實現一個實時 reloading 機制的時候
中介軟體 middleware
以上介紹了 redux 的實現流的過程,應用場景無非於
button -- click --> disptch
-- action --> reducer
-- newState --> view
但是這種實現方式是基於同步的方式的,日常開發中當然少不了 http 這些非同步請求,這種情況下必須等到伺服器資料返回後才重新渲染 view, 顯然某些時候回阻塞頁面的展示。
舉例來說,要新增日誌功能,把 Action
和 State
列印出來,可以對 store.dispatch 進行如下改造。
let next = store.dispatch
store.dispatch = function dispatchAndLog(action) {
console.log('dispatching', action)
next(action)
console.log('next state', store.getState())
}
複製程式碼
上面程式碼中,對 store.dispatch 進行了重定義,在傳送 Action 前後新增了列印功能。這就是中介軟體的雛形。
中介軟體就是一個函式,對 store.dispatch 方法進行了改造,在發出 Action 和執行 Reducer 這兩步之間,新增了其他功能。
applyMiddleware
Redux 提供了applyMiddleware
來裝載middleware
:
它是 Redux 的原生方法,**作用是將所有中介軟體組成一個陣列,依次執行。**下面是它的原始碼。
/**
* @param {...Function} middlewares
* returns {Function} A store enhancer applying the middleware
*/
export default function applyMiddleware(...middlewares) {
return createStore => (...args) => {
const store = createStore(...args)
let dispatch = () => {
throw new Error(
`Dispatching while constructing your middleware is not allowed. ` +
`Other middleware would not be applied to this dispatch.`
)
}
const middlewareAPI = {
getState: store.getState,
dispatch: (...args) => dispatch(...args)
}
const chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch)
return {
...store,
dispatch
}
}
}
複製程式碼
所有中介軟體被放進了一個陣列 chain,然後巢狀執行,最後執行 store.dispatch。可以看到,中介軟體內部(middlewareAPI)可以拿到getState
和dispatch
這兩個方法
compose
實際上是函數語言程式設計中的組合,接收多個函式體並且將其組合成一個新的函式,例如compose
後 [fn1, fn2...] 依次從右到左巢狀執行函式 而compose
用於applyMiddleware
也是為了組合中介軟體
dispatch = compose(...chain)(store.dispatch)
==>
dispatch=fn1(fn2(fn3(store.dispatch)))
/**
* @param {...Function} funcs The functions to compose.
* @returns {Function} A function obtained by composing the argument functions
*/
export default function compose(...funcs) {
if (funcs.length === 0) {
return arg => arg
}
if (funcs.length === 1) {
return funcs[0]
}
return funcs.reduce((a, b) => (...args) => a(b(...args)))
}
複製程式碼
redux-thunk
上面的中介軟體的介紹可以知道
redux 通過 applyMiddleware
來裝載中介軟體,通過 compose 方法可以組合函式
非同步的問題可以通過 redux-thunk
解決,用法也不難 react 元件中使用相關如下:
// 配置 redux 加上這個...
import { createStore, applyMiddleware, compose } from 'redux'
import thunk from 'redux-thunk'
// ...
const store = createStore(getUser, compose(applyMiddleware(thunk)))
// react 中使用
import { connect } from 'react-redux'
handleClick = () => {
this.props.dispatch(dispatch => {
return axios.get('https://randomuser.me/api/').then(res => {
dispatch({
type: types.CHANGE_ARRAY,
payload: {
name: res.data.results[0].name.title
}
})
})
})
}
const mapStateToProps = (state, props) => {
return {
name: state.demo.name
}
}
export default connect(mapStateToProps)(Demo)
複製程式碼
處理非同步的還有很多外掛 如 redux-soga 等,樓主並未實踐過,所以不做延伸...
react-redux
下面是在 react 中使用的程式碼的雛形:
import { createStore } from 'redux'
let defaultState = {
count: 1
}
/**
* Reducer
* */
function demoReducer(state = defaultState, action = {}) {
const { type, payload } = action
const res = Object.assign({}, state)
if (type === 'changeCount') {
res.count = payload.count
}
return res
}
/**
* @Store 存資料的地方,你可以把它看成一個容器。整個應用只能有一個 Store。
* combineReducers({ ...reducers }) 可以組合多個reducer
* */
const store = createStore(
demoReducer,
window.devToolsExtension && window.devToolsExtension() // 配置redux 開發工具
)
// ... 根元素下配置下 Provider
import { Provider } from 'react-redux'
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
// 元件中使用
import { connect } from 'react-redux'
//use
this.dispatch({
type: 'changeCount',
payload: {
count: 22
}
})
const mapStateToProps = (state, props) => {
return {
name: state.demo.name
}
}
export default connect(mapStateToProps)(Demo)
複製程式碼
mapStateToProps
- 用於建立元件跟 store 的 state 的對映關係作為一個函式,它可以傳入兩個引數,結果一定要返回一個 object
- 傳入
mapStateToProps
之後,會訂閱 store 的狀態改變,在每次 store 的 state 發生變化的時候,都會被呼叫 - 如果寫了第二個引數 props,那麼當 props 發生變化的時候,mapStateToProps 也會被呼叫
mapDispatchToProps
mapDispatchToProps
用於建立元件跟 store.dispatch 的對映關係- 可以是一個 object,也可以傳入函式
- 如果
mapDispatchToProps
是一個函式,它可以傳入 dispatch,props,定義 UI 元件如何發出 action,實際上就是要呼叫 dispatch 這個方法
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
// 頁面中使用...
this.props.changeName()
const mapDispatchToProps = ({ changeName } = (dispatch, props) => {
return bindActionCreators(
{
changeName: function() {
return {
type: types.UPDATE_NAME,
payload: {
name: '大大大'
}
}
}
},
dispatch
)
})
export default connect(mapDispatchToProps)(App)
複製程式碼
模組化配置
下面的配置僅供參考。實現的功能:
- 整合
action
、types
、reducer
到一個檔案 - 根據開發/生成環境配置不同的
redux
中介軟體(開發環境配置dev-tools
) - 支援裝飾器模式
redux
熱載入配置(這裡面順便將react
熱載入配置也加上了)
注意:專案基於 create-react-app
eject
後的配置改造實現的。下面用了別名 @ ,需要改下 webpack
的配置,如果你配置不成功。詳情可以看我的 github
上面有原始碼. 連結入口
安裝
npm install redux react-redux redux-thunk --save
npm install redux-devtools-extension react-hot-loader -D
npm install @babel/plugin-proposal-decorators -D
複製程式碼
相關資料夾如圖:
models/demo.js
demo 模組。
// types
const ADD_COUNT = 'ADD_COUNT'
// actions
export const addCount = () => {
return { type: ADD_COUNT }
}
// state
const defaultState = {
count: 11
}
// reducer
export const demoReducer = (state = defaultState, action) => {
switch (action.type) {
case ADD_COUNT:
return { ...state, count: ++state.count }
default:
return state
}
}
export default demoReducer
複製程式碼
models/index.js
模組的匯出口。
import { combineReducers } from 'redux'
import demo from './demo'
export default combineReducers({
demo
})
複製程式碼
redux/index.js
redux
倉庫的總出口
import thunk from 'redux-thunk'
import { compose, createStore, applyMiddleware } from 'redux'
import { composeWithDevTools } from 'redux-devtools-extension'
import rootReducer from './models'
let storeEnhancers
if (process.env.NODE_ENV === 'production') {
storeEnhancers = compose(thunk)
} else {
storeEnhancers = compose(composeWithDevTools(applyMiddleware(thunk)))
}
const configureStore = (initialState = {}) => {
const store = createStore(rootReducer, initialState, storeEnhancers)
if (module.hot && process.env.NODE_ENV !== 'production') {
// Enable Webpack hot module replacement for reducers
module.hot.accept('./models', () => {
console.log('replacing reducer...')
const nextRootReducer = require('./models').default
store.replaceReducer(nextRootReducer)
})
}
return store
}
export default configureStore()
複製程式碼
src/index.js
react 專案的入口配置。
import React from 'react'
import ReactDOM from 'react-dom'
import { AppContainer } from 'react-hot-loader'
import App from './App'
import { Provider } from 'react-redux'
import store from '@/redux'
const render = Component => {
ReactDOM.render(
<AppContainer>
<Provider store={store}>
<Component />
</Provider>
</AppContainer>,
document.getElementById('root')
)
}
render(App)
if (module.hot) {
module.hot.accept('./App', () => {
render(App)
})
}
複製程式碼
App.jsx
import React, { Component, Fragment } from 'react'
import { connect } from 'react-redux'
import { addCount } from '@/redux/models/demo'
import { Button } from 'antd'
const mapStateToProps = state => ({
count: state.demo.count
})
@connect(
mapStateToProps,
{ addCount }
)
class ReduxTest extends Component {
render() {
return (
<Fragment>
{this.props.count}
<Button type="primary" onClick={this.props.addCount}>
Click
</Button>
<hr />
</Fragment>
)
}
}
export default ReduxTest
複製程式碼
.babelrc
配置 babel 裝飾器模式
{
"presets": ["react-app"],
"plugins": [
["@babel/plugin-proposal-decorators", { "legacy": true }]
]
}
複製程式碼
vscode 裝飾器模式如果有報警的話,可以根目錄下新建 jsconfig.json
{
"compilerOptions": {
"experimentalDecorators": true,
"baseUrl": "./",
"paths": {
"@/*": [
"src/*"
]
},
"jsx": "react"
},
"exclude": [
"node_modules",
"build",
"config",
"scripts"
]
}
複製程式碼
參考
- 阮一峰 redux 入門教程
- 配置檔案可以看我的 github : react-demo