React 是构建 UI 的库,只是 DOM 的一个抽象层,并不是 Web 应用的完整解决方案。
有两个方面,它没涉及
1 代码结构
2 组件之间的通信
对于大型的复杂应用来说,这两方面恰恰是最关键的。因此,只用 React 没法写大型应用。
Redux 是 JavaScript 应用的可预测状态容器,用来集中管理状态。(A predictable state container for Javascript apps)
特点:集中管理、可预测、易于测试、易于调试、强大的中间件机制满足你所有需求。
注意
:redux 是一个独立于 react 的库,可以配合任何 UI 库/框架来使用。
注意
:如果你还不知道是否需要使用 Redux,那就是不需要它。
yarn add redux
action
动作,用来描述要执行的动作
reducer
接收到 action ,并且来执行这个动作
store
是 action 和 reducer 的桥梁,将 action 传递给 reducer ,由 reducer 来完成整个动作
整个过程: 管理者(store) 将 专家提出来的想法(action) 传递给 搬砖的人(reducer),最终,由reducer完成了这个任务(动作)。
action 用来描述要执行的动作(功能)
action 实际上就是普通的JS对象
约定1
:必须提供 type
属性,type 属性用来描述当前动作的类型
约定2
:type 属性的值是一个字符串,任务类型使用纯大写字母来表示,多个单词之间使用 _ 分割
约定3
:可以携带完成该动作需要的其他数据,这些数据(属性名)是可以任意名称的
// action
// 添加任务的动作
{ type : 'ADD_TODO' }
// 因为为了完成添加任务的动作,需要一个任务名称,所以,动作中可以携带完成该动作需要的数据
{ type: 'ADD_TODO', name: '吃饭' }
// 删除任务的动作
{ type: 'DELETE_TODO', id: 3 }
reducer 使用来完成 action 的
reducer 实际上就是一个普通的JS函数
该函数能够接受两个参数: (state, action ) => newState
作用:根据 旧状态 和 action(动作) ,来计算出新的状态
注意
:reducer 一定要有返回值!
注意
:在reducer 中不要直接修改 state,应该创建新的 state (状态不可变原则)
注意
:reducer 应该是一个纯函数(同样的输入,必定得到同样的输出),不要有修改参数、调用 Math.random() 等不纯的操作。
// action => { type: 'ADD_TODO', name: '吃饭' }
// 假设 state => [{}, {}]
const reducer = (state, action) => {
switch(action.type) {
case 'ADD_TODO':
// 1 执行 添加任务 的逻辑代码
// state.push() 不要这么做!!!
// 2 返回 添加任务 后的新状态
return [...state, { name: action.name }]
case 'DELETE_TODO':
// 1 执行 删除任务 的逻辑代码
// 2 返回 删除任务 后的新状态
return state.filter(item => item.id !== action.id)
default:
// 如果当前 reducer 遇到无法处理的 action,就会执行 default
// 此时,应该直接返回 state
return state
}
}
注意
:一个 redux 应用中只有一个 store (仓库)
作用:将 action 和 reducer 组合在一起
职责:
import { createStore } from 'redux'
// 将 reducer 作为参数传递给 store,那么,store 中就可以拿到 reducer
const store = createStore( reducer )
// 通过 store 中的 dispatch 分发任务
// dispatch 的参数:就是 action (动作)
store.dispatch( { type: 'ADD_TODO', name: '睡觉' } )
// 获取状态:
const state = store.getState()
// 这个返回值,用来取消本次订阅,取消后,状态变化时,就不会再执行该回调函数了
const unsubscribe = store.subscribe(() => {
console.log('当前状态为:', store.getState())
})
// 取消订阅
unsubscribe()
// 模拟 redux 内部初始化操作:
reducer(undefined, { type: '@@redux/INITx.n.f.s.j.5' })
function reducer(state = 0, action) {
// state 此时为默认值: 0 (因为第一次 redux 传入的值为 undefined)
// action ==> { type: '@@redux/INITx.n.f.s.j.5' }
switch (action.type) {
case 'ADD':
return state + 1
default:
// 因为该 reducer 无法处理 redux 生成的随机 动作类型
// 所以,执行了此处的 default,也就是返回了 默认状态:0
return state
}
}
// 最终调用 store.getState() 得到了初始状态:0
触发动作:store.dispatch(action),dispatch 方法将 action 和 state 传递给 reducer。
实现动作:reducer 根据接收到的 action 和 state,得到最新的 state,并返回。
更新 state:store 接收到 reducer 返回的新 state,更新 store 中的 state。
如果需要将 react 和 redux 配合到一起使用,就需要借助于这个 绑定库 react-redux
yarn add react-redux
Provider 组件
:
<Provider store={ store }>
<App />
</Provider>
connect 函数
:注意:仅仅使用 Provider 组件包裹整个应用,组件是无法直接获取到 redux 中的state 和操作state的方法的!!!
所以,如果要在组件中获取到 redux 中的state和操作state的方法,就使用 connect 函数来包裹组件,那么,组件中就可以获取到 redux 中的内容了
// 第一次调用:可以传入一些配置
// 第二次调用:用来包装想要获取到 redux 状态的组件
connect()(Counter)
// 第一个函数:用来为组件提供状态
// 第二个函数:用来为组件提供操作状态的方法
connect(mapStateToProps, mapDispatchToProps)(Counter)
添加 mapStateToProps 和 mapDispatchToProps 后的使用
// 通过 count 来存储状态
const mapStateToProps = state => {
return {
count: state
}
}
// 在原本使用 dispatch 的地方,用 increment 来替换
const mapDispatchToProps = dispatch => {
return {
increment: dispatch
}
}
Counter = connect(
mapStateToProps,
mapDispatchToProps
)(Counter)
const increment = () => ({ type: 'INCREMENT' })
// 使用:
store.dispatch( increment() )
const addTodo = (name) => ({ type: 'ADD_TODO', name })
// 使用:
store.dispatch( addTodo('吃饭') )
// 相当于:
// store.dispatch( { type: 'ADD_TODO', name: '吃饭' } )
// store.dispatch( { type: 'ADD_TODO', name: '吃饭' } )
// actionTypes.js
const ADD_TODO = 'ADD_TODO'
const DELETE_TODO = 'DELETE_TODO'
export { ADD_TODO, DELETE_TODO }
// actions.js
import { ADD_TODO } from './actionTypes'
// 使用action creator:
const addTodo = (name) => ({ type: ADD_TODO, name })
// reducers.js
import { ADD_TODO } from './actionTypes'
const reducer = (state, action) => {
switch(action.type) {
case ADD_TODO:
...
}
}
注意每个 reducer 只负责管理全局 state 中它负责的一部分。
每个 reducer 的 state 参数都不同,分别对应它管理的那部分 state 数据。
随着应用的膨胀,我们还可以将拆分后的 reducer 放到不同的文件中, 以保持其独立性,并用于专门处理不同的数据域。
// reducers/todos.js
const todos = (state = [], action) => newState
// reducers/visibilityFilter.js
const visibilityFilter = (state = 'all', action) => newState
// reducers/loading.js
const loading = (state = false, action) => newState
// 此时的应用的状态为:{ todos: [], visibilityFilter: 'all', loading: false }
const rootReducer = combineReducers({
todos,
visibilityFilter,
loading
})
const store = createStore(rootReducer)
// 该写法等价于上述combineReducers调用:
function rootReducer(state = {}, action) {
return {
todos: todos(state.todos, action),
visibilityFilter: visibilityFilter(state.visibilityFilter, action)
}
}
combineReducer
函数来合并多个 子reducer,生成一个 根reducercreateStore(reducer, [preloadedState], enhancer) createStore说明
之前我们使用的时候只是使用了第一个参数 reducer
,我们来说一下第二个参数preloadedState
这个参数一般是用来设置 store 的默认值(初始时的 state)
就比如说:我们使用了 combineReducer
之后生成了一个 根reducer ,同样的会把这几个reducer的state合并为一个对象,这个对象就是 store 的state,我们这里可以使用第二个参数来个这个state设置初始值。
第三个属性,Store enhancer 是一个组合 store creator 的高阶函数,返回一个新的强化过的 store creator。这与 middleware 相似,它也允许你通过复合函数改变 store 接口。我们在之后的中间件中会讲到。
// 获取localStorage 中存储的数据
const initialState = JSON.parse(localStorage.getItem("todos")) || {}
// 把初始数据作为第二个参数传入 createStore
const store = createStore(reducer, initialState)
// 监听状态修改时,自动把数据写入localStorage中
store.subscribe(() => {
localStorage.setItem("todos", JSON.stringify(store.getState()))
})
react-router-dom
<Provider store={store}>
<Router>
<Switch>
<Route exact path="/" render={() => <Redirect to="/all" />} />
<Route exact path="/:filter?" component={App} />
</Switch>
</Router>
</Provider>
该路由规则 /:filter 能够匹配的路径为: /all 或 /active 或 /completed 、/123 不能匹配: / 或 /all/123
如何需要匹配 / ,可以在路由规则后面添加 ?,表示当前参数可选,
也就是: /:filter?,此时,该路由规则可以匹配:/ 或 /all 或 /active 或 /completed
// 第一个参数:表示 redux 中的状态
// 只要 redux 中的 state 变了,那么,这个函数就会被重新调用
// 第二个参数:表示 高阶组件connect() 接收到的属性
// 只要 高阶组件connect() 接收到新的属性,那么,这个函数也会别重新调用
const mapStateToProps = (state, ownProps) => ({})
什么时候我们会用到第二个参数呢?
比如:当我们需要获取一些参数,比如路由数据信息中的某些数据时,我们可以通过路由中的 withRouter
高阶组件来包裹一下 某个容器组件
const mapStateToProps = (state, ownProps) => {
console.log(ownProps)
}
...
const TodoListContainer = connect(
mapStateToProps
)(TodoList)
export default withRouter(TodoListContainer)
中间件,可以理解为处理一个功能的中间环节。
下图中,自来水从水库到用户家庭中的每一个环节都是一个中间件。
比如:Express 中间件,可以在接收到请求和响应这些请求之间,执行一些操作。
中间件的优势:可以串联,在一个功能中使用多个中间件。
Redux 中间件原理:创建一个函数,包装 store.dispatch,使用新创建的函数作为新的 dispatch。
注意:使用了 中间件 以后,我们通过代码调用 store.dispatch
就不是 redux 自身提供的 dispatch 了。而是由 中间件 提供的dispatch。每个 中间件 都在 dispatch 外面包了一层。最里面才是 redux 自身提供的 dispatch。
如果有多个中间件:
// 没有中间件:
// 注意:因为没有中间件所以,此处的 store.dispatch 就是 redux 自己提供的dispatch
store.dispatch( action ) --> reducer --> newState
// 有中间件
// 注意:有了中间件以后,store.dispatch 就不再是 redux 自己提供的 dispatch
store.dispatch( action ) --> 中间件1 -> 中间件2 --> reducer --> newState
yarn add redux-logger
yarn add redux-devtools-extension
import { composeWithDevTools } from 'redux-devtools-extension'
createStore(reducer, initialState, composeWithDevTools(applyMiddleWare(logger)) )
我们可以使用 createStore
的第三个参数 来使用中间件,比如 applyMiddleWare(logger)
这样我们就可以在控制台中看到我们的执行前后的数据状态
我们可以自己来写一个中间件,就以logger 为例
const logger = store => next => action => {
// 记录日志代码
console.log("1 dispatching", action)
// 如果只使用了一个中间件:
// 那么,next 就表示原始的 dispatch
// 也就是:logger中间件包装了 store.dispatch
let result = next(action)
// 记录日志代码
console.log("3 next state", store.getState())
return result
}
Chrome浏览器中有一个 插件,可以检测redux的数据状态,但是使用这个插件的时候,需要我们使用 redux-devtools-extension
这个库,并且使用 composeWithDevTools
函数来调用一下 上面的 applyMiddleWare ,代码如上面所示。
注意:使用 redux-logger 中间件的时候需要把 logger 放在最后一个,不然会导致 thunk
或 Promise
执行的不是实际的结果(Note: logger must be the last middleware in chain, otherwise it will log thunk and promise, not actual actions )
yarn add redux-thunk
import thunk from 'redux-thunk'
createStore(..., .., applyMiddleWare(thunk, logger))
const addTodoAsync = (text) => {
return dispatch => {
setTimeout(() => {
// 分发同步action
dispatch({ type: 'ADD_TODO', text })
}, 2000)
}
}
// 组件分发异步action:
// 注意:此处的 dispatch 是 thunk中间件 包装后的 dispatch
// 而 thunk 中间件能够处理 addTodoAsync 中返回的函数
props.dispatch( addTodoAsync('吃饭') )
同步操作只要发出一种 Action 即可,异步操作的差别是它要发出三种 Action。
比如:获取服务器数据为例
{type: 'GET_DATA_START'}
{type:'GET_DATA_SUCCESS', result: res}
{type:'GET_DATA_ERROR', error: message}
下面来用一个简单的代码来看一下
在 actions 中添加一个 异步的 action creator
// 异步获取数据
const getDataAsync = () => {
return async dispatch => {
dispatch({ type: "GET_DATA_START" })
try {
const res = await axios.get("http://localhost:8081/todos")
dispatch({ type: "GET_DATA_SUCCESS", todos: res.data })
} catch (e) {
dispatch({ type: "GET_DATA_ERROR", err: e })
}
}
}
在 reducer 中添加相应的处理逻辑,这里使用了 合并reducer 的方式,你也可以用一个reducer,但是state是一个对象
const todos = (state = [], action) => {
switch (action.type) {
case "GET_TODOS_START":
return state
case "GET_TODOS_SUCCESS":
return action.todos
case "GET_TODOS_ERROR":
return state
default:
return state
}
}
// 是否显示加载中
const loading = (state = false, action) => {
switch (action.type) {
case "GET_TODOS_START":
return true
case "GET_TODOS_SUCCESS":
case "GET_TODOS_ERROR":
return false
default:
return state
}
}
const error = (state = "", action) => {
switch (action.type) {
case "GET_TODOS_ERROR":
return "network error"
default:
return state
}
}
const rootReducer = combineReducers({
todos,
loading,
error
})
export default rootReducer
在容器组件中 把方法映射到组件 props 上
const mapStateToProps = state => {
console.log(state)
return {
data: state.data,
loading: state.loading,
error: state.error
}
}
const mapDispatchToProps = dispatch => {
return {
getDataAsync() {
return dispatch(getDataAsync())
}
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(List)
在展示组件中使用
class List extends Component {
componentDidMount() {
this.props.getDataAsync()
}
render() {
const { data, loading, error } = this.props
return (
<>
{loading ? <p>数据加载中...</p> : null}
{error ? <p>{error}</p> : null}
<ul>
{data.map(item => (
<li key={item.id}>
<span>{item.text}</span>
</li>
))}
</ul>
</>
)
}
}