React 只是 DOM 的一个抽象层,不涉及
代码结构
和组件间的通信
。所以无法用 React 写大型应用,为了解决这个问题,引出了 Redux 架构。
有可能不需要 Redux
不需要使用 Redux 的情况:
- 用户的使用方式非常简单
- 用户之间没有协作
- 不需要与服务器大量交互,也没有使用
WebSocket
- 视图层(View)只从单一来源获取数据
Redux 的适用场景:多交互、多数据源
- 用户的使用方式复杂
- 不同身份的用户有不同的使用方式(比如普通用户和管理员)
- 多个用户之间可以协作
- 与服务器大量交互,或者使用了
WebSocket
View
要从多个来源获取数据
从组件角度,使用 Redux 的场景:
- 某个组件的状态,需要共享
- 某个状态需要在任何地方都可以拿到
- 一个组件需要改变全局状态
- 一个组件需要改变另一个组件的状态
Redux 设计思想
1、Web 应用是一个状态机,视图与状态一一对应。
2、所有状态都保存在一个对象里面。
基本概念与 API
1、Store
保存数据的容器,整个应用只能有一个 Store。Redux 中使用 createStore
函数来生成 Store。
import { createStore } from 'redux'
// createStore 函数接受另一个函数作为参数,返回新生成的 Store 对象
const store = createStore(fn)
2、State
Store 对象包含所有数据,State 是 Store 中某个时点的数据。当前的 State 可以通过 store.getState()
得到。
import { createStore } from 'redux'
const store = createStore(fn)
const state = store.getState()
Redux 规定,一个 State 对应一个 View。
3、Action
State 的变化,会导致 View 的变化。但是,用户接触不到 State,只能接触到 View。所以,State 的变化必须是 View 导致的。Action 就是 View 发出的通知,表示 State 应该要发生变化了。
Action 是一个对象。其中的type属性是必须的,表示 Action 的名称。其他属性可以自由设置。
const action = {
type: 'ADD_TODO',
payload: 'Learn Redux'
}
Action 描述当前发生的事情,使用 Action 运送数据到 Store,可以改变 State
4、Action Creator
用来生成 Action 的函数。因为 View 要发送多少种消息就需要多少种 Action,手写会很麻烦。
const ADD_TODO = '添加 TODO'
// addTodo函数就是一个 Action Creator
function addTodo(text) {
return {
type: ADD_TODO,
text
}
}
const action = addTodo('Learn Redux')
5、store.dispatch()
store.dispatch()
是 View 发出 Action 的唯一方法。
import { createStore } from 'redux'
const store = createStore(fn)
// store.dispatch 接受一个 Action 对象作为参数,将它发送出去
store.dispatch({
type: 'ADD_TODO',
payload: 'Learn Redux'
})
结合 Action Creator
store.dispatch(addTodo('Learn Redux'))
6、Reducer
Store 收到 Action 以后,生成新的 State 的过程就叫 Reducer。Reducer 是一个函数,它接受 Action 和当前 State 作为参数,返回一个新的 State。
const reducer = function (state, action) {
// ...
return new_state
}
可以将整个应用的初始状态,作为 State 的默认值。
const defaultState = 0;
const reducer = (state = defaultState, action) => {
switch (action.type) {
case 'ADD':
return state + action.payload
default:
return state
}
};
const state = reducer(1, {
type: 'ADD',
payload: 2
})
在生成 Store 的时候,可以将 Reducer 传入 createStore
方法中,触发 Reducer 的自动执行。就可以不用手动调用。
import { createStore } from 'redux'
const store = createStore(reducer)
Reducer 函数可以作为参数传入 reduce
方法中。
// 将 Action 对象按照顺序作为一个数组
const actions = [
{ type: 'ADD', payload: 0 },
{ type: 'ADD', payload: 1 },
{ type: 'ADD', payload: 2 }
];
const total = actions.reduce(reducer, 0); // 3
7、纯函数
Reducer
函数是一个纯函数,只要是同样的输入,一定得到同样的输出。这样可以保证同样的 State ,必定得到同样的 View。
Reducer函数作为纯函数必须遵守的约束:
- 不得改写参数
- 不能调用系统 I/O 的API
- 不能调用
Date.now()
或者Math.random()
等不纯的方法,因为每次会得到不一样的结果
Reducer 函数里面不能改变 State,必须返回一个全新的对象。
// State 是一个对象
function reducer(state, action) {
return Object.assign({}, state, { thingToChange })
// 或者
return { ...state, ...newState }
}
// State 是一个数组
function reducer(state, action) {
return [...state, newItem]
}
8、store.subscribe()
Store 允许使用 store.subscribe
方法设置监听函数,一旦 State 发生变化,就自动执行这个函数。
import { createStore } from 'redux'
const store = createStore(reducer)
store.subscribe(listener)
只要把 View 的更新函数(对于 React 项目,就是组件的 render
方法或 setState
方法)放入 listen,就会实现 View 的自动渲染。
想要解除这个监听,只需要调用 store.subscribe 方法返回的一个函数。
let unsubscribe = store.subscribe(() =>
console.log(store.getState())
)
unsubscribe()
Store 的实现
Store 提供了三个方法:
store.getState()
得到 State 的方法store.dispatch()
View 发出 Action 的方法store.subscribe()
State 的监听函数
createStore
方法还可以接受第二个参数,表示 State 的最初状态。这通常是服务器给出的。
// window.STATE_FROM_SERVER就是整个应用的状态初始值
// 它会覆盖 Reducer 函数的默认初始值
let store = createStore(todoApp, window.STATE_FROM_SERVER)
createStore
方法的简单实现
const createStore = (reducer) => {
let state
let listeners = []
const getState = () => state
const dispatch = (action) => {
state = reducer(state, action)
listeners.forEach(listener => listener())
};
const subscribe = (listener) => {
listeners.push(listener)
return () => {
listeners = listeners.filter(l => l !== listener)
}
};
dispatch({})
return { getState, dispatch, subscribe }
}
Reducer 的拆分
Reducer 函数负责生成 State。由于整个应用只有一个 State 对象,包含所有数据,对于大型应用来说,这个 State 必然十分庞大,导致 Reducer 函数也十分庞大。
const chatReducer = (state = defaultState, action = {}) => {
const { type, payload } = action
switch (type) {
case ADD_CHAT:
return Object.assign({}, state, {
chatLog: state.chatLog.concat(payload)
})
case CHANGE_STATUS:
return Object.assign({}, state, {
statusMessage: payload
})
case CHANGE_USERNAME:
return Object.assign({}, state, {
userName: payload
})
default: return state
}
}
上面代码中,三种 Action 分别改变 State 的三个属性:
- ADD_CHAT:chatLog属性
- CHANGE_STATUS:statusMessage属性
- CHANGE_USERNAME:userName属性
这三个属性之间没有联系,这提示我们可以把 Reducer 函数拆分。不同的函数负责处理不同属性,最终把它们合并成一个大的 Reducer 即可。一个 React 根组件由很多子组件构成,这样拆分子组件与子 Reducer 完全可以对应。
const chatReducer = (state = defaultState, action = {}) => {
return {
// Reducer 函数被拆成了三个小函数,每一个负责生成对应的属性
chatLog: chatLog(state.chatLog, action),
statusMessage: statusMessage(state.statusMessage, action),
userName: userName(state.userName, action)
}
}
Redux 提供了一个 combineReducers
方法,用于 Reducer 的拆分。只要定义各个子 Reducer 函数,然后用这个方法,将它们合成一个大的 Reducer。
import { combineReducers } from 'redux'
// State 的属性名必须与子 Reducer 同名
const chatReducer = combineReducers({
chatLog,
statusMessage,
userName
})
export default todoApp
当 State 的属性名与子 Reducer 不同名,参考以下写法:
const reducer = combineReducers({
a: doSomethingWithA,
b: processB,
c: c
})
// 等同于
function reducer(state = {}, action) {
return {
a: doSomethingWithA(state.a, action),
b: processB(state.b, action),
c: c(state.c, action)
}
}
combineReducer
的简单实现:
// 该函数根据 State 的 key 去执行相应的子 Reducer
// 并将返回结果合并成一个大的 State 对象
const combineReducers = reducers => {
return (state = {}, action) => {
return Object.keys(reducers).reduce(
(nextState, key) => {
nextState[key] = reducers[key](state[key], action);
return nextState
},
{}
)
}
}
使用时将子 Reducer 放在一个文件里面,然后统一引入
import { combineReducers } from 'redux'
import * as reducers from './reducers'
const reducer = combineReducers(reducers)
工作流程
1、用户发出 Action
store.dispatch(action)
2、Store 自动调用 Reducer,并且传入两个参数:当前 State 和收到的 Action。 Reducer 会返回新的 State 。
let nextState = todoApp(previousState, action)
3、State 一旦有变化,Store 就会调用监听函数。
// 设置监听函数
store.subscribe(listener)
listener 可以通过 store.getState()
得到当前状态。如果使用的是 React,这时可以触发重新渲染 View。
function listerner() {
let newState = store.getState()
component.setState(newState)
}
实例:计数器
const Counter = ({ value, onIncrement, onDecrement }) => (
<div>
<h1>{value}</h1>
<button onClick={onIncrement}>+</button>
<button onClick={onDecrement}>-</button>
</div>
)
const reducer = (state = 0, action) => {
switch (action.type) {
case 'INCREMENT': return state + 1
case 'DECREMENT': return state - 1
default: return state;
}
}
const store = createStore(reducer);
const render = () => {
ReactDOM.render(
// 为Counter添加递增和递减的 Action
<Counter
// 每次 State 的变化都会导致网页重新渲染
value={store.getState()}
onIncrement={() => store.dispatch({type: 'INCREMENT'})}
onDecrement={() => store.dispatch({type: 'DECREMENT'})}
/>,
document.getElementById('root')
)
}
render()
// Store 的监听函数设置为render
store.subscribe(render)