前言
之前沒太理解redux,在使用時總是照葫蘆畫瓢,看專案裡別人如何使用,自己就如何使用,這一次徹底學習了下官方文件,記錄。
在學習redux初時,有三個概念需要了解。
- action
- reducer
- store
Action
型別是一個Object
更改store
中state
的唯一方法,它通過store.dispatch
將action
傳到store
中
一個簡單的action
function addTodo(text) {
return {
type: ADD_TODO,
text
}
}
複製程式碼
dispatch(addTodo(text))
複製程式碼
Reducer
根據action
,來指定store中的state如何改變。
store
儲存state
store.getState();
複製程式碼
- 提供
getState()
方法獲取state - 提供
dispatch(action)
更新state subscribe(listener)
來註冊、取消監聽器
更新store的步驟
1.建立action,action中必須要有type 2.建立reducer,根據action中的type來更新store中的state 3.初始化store
理解不可變性
在reducer更新state時,不能改變原有的state,只能重新建立一個新的state。這裡提供了幾個方法可以來建立一個不同的物件。
- 使用immutable-js建立不可變的資料結構
- 使用JavaScript庫(如Loadsh)來執行不可變的操作
- 使用ES6語法執行不可變操作
之前並不瞭解immutable-js
,所以還是使用es6的語法來執行不可變操作。
let a = [1, 2, 3]; // [1, 2, 3]
let b = Object.assign([], a); // [1, 2, 3]
// a !== b
複製程式碼
上面和下面是相同的
// es6語法
let a = [1, 2, 3]; // [1, 2, 3]
let b = [...a]; // [1, 2, 3]
// a !== b
複製程式碼
初始化store
在建立store時要將注意傳入開發者工具相關引數
import { createStore, applyMiddleware, compose } from 'redux'
import thunk from 'redux-thunk'
import { createLogger } from 'redux-logger'
import api from '../middleware/api'
import rootReducer from '../reducers'
import DevTools from '../containers/DevTools'
const configureStore = preloadedState => {
const store = createStore(
rootReducer,
preloadedState,
compose(
applyMiddleware(thunk, api, createLogger()),
DevTools.instrument()
)
)
// ..省略相關程式碼
return store
}
export default configureStore
複製程式碼
createStore
引數
- reducer (Function,必選):用於返回新的
state
,給出當前的state
和action
- preloadedState (Any,可選):初始化
state
, 你可以選擇將其指定為通用應用程式中的伺服器狀態,或者還原以前序列化的使用者會話,如果使用combineReducers
生成reducer
,則它必須是一個普通物件,其形狀與傳遞給它的鍵相同。否則,您可以自由地傳遞reducer
只要能夠理解。 - enhancer (Function,可選),可以指定它使用第三方功能增強
store
,例如中介軟體等等。隨Redux
一起提供的enhancer
只有applyMiddleware()
,傳入的enhancer只能是一個。
返回值
(Store): 儲存應用完整state
的物件,只要dispatching actions
才能改變它的state
。你可以用subscribe
它state
的改變來更新UI。
Tips
- 最多建立一個
store
在一個應用當中,使用combineReducers
來建立根reducer
- 你可以選擇狀態的格式,可以選擇普通物件或類似
Immutable
,如果不確定,先從普通物件開始 - 如果state是個普通物件,請確定永遠不要改變它,例如從
reducers
返回物件時,不要使用Object.assign(state, newData)
,而是返回Object.assign({}, state, newData)
。這樣就不會覆蓋以前的狀態,或者使用return {...state, ...newData}
- 要使用多個
enhancer
可以使用compose()
, - 建立
store
時,Redux
會傳送一個虛擬的action
用來初始化store
的state
,初始化時第一個引數未定義,那麼store的state會返回undefined
Enhancer
增強器
Middleware
官方文件中有提到,中介軟體是用來包裝dispatch
的
這裡看一個官方的例子,從這個例子中就可以看到,傳入引數是action
,隨後可以對這個action
進行一些操作。
import { createStore, applyMiddleware } from 'redux'
import todos from './reducers'
function logger({ getState }) {
return next => action => {
console.log('will dispatch', action)
// Call the next dispatch method in the middleware chain.
const returnValue = next(action)
console.log('state after dispatch', getState())
// This will likely be the action itself, unless
// a middleware further in chain changed it.
return returnValue
}
}
const store = createStore(todos, ['Use Redux'], applyMiddleware(logger))
store.dispatch({
type: 'ADD_TODO',
text: 'Understand the middleware'
})
// (These lines will be logged by the middleware:)
// will dispatch: { type: 'ADD_TODO', text: 'Understand the middleware' }
// state after dispatch: [ 'Use Redux', 'Understand the middleware' ]
複製程式碼
使用applyMiddleware
引數可以使多箇中介軟體,最後返回的是一個enhancer
相關提示
- 有一些中介軟體可能只在某個特定環境下使用,比如日誌中介軟體,可能在生成環境就不需要了。需要注意引用。
let middleware = [a, b]
if (process.env.NODE_ENV !== 'production') {
const c = require('some-debug-middleware')
const d = require('another-debug-middleware')
middleware = [...middleware, c, d]
}
const store = createStore(
reducer,
preloadedState,
applyMiddleware(...middleware)
)
複製程式碼
Provider與connect
需要額外安裝
yarn add react-redux
複製程式碼
provider和connect必須一起使用,這樣store
可以作為元件的props
傳入。關於Provider
和connect
,這裡有一篇淘寶的文章可以看下Provider和connect
大致使用如下,在root container
當中,會加入Provider
const App = () => {
return (
<Provider store={store}>
<Comp/>
</Provider>
)
};
複製程式碼
在根佈局下的元件當中,需要使用到connect
。
mapStateToProps
connect
方法第一個引數mapStateToProps
是可以將store
中的state
變換為元件內部的props
來使用。
const mapStateToProps = (state, ownProps) => {
// state 是 {userList: [{id: 0, name: '王二'}]}
// 將user加入到改元件中的props當中
return {
user: _.find(state.userList, {id: ownProps.userId})
}
}
class MyComp extends Component {
static PropTypes = {
userId: PropTypes.string.isRequired,
user: PropTypes.object
};
render(){
return <div>使用者名稱:{this.props.user.name}</div>
}
}
const Comp = connect(mapStateToProps)(MyComp);
複製程式碼
mapDispatchToProps
connect
方法的第二個引數,它的功能是將action
作為元件的props
。
const mapDispatchToProps = (dispatch, ownProps) => {
return {
increase: (...args) => dispatch(actions.increase(...args)),
decrease: (...args) => dispatch(actions.decrease(...args))
}
}
class MyComp extends Component {
render(){
const {count, increase, decrease} = this.props;
return (<div>
<div>計數:{this.props.count}次</div>
<button onClick={increase}>增加</button>
<button onClick={decrease}>減少</button>
</div>)
}
}
const Comp = connect(mapStateToProps, mapDispatchToProps)(MyComp);
複製程式碼
利用props使用store
import { setUser } from 'action';
// 在使用了connect的元件中 store在它的props當中
const { dispatch } = this.porps;
const user = ...;
// 直接分發設定user
dispatch(setUser(user));
複製程式碼
非同步場景下更新store
- Thunk middleware
- redux-promise
- redux-observable
- redux-saga
- redux-pack
- 自定義...
Redux-thunk
在沒有使用Redux-thunk
之前,當我們需要改變store中的state,只能使用使用dispath
傳入action
的形式,這裡有個官方的例子能夠說明它的使用場景。
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from './reducers';
// Note: this API requires redux@>=3.1.0
const store = createStore(
rootReducer,
applyMiddleware(thunk)
);
function fetchSecretSauce() {
return fetch('https://www.google.com/search?q=secret+sauce');
}
// These are the normal action creators you have seen so far.
// The actions they return can be dispatched without any middleware.
// However, they only express “facts” and not the “async flow”.
function makeASandwich(forPerson, secretSauce) {
return {
type: 'MAKE_SANDWICH',
forPerson,
secretSauce
};
}
function apologize(fromPerson, toPerson, error) {
return {
type: 'APOLOGIZE',
fromPerson,
toPerson,
error
};
}
function withdrawMoney(amount) {
return {
type: 'WITHDRAW',
amount
};
}
// Even without middleware, you can dispatch an action:
store.dispatch(withdrawMoney(100));
// But what do you do when you need to start an asynchronous action,
// such as an API call, or a router transition?
// Meet thunks.
// A thunk is a function that returns a function.
// This is a thunk.
function makeASandwichWithSecretSauce(forPerson) {
// Invert control!
// Return a function that accepts `dispatch` so we can dispatch later.
// Thunk middleware knows how to turn thunk async actions into actions.
return function (dispatch) {
return fetchSecretSauce().then(
sauce => dispatch(makeASandwich(forPerson, sauce)),
error => dispatch(apologize('The Sandwich Shop', forPerson, error))
);
};
}
// Thunk middleware lets me dispatch thunk async actions
// as if they were actions!
store.dispatch(
makeASandwichWithSecretSauce('Me')
);
// It even takes care to return the thunk’s return value
// from the dispatch, so I can chain Promises as long as I return them.
store.dispatch(
makeASandwichWithSecretSauce('My wife')
).then(() => {
console.log('Done!');
});
複製程式碼
thunk
可以讓我們在dispatch
執行時,可以傳入方法,而不是原本的action
。
我們可以看一下thunk
的原始碼,當action
是方法時,它會將action
進行返回。
function createThunkMiddleware(extraArgument) {
return ({ dispatch, getState }) => next => action => {
// action的型別是方法時,放回action
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
}
return next(action);
};
}
const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;
export default thunk;
複製程式碼
經過這樣,我們就可以理解為什麼在上述的官方例子當中可以這麼使用。
store.dispatch(
makeASandwichWithSecretSauce('My wife')
).then(() => {
console.log('Done!');
});
複製程式碼
makeASandwichWithSecretSauce
實際會返回fetch().then()
返回值,而fetch().then()
返回的是Promise物件。
Redux-saga
在開始講述saga
以前,先講下與它相關的ES6語法 Generator
函式
function* helloWorldGenerator() {
// 可以將yield看成return,只不過yield時,還能繼續
yield 'hello';
yield 'world';
return 'ending';
}
var hw = helloWorldGenerator();
hw.next()
// { value: 'hello', done: false }
hw.next()
// { value: 'world', done: false }
hw.next()
// { value: 'ending', done: true }
hw.next()
// { value: undefined, done: true }
複製程式碼
非同步Generator函式
這裡有2個方法,一個是通過回撥寫的,一個是通過generator來寫的
fs.readFile('/etc/passwd', 'utf-8', function (err, data) {
if (err) throw err;
console.log(data);
});
複製程式碼
function* asyncJob() {
// ...其他程式碼
var f = yield readFile(fileA);
// ...其他程式碼
}
複製程式碼
官方文件的一個例子如下
function render() {
ReactDOM.render(
<Counter
value={store.getState()}
onIncrement={() => action('INCREMENT')}
onDecrement={() => action('DECREMENT')}
onIncrementAsync={() => action('INCREMENT_ASYNC')} />,
document.getElementById('root')
)
}
複製程式碼
在使用saga
時,都會建立一個saga.js
,其餘的都是和普通的redux一樣,需要建立action``reducer
和store
import { delay } from 'redux-saga'
import { put, takeEvery } from 'redux-saga/effects'
// ...
// Our worker Saga: 將執行非同步的 increment 任務
export function* incrementAsync() {
yield delay(1000)
yield put({ type: 'INCREMENT' })
}
// Our watcher Saga: 在每個 INCREMENT_ASYNC action spawn 一個新的 incrementAsync 任務
export function* watchIncrementAsync() {
yield takeEvery('INCREMENT_ASYNC', incrementAsync)
}
複製程式碼
當主動觸發了onIncrementAsync
回撥之後,就會傳送一個INCREMENT_ASYNC
,在saga
接受到這個action時候,就會incrementAsync
,在這個方法當中會延遲1000毫秒,隨後put(類似於dispatch)傳送一個type為increment
的事件,在reducer
當中,可以根據這個action
做出對store
的state
進行操作。
我們可以看到這裡yield的使用更像是await。
兩種其實都是通過不同的非同步方式對store進行操作。thunk本身其實沒有非同步的功能,但是它能夠擴充dispath,加入傳入的是一個非同步方法,那就讓它能夠具有非同步的功能。
設定開發者工具
在官方Example當中有提到,建立一個DevTools
檔案,ctrl-h
開啟顯示toggle,ctrl-w
改變開發者工具的位置
import React from 'react'
import { createDevTools } from 'redux-devtools'
import LogMonitor from 'redux-devtools-log-monitor'
import DockMonitor from 'redux-devtools-dock-monitor'
export default createDevTools(
<DockMonitor toggleVisibilityKey="ctrl-h"
changePositionKey="ctrl-w">
<LogMonitor />
</DockMonitor>
)
複製程式碼
然後將該元件放在根目錄
import React from 'react'
import PropTypes from 'prop-types'
import { Provider } from 'react-redux'
import DevTools from './DevTools'
import { Route } from 'react-router-dom'
import App from './App'
import UserPage from './UserPage'
import RepoPage from './RepoPage'
const Root = ({ store }) => (
<Provider store={store}>
<div>
<Route path="/" component={App} />
<Route path="/:login/:name"
component={RepoPage} />
<Route path="/:login"
component={UserPage} />
<DevTools />
</div>
</Provider>
)
Root.propTypes = {
store: PropTypes.object.isRequired,
}
export default Root
複製程式碼
最後在createStore
時需要傳入
import DevTools from '../devtool'
const store = createStore(
rootReducer,
preloadedState,
compose(
applyMiddleware(thunk),
DevTools.instrument()
)
)
複製程式碼
效果圖如下
實戰
我們需要的要使用redux需要
- 建立action
- 建立對應reducer
- 建立store
同時,為了方便
- 需要有Provider
專案目錄
專案目錄如下所示
action/index.js
建立一個action
,用於告知reducer
,設定使用者資訊,增加一個type
,讓reducer
根據type
來更新store
中的state
。
export const TYPE = {
SET_USER: 'SET_USER'
};
export const setUser = (user) => ({
type: 'SET_USER',
user
});
複製程式碼
reducer/user.js
建立一個關於user
的reducer
import {
TYPE
} from '../action'
const createUser = (user) => user;
const user = (state = {}, action) => {
console.log(action);
switch (action.type) {
case TYPE.SET_USER:
// 根據type來更新使用者資訊
return {...state, ...createUser(action.user)};
default:
return state;
}
}
export {
user
}
複製程式碼
reducers/index.js
根reducer
,用於將其他不同業務的reducer
合併。
import { combineReducers } from 'redux';
import { user } from './user';
export default combineReducers({
user
});
複製程式碼
store/config-store.dev.js
store
中有不同的初始化store
的方法,dev中有開發者工具,而pro中沒有。這裡做了個區分。
import { createStore, applyMiddleware, compose } from 'redux'
import thunk from 'redux-thunk'
import rootReducer from '../reducers'
import DevTools from '../devtool'
const configureStore = preloadedState => {
const store = createStore(
rootReducer,
preloadedState,
compose(
applyMiddleware(thunk),
DevTools.instrument()
)
)
if (module.hot) {
// Enable Webpack hot module replacement for reducers
module.hot.accept('../reducers', () => {
store.replaceReducer(rootReducer)
})
}
return store
}
export default configureStore
複製程式碼
store/configure-store.prod.js
import { createStore, applyMiddleware } from 'redux'
import thunk from 'redux-thunk'
import rootReducer from '../reducers'
const configureStore = preloadedState => createStore(
rootReducer,
preloadedState,
applyMiddleware(thunk)
)
export default configureStore
複製程式碼
store/configure-store.js
根據不同環境讀取不同的初始化store的檔案。
if (process.env.NODE_ENV === 'production') {
module.exports = require('./configure-store.prod')
} else {
module.exports = require('./configure-store.dev')
}
複製程式碼
devtool/index.js
開發者元件的配置檔案。
import React from 'react'
import { createDevTools } from 'redux-devtools'
import LogMonitor from 'redux-devtools-log-monitor'
import DockMonitor from 'redux-devtools-dock-monitor'
export default createDevTools(
<DockMonitor toggleVisibilityKey="ctrl-h"
changePositionKey="ctrl-w">
<LogMonitor />
</DockMonitor>
)
複製程式碼
index.js
在index.js中初始化store
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import registerServiceWorker from './registerServiceWorker';
import configureStore from './store/store/configure-store';
const store = configureStore();
ReactDOM.render(
<App store={store}/>
, document.getElementById('root'));
registerServiceWorker();
複製程式碼
app.jsx
在根檔案中,建立provider
import React, { Component } from 'react'
import './App.css'
import './reset.css'
import 'antd/dist/antd.css'
import Auth from './pages/auth'
import Star from './pages/star/star'
import { BrowserRouter, Route, Redirect } from 'react-router-dom'
import DevTools from './store/devtool'
import { Provider } from 'react-redux'
class App extends Component {
constructor(props) {
super(props)
this.onClickAuth = this.onClickAuth.bind(this)
}
onClickAuth() {}
/**
* 渲染開發者工具
*/
renderDevTools() {
if (process.env.NODE_ENV === 'production') {
return null;
}
return (<DevTools />)
}
render() {
return (
<Provider store={this.props.store}>
<div className="App">
<BrowserRouter basename="/">
<div>
<Route exact path="/" component={Auth} />
<Route path="/auth" component={Auth} />
<Route path="/star" component={Star} />
{ this.renderDevTools() }
</div>
</BrowserRouter>
</div>
</Provider>
)
}
}
export default App
複製程式碼
更新使用者資訊
import React, { Component } from 'react';
import './star.scss';
import globalData from '../../utils/globalData';
import StringUtils from '../../utils/stringUtils';
import { List, Avatar, Row, Col } from 'antd';
import Api from '../../utils/api';
import Head from '../../components/Head/Head';
import ResInfo from '../../components/resInfo/resInfo';
import ControlList from '../../components/control/control-list';
import StarList from '../../components/star-list/star-list';
import Eventbus from '@/utils/eventbus.js';
import { connect } from 'react-redux';
import { setUser } from '../../store/action';
class Star extends Component {
constructor(props) {
super(props);
this.state = {
tableData: [],
originTableData: [],
userInfo: {},
rawMdData: ''
};
}
componentDidMount() {
this.getUserInfo();
}
componentWillUnmount() {
}
getUserInfo() {
Api.getAuthenticatedUser()
.then(data => {
this.handleGetUserInfoSuccessResponse(data);
})
.catch(e => {
console.log(e);
});
}
/**
* 獲取完使用者資訊
*/
handleGetUserInfoSuccessResponse(res) {
this.setState({
userInfo: res.data
});
this.getStarFromWeb();
this.refs.controlList.getTagsFromWeb();
const { dispatch } = this.props;
// 更新使用者資訊
dispatch(setUser(this.state.userInfo));
}
// ...省略一些程式碼
render() {
return (
<div className="star">
<Head
ref="head"
head={this.state.userInfo.avatar_url}
userName={this.state.userInfo.login}
/>
<Row className="content-container">
<Col span={3} className="control-list-container bg-blue-darkest">
<ControlList
ref="controlList"
onClickRefresh={this.onClickRefresh}
onClickAllStars={this.onClickAllStars}
onClickUntaggedStars={this.onClickUntaggedStars}
/>
</Col>
<Col span={5} className="star-list-container">
<StarList
tableData={this.state.tableData}
onClickResItem={this.onClickResItem.bind(this)}
/>
</Col>
<Col span={16}>
<div className="md-container">
<ResInfo resSrc={this.state.rawMdData} />
</div>
</Col>
</Row>
</div>
);
}
}
const mapStateToProps = (state, ownProps) => ({
user: state.user
});
export default connect(mapStateToProps)(Star);
複製程式碼