day-07
Redux 基础
今日目标
✔ 掌握 Redux 核心概念和基本使用。
✔ 掌握 react-redux 的使用。
✔ 掌握 TODOLIST 的实现思路。
了解 Redux
目标
能够说出为什么需要使用 Redux。
内容
- 概念
Redux 是一个全局状态管理的 JS 库。
-
背景
a,React 的定位只是一个用来构建用户界面的库,并不是 Web 应用的完整解决方案。因此 React 在涉及到复杂组件之间的通信时会比较棘手,而对于大型项目来说,这方面恰恰是最关键的,因此只用 React 写大型项目会比较吃力。
b,2014 年 Facebook 提出了 Flux 架构的概念,引发了很多的实现。
c,2015 年,Redux 出现,将 Flux 与函数式编程(Reducer)结合一起,很短时间内就成为了最热门的前端状态管理库,类似的还有 Mobx、Saga 等状态管理工具。
为什么要用 Redux
-
不使用 Redux(图左边)
a,只能使用父子组件通讯、状态提升等 React 自带机制,理远房亲戚(非父子)关系的组件通讯时乏力。
b,组件之间的数据流混乱,出现 Bug 时难定位。
-
使用 Redux(图右边)
a,集中式存储和管理应用的状态,处理组件通讯问题时,无视组件之间的层级关系。
b,简化复杂应用中的数据传递问题,数据流清晰,易于定位 Bug。
小结
Redux 解决了什么问题?
React跨组件的通信问题。
Redux 安装
目标
能够在 React 项目中准备 Redux 开发环境。
步骤
- 创建 React 项目。
npx create-react-app redux-basic
- 启动项目
yarn start
- 安装 Redux 包
yarn add redux
Redux 核心概念
目标
能够了解 Redux 三个核心概念的职责。
内容
-
为了让代码各部分职责清晰、明确,Redux 提出三个核心概念,需要我们写代码的时候遵守。
a,action(动作):描述要做的事情(要干啥)。
b,reducer(函数):更新状态(怎么干)。
c,store(仓库):整合 action 和 reducer(谁来指挥)。
-
通过例子来理解三个核心概念。
a,action:相当于公司里面要做的事情,比如打扫卫生这个事等。
b,reducer:相当于公司的员工,负责执行。
c,store:相当于公司的老板。
d,流程:老板(store)分配(dispatch)要做的事情(action)给员工(reducer),员工干完活把结果交给老板。
e,在视图当中,通过 store dispatch 一个 action,reducer 会自动收到通知来更新 state,state 一旦变化,说有使用 state 的视图自然就变了。
Redux 核心概念 action
目标
定义一个最基本的 action。
内容
-
action 用来描述要做的事情,项目中的每一个功能都是一个 action,比如:
a,计数器案例:计数器加 1、减 1。
b,todomvc 案例:添加任务、删除任务等。
c,项目:登录,退出等。
-
特点:
a,只描述做什么。
b,本质上就是一个 JS 对象,必须带有
type
属性,用于区分动作的类型。c,可以通过 payload 携带额外的数据。
{ type: 'increment' }
// payload: 参数
{ type: 'decrement', payload: 2 }
{ type: 'addTodo', payload: '吃饭' }
{ type: 'addTodo', payload: '睡觉' }
{ type: 'removeTodo', payload: 3 }
store/actions.js
export const incremen = {
type: 'INCREMENT',
payload: 5,
}
export const decrement = {
type: 'DECREMENT',
payload: 5,
}
Redux 核心概念 action creator
目标
学会使用函数(actionCreator)去创建一个 action。
内容
- 问题:直接使用对象来创建 action 不灵活,参数写死了。
- 解决:可以使用函数来创建 action,通过传参把不一样的数据传递过去就好了,我们把这个创建 action 的函数叫做 actionCreator。
- 返回值:返回一个 action 对象。
- 好处:代码更加简洁,容易复用。
核心代码
store/actions.js
export const increment = (payload) => ({
type: 'INCREMENT',
payload,
})
export const decrement = (payload) => ({
type: 'DECREMENT',
payload,
})
小结
actionCreator 的作用和返回值是什么?
答:actionCreator作用是创建action,可以传参;返回值是action对象。
Redux 核心概念 reducer
目标
能够掌握 reducer 的基本写法。
内容
reducer:本质上是一个函数,作用是根据 action 来更新状态,有如下特点。
- 函数签名为:
(prevState, action) => newState
。 - 接收上一次的状态和 action,根据 action 的类型执行对应的操作,最终返回新的状态。
- 原则:不要在 reducer 函数内部直接修改 state。
store/reducers.js
export default function counter(state = 10, action) {
// 处理各种各样的 action
switch (action.type) {
case 'INCREMENT':
return state + action.payload
case 'DECREMENT':
return state - action.payload
default:
// 记得要有默认返回的处理
return state
}
}
Redux 核心概念 纯函数
目标
了解纯函数的特点。
内容
-
纯函数是函数式编程中的概念,对于纯函数来说有一个很重要的特点:相同的输入总是得到相同的输出。
-
纯函数常具有以下特点。
a,不得改写参数,不能使用全局变量。
b,不能调用 Date.now() 或者 Math.random() 等不纯的方法,因为每次会得到不一样的结果。
c,不包含副作用的处理,副作用:AJAX 请求、操作本地数据、或者操作函数外部的变量等。
-
好处:代码简洁、方便测试、方便性能优化。
-
为什么说纯函数呢?因为 reducer 要求自身就必须是一个纯函数。
const add = (a, b) => a + b
add(1, 2)
const add = (a, b) => a + b + Math.random()
add(1, 2)
add(1, 2)
const arr = [1, 2, 3, 4, 5]
arr.slice(1, 2)
arr.slice(1, 2)
arr.slice(1, 2)
const arr = [1, 2, 3, 4, 5]
arr.splice(1, 2)
arr.splice(1, 2)
Redux 核心概念 store
目标
掌握 store 的创建和基本使用。
内容
-
store:仓库,是 Redux 的核心,负责整合 action 和 reducer。
-
特点
a,一个应用只有一个 store。
b,创建:
const store = createStore(reducer)
。c,获取数据:
store.getState()
。d,更新数据:
store.dispatch(action)
。 -
其他 API
a,订阅(监听)状态变化:
const unSubscribe = store.subscribe(() => {})
,注意要先订阅,后续的更新才能被观测到。b,取消订阅状态变化:
unSubscribe()
。
store/index.js
,注意演示上面 API 需要先在 index.js
中引入此文件。
// store: 整个数据的仓库,负责关联 reducer 和 action,通过 store 对象可以给 reducer 分配 action
import { createStore } from 'redux'
import reducer from './reducers'
const store = createStore(reducer)
export default store
小结
通过哪个 API 来创建 Store?创建 Store 需要什么参数?
答:通过createStore来创建Store,创建Store需要reducer。
点击计数 📝
目标
掌握在视图中使用和更新数据。
读取数据
App.js
import React from 'react'
import store from './store'
export default function App() {
return (
<div>
<h3>count: {store.getState()}</h3>
</div>
)
}
更改数据
import React from 'react'
import store from './store'
import { increment, decrement } from './store/actions'
export default function App() {
return (
<div>
<p>count: {store.getState()}</p>
<div>
<button onClick={() => store.dispatch(increment(1))}>+1</button>
<button onClick={() => store.dispatch(increment(5))}>+5</button>
<button onClick={() => store.dispatch(decrement(1))}>-1</button>
<button onClick={() => store.dispatch(decrement(5))}>-5</button>
</div>
</div>
)
}
解决问题
import ReactDOM from 'react-dom'
import App from './App'
import store from './store'
ReactDOM.render(<App />, document.querySelector('#root'))
store.subscribe(() => {
ReactDOM.render(<App />, document.querySelector('#root'))
})
小结
store 中数据的变化,通过哪个方法可以被观测到?如何让视图更新?
答:store.subscribe()
方法可以观测到store中数据的变化,重新渲染数据。
Redux 执行过程
目标
了解 Redux 的执行过程。
获取默认值的执行过程
-
只要创建 store,Redux 内部就会调用一次 reducer,打印试一下
console.log(action.type)
。 -
类似:
reducer(undefined, {type: "@@redux/INITv.a.4.t.t.p"})
。 -
这一次调用 reducer 的目的:获取状态的默认值。
-
因为传入的状态值是 undefined ,并且是一个随机的 action type,所以!
a,状态值因为 undefined,所以,我们设置的默认值就会生效,比如,此处的:10。
b,因为是一个随机的 action type,所以,reducer 中 switch 一定无法命中,那就一定会走 default,也就是直接返回了状态的默认值,也就是:10。
-
Redux 内部拿到这个数据(比如此处的 10)以后,就用这个数据作为了 store 中的最新状态值。
-
因此,将来当我们调用
store.getState()
方法来获取 Redux 状态值的时候,拿到的就是 10 了。
import { createStore } from 'redux'
import reducer from './reducer'
// 只要创建 store 传递了 reducer,Redux 内部就会自动的 dispatch 一次 action
// 目的:就是为了 store 能够有初始值,store.dispatch({ type: '@@xx699' })
const store = createStore(reducer)
// 所以可以拿到初始值
store.getState()
export default store
点击按钮后的执行过程
- 点击按钮,派发动作
store.dispatch(action)
。 - 只要触发了 action,Redux 内部就会自动调用 reducer,根据上一次的状态和 action 计算出新的状态并返回。
- reducer 执行完毕后,将最新的状态交给 store,store 用最新的状态替换旧状态,状态更新完毕。
小结
点击按钮视图更新的流程是什么?
答:1.点击按钮,派发动作store.dispatch(action)
2.只要触发了action,Redux内部就会自动调用reducer,根据上一次的状态和action计算出新的状态并返回。
3.reducer执行完毕后,将最新的状态交给store,store用最新的状态替换
react-redux
基本介绍
- 问题:为什么要使用 react-redux 这个库?
- 回答:React 和 Redux 是两个独立的库,两者之间职责独立,因此,为了更好的实现在 React 中使用 Redux 进行状态管理,就需要一种机制,将这两个独立的库关联在一起,这就是 react-redux 出现的原因。
- react-redux 是 Redux 官方提供的 React 绑定库。
基本使用
- 安装
yarn add react-redux
- 配置
index.js
import ReactDOM from 'react-dom'
import App from './App.js'
import store from './store/store.js'
// 1.导入包
import { Provider } from 'react-redux'
// 2.通过 Provider 包裹根组件并提供 store 供其他组件内部使用
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.querySelector('#root')
)
// 用了 react-redux 下面手动触发更新的方式就没用了
/* store.subscribe(() => {
ReactDOM.render(<App />, document.querySelector('#root'))
}) */
✍ 一旦使用了 react-redux,获取和更新数据的方式就变化了,要按照这个库的要求来。
useSelector
react-redux
这个库提供了 useSelector,作用:从 store 中获取状态。- selector 函数应该是个纯函数。
App.js
import React from 'react'
import { useSelector } from 'react-redux'
const App = () => {
//3.获取store中的数据,接收函数,函数参数代表所有的状态,返回值表示需要返回的状态
const count = useSelector((state) => state)
return (
<div>
<h3>{count}</h3>
</div>
)
}
export default App
useDispatch
作用:得到 dispatch 来触发 action(触发 action 会执行 reducer,reducer 负责数据的修改,react-redux 内部会监听数据的变化自动进行视图更新)。
App.js
import React from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { increment, decrement } from './store/actions'
const App = () => {
const count = useSelector((state) => state)
//4.使用useDispatch hook生成dispatch
const dispatch = useDispatch()
return (
<div>
<h3>{count}</h3>
<div>
<button onClick={() => dispatch(increment(1))}>+1</button>
<button onClick={() => dispatch(increment(5))}>+5</button>
<button onClick={() => dispatch(decrement(1))}>-1</button>
<button onClick={() => dispatch(decrement(5))}>-5</button>
</div>
</div>
)
}
export default App
如果 Test 组件想用,看看有多方便吧,无需传值,直接拽过来!
import React from 'react'
import { useSelector } from 'react-redux'
export default function Test() {
const money = useSelector((state) => state)
return <div>{money}</div>
}
Reducer 的分离与合并
概述
-
随着项目功能变得越来越复杂,需要 Redux 管理的状态也会越来越多,此时,有两种方式来处理状态的更新。
a,使用一个 reducer,处理项目中所有状态。
b,使用多个 reducer,按照项目功能划分,每一个 reducer 处理该功能的状态。
-
推荐:使用第二种方案,每个 reducer 处理单一功能的状态,职责更明确。
-
问题:此时项目中会有多个 reducer,但是 store 只能接收一个 reducer,因此需要将多个 reducer 合并为一个 reducer,才能传递给 store 使用。
-
解决:使用 Redux 中的
combineReducers({ counter: counterReducer, user: userReducer })
函数。 -
注意:组件中再想只使用 counter 的状态,需要
const count = useSelector((state) => state.counter)
-
每个 reducer 应该只关注自己的数据,例如:
a,登录功能:
loginReducer
处理的只应该是跟登录相关的状态。b,个人资料:
profileReducer
处理的只应该是跟个人资料相关的状态。c,文章列表、文章详情、文章评论等。
步骤
reducers.js
中新建 userReducer。- 通过 combineReducers 合并 counter 和 user 并导出。
- 修改
App.js
和Test.js
获取数据的方式。 - 新建
User.js
测试 userReducer 的使用。
代码
reducers.js
import { combineReducers } from 'redux'
function counter(state = 10, action) {
switch (action.type) {
case 'INCREMENT':
return state + action.payload
case 'DECREMENT':
return state - action.payload
default:
return state
}
}
function user(state = { name: 'ifer', age: 18 }, action) {
switch (action.type) {
case 'UPDATENAME':
return {
...state,
name: action.payload,
}
default:
return state
}
}
export default combineReducers({
counter,
user,
})
App.js
import React from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { increment, decrement } from './store/actions'
import Test from './Test'
import User from './User'
const App = () => {
const count = useSelector((state) => state.counter)
const dispatch = useDispatch()
return (
<div>
<h3>{count}</h3>
<div>
<button onClick={() => dispatch(increment(1))}>+1</button>
<button onClick={() => dispatch(increment(5))}>+5</button>
<button onClick={() => dispatch(decrement(1))}>-1</button>
<button onClick={() => dispatch(decrement(5))}>-5</button>
</div>
<Test />
<User />
</div>
)
}
export default App
Test.js
import React from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { increment } from './store/actions'
export default function Test() {
const count = useSelector((state) => state.counter)
const dispatch = useDispatch()
return (
<div>
<p>Test {count}</p>
<div>
<button onClick={() => dispatch(increment(5))}>click</button>
</div>
</div>
)
}
User.js
import React from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { updateName } from './store/actions'
export default function User() {
const user = useSelector((state) => state.user)
const dispatch = useDispatch()
const handleClick = () => {
dispatch(updateName('xxx'))
}
return (
<div>
<h3>{user.name}</h3>
<button onClick={handleClick}>update name</button>
</div>
)
}
actions.js
export const updateName = (payload) => ({
type: 'UPDATENAME',
payload,
})
ActionTypes
概述
- 是什么:action 对象中的 type 属性。
- Redux 项目中,同一个 type 会在不同文件中多次被用到,比如 actions.js、reducers.js 等。
- 目标:集中处理 action type,保持一致性,容易维护!
操作
- 在 store 目录中创建
actionTypes.js
或constants.js
文件。 - 使用常量创建 ActionType 并导出。
- 命名推荐:
模块_动作
,比如:- 计数器:
COUNTER_INCREMENT
表示计数器模块中的 INCREMENT 动作。 - TodoList:
TODOLIST_ADD
表示 TodoList 案例中 ADD 动作。 - 登录:
LOGIN_GETCODE
表示登录模块中获取验证码的动作,LOGIN_SUBMIT
表示登录模块中的提交功能。 - 个人信息:
PROFILE_GETINFO
表示个人资料模块中的获取信息动作;PROFILE_UPDATEINFO
等。
- 计数器:
- 哪里需要用到就按需导入。
代码
actionTypes.js
export const COUNTER_INCREMENT = 'COUNTER_INCREMENT'
export const COUNTER_DECREMENT = 'COUNTER_DECREMENT'
export const USER_UPDATENAME = 'USER_UPDATENAME'
actions.js
import { COUNTER_INCREMENT, COUNTER_DECREMENT, USER_UPDATENAME } from './actionTypes'
export const increment = (payload) => ({
type: COUNTER_INCREMENT,
payload,
})
export const decrement = (payload) => ({
type: COUNTER_DECREMENT,
payload,
})
export const updateName = (payload) => ({
type: USER_UPDATENAME,
payload,
})
reducers.js
import { combineReducers } from 'redux'
import { COUNTER_INCREMENT, COUNTER_DECREMENT, USER_UPDATENAME } from './actionTypes'
function counter(state = 10, action) {
switch (action.type) {
case COUNTER_INCREMENT:
return state + action.payload
case COUNTER_DECREMENT:
return state - action.payload
default:
return state
}
}
function user(state = { name: 'ifer', age: 18 }, action) {
switch (action.type) {
case USER_UPDATENAME:
return {
...state,
name: action.payload,
}
default:
return state
}
}
export default combineReducers({
counter,
user,
})
在项目中配置和使用Redux完整流程
1.先准备reducer
export default function counter(state = 10, action) {
switch (action.type) {
case 'INCREMENT':
return state + action.payload
case 'DECREMENT':
return state - action.payload
default:
return state
}
}
2.通过combineReducers
合并reducer得到rootReducer
import { combineReducers } from 'redux'
import counter from './counter'
const rootReducer = combineReducers({ counter })
export default rootReducer
3.根据rootReducer
创建store
export default createStore(rootReducer)
4.在入口文件配置react-redux
并传递store
import { Provider } from 'react-redux'
import store from './store'
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.querySelector('#root')
)
5.获取状态
import { useSelector } from 'react-redux'
const App = () => {
const counter = useSelector((state) => state.counter)
return (
<div>
<h3>{counter}</h3>
</div>
)
}
export default App
6.修改状态
import { useDispatch } from 'react-redux'
import { increment } from './store/actions'
const App = () => {
const dispatch = useDispatch()
return (
<div>
<button onClick={() => dispatch(increment(1))}>+1</button>
</div>
)
}
export default App
中间件概述
目标
能够理解为什么需要 Redux 中间件。
内容
默认情况下,Redux 自身只能处理同步数据流,但是在实际项目开发中,状态的更新、获取,通常是使用异步操作来实现。
- 问题:如何在 Redux 中进行异步操作呢?
- 回答:通过 Redux 中间件机制来实现。
- 中间件,可以理解为处理一个功能的中间环节,对于 Redux 中间件来说就是在数据到达 reducer 之前进行一系列的处理操作。
触发时机
-
Redux 中间件执行时机:在 dispatching action 和 到达 reducer 之间。
a,没有中间件:
dispatch(action) => reducer
。b,使用中间件:
dispatch(action) => 执行中间件代码 => reducer
。 -
原理:封装了 Redux 的 dispatch 方法。
a,没有中间件:
store.dispatch()
使用的是 Redux 库自己提供的 dispatch 方法,用来发起状态更新。b,使用中间件:
store.dispatch()
使用的是中间件封装处理后的 dispatch,但是最终还是会调用 Redux 库自己提供的 dispatch 方法。
没有中间件
有中间件
logger 中间件
- 安装:
yarn add redux-logger
。 - 导入 redux-logger。
- 从 redux 中导入 applyMiddleware 函数。
- 将 applyMiddleware() 调用作为 createStore 函数的第二个参数。
- 调用 applyMiddleware 函数时,将 logger 作为参数传入。
- 后续调用 store.dispatch() 时,控制台就会有日志信息输出。
store/index.js
import { createStore, applyMiddleware } from 'redux'
import logger from 'redux-logger'
import rootReducer from './reducers'
export default createStore(rootReducer, applyMiddleware(logger))
redux-thunk 使用
redux-thunk
中间件可以处理函数形式的 action,而在函数形式的 action 中就可以执行异步操作代码,完成异步操作。
- 安装:
yarn add redux-thunk
。 - 导入 redux-thunk。
- 将 thunk 添加到中间件列表中。
- 修改 action creator,返回一个函数。
- 在函数形式的 action 中执行异步操作,在异步操作成功后,分发 action 更新状态。
store/index.js
import thunk from 'redux-thunk'
import logger from 'redux-logger'
import rootReducer from './reducers'
export default createStore(rootReducer, applyMiddleware(thunk, logger))
store/actions/todo.js
export const clearTodo = () => {
return (dispatch) => {
setTimeout(() => {
dispatch({
type: CLEAR_TODO,
})
}, 1000)
}
}
redux-devtools-extension
目标
开发 React 项目时,能够通过 Chrome 开发者工具调试跟踪 Redux 状态。
步骤
- 保证浏览器安装了 Redux 的开发者工具。
- 通过包管理器在项目中安装
yarn add redux-devtools-extension
。 - 在 store/index.js 中进行配置。
- 启动 react 项目,打开 chrome 开发者工具,测试
import { createStore, applyMiddleware } from 'redux'
import reducer from './reducers'
import thunk from 'redux-thunk'
import { composeWithDevTools } from 'redux-devtools-extension'
export default createStore(reducer, composeWithDevTools(applyMiddleware(thunk)))