react進階-redux
開始
Redux
,一種新型的前端“架構模式”。經常和 React.js
一併提出,你要用 React.js
基本都要伴隨着 Redux
和 React.js
結合的庫 React-redux
。
要注意的是,
Redux
和React-redux
並不是同一個東西。Redux
是一種架構模式(Flux 架構的一種變種),它不關注你到底用什麼庫,你可以把它應用到React
和Vue
,甚至跟jQuery
結合都沒有問題。而React-redux
就是把Redux
這種架構模式和React.js
結合起來的一個庫,就是Redux
架構在React.js
中的體現。
安裝redux
和 react-redux
npm i -S redux react-redux
那麼如何去使用它們? 首先得了解provider
組件,和connect
以及store
,action
,reducer
.
解析 redux
redux
中含有store
,action
,reducer
.redux
的作用是什麼? 它們是如何工作的.
redux
的作用就是用來管理數據狀態的.
隨着 JavaScript 單頁應用開發日趨複雜,JavaScript 需要管理比任何時候都要多的 state (狀態)。 這些 state 可能包括服務器響應、緩存數據、本地生成尚未持久化到服務器的數據,也包括 UI 狀態,如激活的路由,被選中的標籤,是否顯示加載動效或者分頁器等等。
管理不斷變化的 state 非常困難。如果一個 model 的變化會引起另一個 model 變化,那麼當 view 變化時,就可能引起對應 model 以及另一個 model 的變化,依次地,可能會引起另一個 view 的變化。直至你搞不清楚到底發生了什麼。state 在什麼時候,由於什麼原因,如何變化已然不受控制。 當系統變得錯綜複雜的時候,想重現問題或者添加新功能就會變得舉步維艱。
store
,action
,reducer
是如何協調工作的?
假設我們有一個事情清單要維護:
const state = {
todos: [
{
text: 'eat food',
completed: true,
},
{
text: 'Exercise',
completed: false
},
]
};
我們如何去維護這些數據? 比如增加一個要做的事情.
而reducer
就是做這個工作的,它完成了數據的初始化,並且決定了對這個數據有哪些行爲.並且它還是一個純函數(即不對參數進行修改的函數),要保證只要輸入的參數一樣,那麼結果一定相同.
const defaultState = {
todos: [],
}
function todoReducer(state=defaultState, action) {
switch (action.type) {
case 'insert':
const todo = { text: action.text, completed: false };
return { ...state, todos: [...state.todos, todo] };
default:
return { ...state };
}
}
我們只能夠通過這個todoReducer
來操作數據,以上我們需要傳入行爲action
,定義操作的類型以及數據.顯然這樣通過字符串的形式來判斷行爲,是不行的.所以我們可以預先定義action
,然後再傳入,可以聯想到設計模式中的工廠模式.
action.js
:
export const actionType = {
insert: 'insert',
}
export const insertTodo = (value)=>({
type: actionType.insert,
text: value,
});
acitonType
規定了aciton
的有那些行爲,並且提供了一些action
,這裏我們演示只有一個增加的action
.
然後我們reducer
就能夠寫成:
import { actionType } from './action';
const defaultState = {
todos: [],
}
function todoReducer(state = defaultState, action) {
switch (action.type) {
case actionType.insert:
const todo = { text: action.text, completed: false };
return { ...state, todos: [...state.todos, todo] };
default:
return { ...state };
}
}
export default todoReducer;
這樣子的話,我們可以通過獲取action
,給reducer
來完成我們對數據的操作.
那麼我們如何獲取state
數據? 如果有一個getter
函數就好了,這就是store
,並且它還封裝了reducer
.
store.js
import todoReducer from './todoReducer';
function createStore(reducer) {
let state = undefined;
const dispatch = (action) => {
state = reducer(state, action);
}
// 初始化state
dispatch({});
const getState = () => state;
return { getState, dispatch };
}
const store = createStore(todoReducer);
export default store;
這裏的store
有獲取state
的方法getState
,還有對其操作的函數dispatch
.故我們只要通過store
就能夠對數據進行操作.
示例:
import store from './store';
import { insertTodo } from './action';
import { Component } from 'react';
class Test extends Component {
constructor(props) {
super(props);
console.table(store.getState());
store.dispatch(insertTodo('run'));
store.dispatch(insertTodo('write a blog'));
console.table(store.getState());
}
render() {
return (null);
}
}
export default Test;
這裏我們增加了兩個事件,驗證一下:
這就是redux
的工作原理,還有一個問題就是store
就像是全局變量,可能兩個組件的數據變量可能會重複,會出現不方便維護,解決方案是我們爲每個組件設置成單獨的數據域,就好像:
{
app:{...},
todoList: {...},
...
}
類似這種效果,這樣也不會產生數據的污染.
redux
恰好就提供了combineReducers
函數來實現這個效果.它就是結合reducer
來爲索引.
先創建一個總的 reducer
:
import { combineReducers } from 'redux';
import todoReducer from './todoReducer';
const rootReducer = combineReducers({
todoReducer,
});
export default rootReducer;
這裏將剛纔的todoReducer
放進去總reducer
.
然後我們的store
引入該reducer
:
import rootReducer from './rootReducer';
import { createStore } from 'redux';
const store = createStore(rootReducer);
export default store;
createStore
,是redux
已經幫我們寫好了,我們直接用就行.
constructor(props) {
super(props);
console.table(store.getState());
store.dispatch(insertTodo('run'));
store.dispatch(insertTodo('write a blog'));
console.table(store.getState());
}
我們同樣的對其進行打印,看看數據是否有變化.
我們可以看到,store
保存的數據結構已經發生了變化,已經開始分區域保存數據,實際保存數據的其實是reducer
,只不過store
包含了所有的reducer
.
結合 react-redux
React-Redux
是Redux
的官方React
綁定庫。它能夠使你的React
組件從Redux store
中讀取數據,並且向store
分發actions
以更新數據.
react-redux
提供了兩個東西,<Provider/>
組件和connect
函數.
React-Redux
將所有組件分成兩大類:UI 組件(presentational component)和容器組件(container component)。UI 組件:
只負責 UI 的呈現,不帶有任何業務邏輯,沒有狀態(即不使用this.state這個變量,所有數據都由參數(this.props)提供,
不使用任何 Redux 的 API.
容器組件:
容器組件的特徵恰恰相反,負責管理數據和業務邏輯,不負責 UI 的呈現,帶有內部狀態,使用 Redux 的 API
Provider
React-Redux
提供<Provider/>
組件,能夠使你的整個app
訪問到Redux store
中的數據.
源碼:
class Provider extends Component {
getChildContext() {
return {
store: this.props.store
};
}
render() {
return this.props.children;
}
}
可以看到其作用就是將store
綁定在了上下文對象context
,然後子組件就可以通過context
獲取到store
.
connect()
connect
方法,用於從 UI
組件生成容器組件.
import React, { Component } from 'react'
import PropTypes from 'prop-types'
export const connect = (mapStateToProps) => (WrappedComponent) => {
class Connect extends Component {
static contextTypes = {
store: PropTypes.object
}
render () {
const { store } = this.context
let stateProps = mapStateToProps(store.getState())
// {...stateProps} 意思是把這個對象裏面的屬性全部通過 `props` 方式傳遞進去
return <WrappedComponent {...stateProps} />
}
}
return Connect
}
它從context
獲取了store
,並且將store
裏的數據,通過props
注入到了組件,所以組件可以通過props
裏讀取store
裏的數據,而mapStateToProps
,顧名思義就是將store
裏的state
映射到組件的props
.
還能繼續深入對dispatch
進行映射,同樣映射到props
裏,並且數據dispatch
之後要重新渲染組件.
export const connect = (mapStateToProps, mapDispatchToProps) => (WrappedComponent) => {
class Connect extends Component {
static contextTypes = {
store: PropTypes.object
}
constructor () {
super()
this.state = {
allProps: {}
}
}
componentWillMount () {
const { store } = this.context
this._updateProps()
// 訂閱,只要dispatch就會調用傳入的函數
store.subscribe(() => this._updateProps())
}
_updateProps () {
const { store } = this.context
let stateProps = mapStateToProps
? mapStateToProps(store.getState(), this.props)
: {} // 防止 mapStateToProps 沒有傳入
let dispatchProps = mapDispatchToProps
? mapDispatchToProps(store.dispatch, this.props)
: {} // 防止 mapDispatchToProps 沒有傳入
this.setState({
allProps: {
...stateProps,
...dispatchProps,
...this.props
}
})
}
render () {
return <WrappedComponent {...this.state.allProps} />
}
}
return Connect
}
這樣就也就完成了state
和dispatch
映射到props
的過程.
實際上react-redux
已經封裝了該函數,所以我們直接用就行了.
示例: 將 redux
和react
結合起來,圖像化.
import React from 'react';
import TodoList from './components/react-redux/TodoList';
import { connect } from 'react-redux';
import { insertTodo } from './store/action'
class App extends React.Component {
constructor(props) {
super(props);
this.state = { value: '' };
}
handleChange = (e) => {
const value = e.target.value;
this.setState({
value,
});
}
handleClick = () => {
this.props.insertTodo(this.state.value);
this.setState({
value:'',
});
}
render() {
return (
<div>
<input value={this.state.value} onChange={this.handleChange} />
<button onClick={this.handleClick}>add</button>
<TodoList todos={this.props.todos} />
</div>
);
}
}
const mapToStateToProps = (state) => {
return {
todos: state.todoReducer.todos,
}
}
const mapToDispatchToProps = (dispatch) => {
return {
insertTodo: (value) => { dispatch(insertTodo(value)) },
}
}
export default connect(mapToStateToProps, mapToDispatchToProps)(App);
這裏我們返回的是一個由connect
生成的容器組件.並且完成了映射,將state.todoReducer.todos
映射到了props.todos
上,dispatch
也是如此.所以在組件裏,我們直接用props
調用就行了,TodoList
就是簡單的react
組件.
附上TodoList.js
:
import React, { Component } from 'react';
import Todo from './Todo';
class TodoList extends Component {
render() {
const { todos } = this.props;
return (
<ul>
{todos.map(todo => {
return <Todo todo={todo} />;
})}
</ul>
);
}
}
export default TodoList;
以及Todo.js
:
import React, { Component } from 'react';
class Todo extends Component {
render() {
const { todo } = this.props;
return (
<div>
{todo.text}
{todo.completed ? <input type='checkbox' checked />
: <input type='checkbox' />}
</div>
);
}
}
export default Todo;
記住一定要
connect
生成的組件一定要在<Provider/>
下纔能有效,因爲connect
要通過context
獲取store
,而store
放在context
的過程是有<Provider/>
,前面已經講過了.
import { Provider } from 'react-redux';
import store from './store/store';
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>, document.getElementById('root'));
最後的效果: