携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第5天,点击查看活动详情
Redux基本介绍
目标
了解的Redux的基本内容;知道它在react技术栈中的位置。
redux
Redux 是 JavaScript 应用的状态容器,提供可预测的状态管理。
当遇到如下问题时,建议开始使用 Redux:
- 你有很多数据随时间而变化
- 你希望状态有一个唯一确定的来源
- 你发现将所有状态放在顶层组件中管理已不可维护
redux和react的关系
Redux并不只为react应用提供状态管理, 它还支持其它的框架。
为什么Rect要用 Redux
React 是 DOM 的一个抽象层(UI 库),并不是 Web 应用的完整解决方案。因此react在涉及到数据的处理以及组件之间的通信时会比较复杂。
对于大型的复杂应用来说,这两方面恰恰是最关键的。因此,只用 React,写大型应用比较吃力。
- 2014 年 Facebook 提出了 Flux 架构的概念,引发了很多的实现。
- 2015 年,Redux 出现,将 Flux 与函数式编程(reducer)结合一起,很短时间内就成为了最热门的前端架构。
- Flux 是最早的状态管理 工具,它提供了状态管理的思想,也提供对应的实现
- 除了 Flux、Redux 之外,还有:Mobx 等状态管理工具
-
主要的区别:组件之间的通讯问题
-
不使用 Redux (图左边) :
- 只能使用父子组件通讯、状态提升等 React 自带机制
- 处理远房亲戚(非父子)关系的组件通讯时乏力
- 组件之间的数据流混乱,出现 Bug 时难定位
-
使用 Redux (图右边):
- 集中式存储和管理应用的状态
- 处理组件通讯问题时,无视组件之间的层级关系
- 简化大型复杂应用中组件之间的通讯问题
- 数据流清晰,易于定位 Bug
小结
- redux是?
- 它的作用是?
- redux只能在react中使用么?
问题导入-react多组件共享
目标
分析问题,并搭建代码基本结构
问题描述
多组件(跨层级的)共享数据
目录结构
├── src
├── store # redux目录,一般约定叫store
│ ├── index.js # 定义并导出store. 其中会导入reducer
│ └── reducer # reducer函数
├── App.js # 根组件,引入Father 和 Uncle组件
├── Father.js # Father组件。其中引入了Son组件
├── index.js # 项目的入口文件,会导入并渲染App.js
├── Son.js # Son组件
└── Uncle.js # Unclde组件
使用redux来解决的思路
小结
- redux可以无视组件层级
- 对于组件系统来说,redux就是一个第三方的,全局的“变量”
Redux 三个核心概念
目标
能够理解redux三个核心概念
图示
理解三个核心概念
核心概念:store、action、reducer
action
-
动作。
-
一个js对象,包含两个属性:
- type: 标识属性,值是字符串。多个type用action分开
- payload:数据属性,可选。表示本次动作携带的数据
-
actions 只是描述了有事情发生了这一事实,并没有描述应用如何更新 state。
-
特点:
- 只描述做什么
- JS 对象,必须带有
type属性,用于区分动作的类型 - 根据功能的不同,可以携带额外的数据,配合该数据来完成相应功能
示例:
// action
{ type: 'increment' } // +1
{ type: 'decrement', count: 2 } // -1
// todomvc 案例
// { type: 'addTodo' }
{ type: 'addTodo', name: '吃饭' }
{ type: 'addTodo', name: '睡觉' }
// { type: 'removeTodo' }
{ type: 'removeTodo', id: 3 }
reducer
-
一个纯函数
-
作用
-
- 初始化状态
- 修改状态
-
-
修改状态
根据传入的旧状态和action,返回新状态
公式:
(previousState, action) => newState
示例
const reducer = (state = 0, action) => {
switch (action.type) {
case 'add':
// 返回新的state
return state + 1
case 'addN':
// 返回新的state
return state + action.payload
default:
return state
}
}
store
-
store:仓库,Redux 的核心,整合 action 和 reducer
-
特点:
- 一个应用只有一个 store
- 维护应用的状态,获取状态:
store.getState() - 创建 store 时接收 reducer 作为参数:
const store = createStore(reducer) - 发起状态更新时,需要分发 action:
store.dispatch(action)
-
其他 API, — 订阅(监听)状态变化:
const unSubscribe = store.subscribe(() => {})— 取消订阅状态变化:unSubscribe()
Redux功能演示核心代码
安装redux
npm i redux
目标
通过一段代码,来对应理解redux中的三个核心概念,并学习相关的api
三个核心概念对应的代码
定义reuder
initState = 0
function reducer(state = initState, action) {
return state
}
定义action
const action1 = { type:'addN', payload: 12 }
// store.dispatch(action1)
const action2 = { type:'add', payload: 1 }
定义store
import { createStore } from 'redux'
// 创建 store
const store = createStore(reducer)
store 相关API
- store获取状态
store.getState() - store修改数据,通过派发action。
store.dispatch({type:xxx, payload: xx}}) - store添加订阅者。
// 格式:
// 取消订阅 = store.subscribe(订阅者)
// 订阅者:就是一个函数,当state值变化时,store会执行它
// store.subscript()的返回值也是一个函数,执行它时,它的
const unSubscribe = store.subscribe(() => {
// 状态改变时,执行相应操作
})
// 取消监听状态变化
// unSubscribe()
- 取消订阅者。
unSubscribe()
核心代码
import { createStore } from 'redux'
initState = 0
function reducer(state = initState, action) {
return state
}
// 创建 store
const store = createStore(reducer)
// store获取状态
console.log(store.getState())
// 更新状态
// 1. dispatch 派遣,派出。表示:分发一个 action,也就是发起状态更新
// 2. store.dispatch会 调用reducer函数,并将action传递给reducer
const action1 = { type:'addN', payload: 12 }
store.dispatch(action1)
const action2 = { type:'add', payload: 1 }
store.dispatch(action2)
// store 添加订阅者
// store.subscribe
// 订阅者:就是一个函数,当state值变化时,store会执行它
const unSubscribe = store.subscribe(() => {
// 状态改变时,执行相应操作
console.log('数据变化了....')
})
// 取消监听状态变化
unSubscribe()
Redux 代码执行过程
获取默认值
只要创建 store,那么,Redux 就会调用一次 reducer, 且type是一个随机值。如下是一个示例:
- type是随机值确保了它不会被用户业务逻辑处理,而只能去匹配默认值。
这一次调用 reducer 的目的:获取状态的默认值。这个初始值将成为下一次调用 store.getState() 方法来获取 Redux 状态值的preState
更新状态
- 当你需要更新状态时,就先分发动作
store.dispatch(action) - Redux 内部,store 就会调用 reducer,传入:上一次的状态(当前示例中就是:
0)和 action({ type: 'add' }),计算出新的状态,并返回这个新值。 - reducer 执行完毕后,将最新的状态交给 store,store 用最新的状态替换旧状态,状态更新完毕
import { createStore } from 'redux'
const store = createStore(reducer)
function reducer(state = 0, action) {
console.log('reducer:', state, action)
switch (action.type) {
case 'add':
return state + 1
default:
return state
}
}
console.log('状态值为:', store.getState()) // 10
// 发起更新状态:
// 参数: action 对象
store.dispatch({ type: 'increment' })
// 相当于: reducer(10, { type: 'increment' })
console.log('更新后:', store.getState()) // 11
解决问题-redux在react中使用
目标
用redux来解决前面提到的多组件数据共享的问题
目录结构
├── src
├── store # redux目录,一般约定叫store
│ ├── index.js # 定义并导出store. 其中会导入reducer
│ └── reducer # reducer函数
├── App.js # 根组件,引入Father 和 Uncle组件
├── Father.js # Father组件。其中引入了Son组件
├── index.js # 项目的入口文件,会导入并渲染App.js
├── Son.js # Son组件
└── Uncle.js # Unclde组件
store/index.js
index.js
用reducer来初始化store
import { createStore } from 'redux'
import reducer from './reducer'
export default createStore(reducer)
reducer.js
设置初始值,并提供修改数据的逻辑
const initState = 0
export default function reducer (state = initState, action) {
console.log(action)
switch (action.type) {
case 'addN':
return state + action.payload
case 'sub':
return state - 1
default:
return state
}
}
src/index.js
import React from 'react'
import ReactDom from 'react-dom'
import App from './App'
import store from './store/index.js'
ReactDom.render(<App />, document.getElementById('root'))
// 数据变化时,重新渲染
store.subscribe(() => {
ReactDom.render(<App />, document.getElementById('root'))
})
src/App.js
import React from 'react'
import Father from './Father'
import Uncle from './Uncle'
export default function App () {
return (
<div style={{ border: '1px solid #ccc', margin: 10, padding: 10 }}>
根组件
<Father />
<Uncle />
</div>
)
}
son.js
获取数据: store.getState()
修改数据: store.dispatch(action)
import React, { useState } from 'react'
import store from './store/index'
export default function Son () {
const [n, setN] = useState(12)
const add = () => {
store.dispatch({ type: 'addN', payload: n })
}
return (
<div style={{ border: '1px solid #ccc', margin: 10, padding: 10 }}>
子组件
{store.getState()}
<button onClick={add}>+n</button>
<input value={n} onChange={(e) => setN(e.target.value)} />
</div>
)
}
uncle.js
修改数据: store.dispatch(action)
import React from 'react'
import store from './store/index'
export default function Uncle () {
return (
<div style={{ border: '1px solid #ccc', margin: 10, padding: 10 }}>
Uncle组件
<button
onClick={() => {
store.dispatch({ type: 'sub' })
}}>
-1
</button>
</div>
)
}
React-redux基本介绍
目标
能够说出为什么需要使用react-redux
react中直接使用redux有很多不方便的地方
- 每个组件都需要单独导入store
- 在根组件上的写法不友好
react-redux 库
是 Redux 官方提供的 React 绑定库
- React 和 Redux 是两个独立的库,两者之间职责独立。
- Redux可以和其他的js库,框架一起使用,而并不专门用于react。
- 为了实现在 React 中使用 Redux 进行状态管理 ,就需要一种机制,将这两个独立的库关联在一起。这时候就用到 React-Redux 这个绑定库了
- 作用: 为 React 接入 Redux,实现在 React 中使用 Redux 进行状态管理。
react-redux-基本使用
目标
使用react-redux简化redux在react项目中的使用
步骤
-
安装
npm i react-redux -
使用
- 按redux的要求,创建好store, reducer,action等等
- 从react-redux中引入 provider, useSelector, useDispatch来 操作 redux
API
Provider
- 用法:直接包装在根组件上。
<Provider store={store}> - 好处:相比react + redux,这样就不需要每个组件都引入store了
useSelector
-
用法:获取公共状态
-
好处:
- 相比react + redux,不需要使用store.getState()了
- state变化了,它会自动更新
-
格式:
const 状态 = useSelector(store的数据 => 你需要的部分)
useDispatch
- 用法:派发action,修改数据
- 格式:
const dispatch = useDispatch(); dispatch(action)
代码
index.js
import React from 'react'
import ReactDom from 'react-dom'
import App from './App'
import store from './store'
import { Provider } from 'react-redux'
ReactDom.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
uncle.js
import React from 'react'
import { useDispatch, useSelector } from 'react-redux'
export default function Uncle () {
// 修改数据
const dispatch = useDispatch()
// 获取数据
// state:就是 redux中的数据
const num = useSelector((state) => {
return state
})
return (
<div style={{ border: '1px solid #ccc', margin: 10, padding: 10 }}>
Uncle组件, {num}
<button
onClick={() => {
dispatch({ type: 'sub1' })
}}>
-1
</button>
</div>
)
}
Reducer 的分离与合并-思路
问题
随着项目功能变得越来越复杂,需要 Redux 管理的状态也会越来越多。导致reducer函数越来越重。
例如,项目要管理图书和个人信息
解决思路
拆分。按业务模块拆分,把大的reducer拆成小文件。
示例:
├── src
├── store # redux目录,一般约定叫store
| ├── index.js # 定义并导出store. 其中会导入reducer
| └── reducers # 多个模块的reducer
| ├── reducer1.js # 模块1的reducer
| ├── reducer2.js # 模块2的reducer
| └── index.js # reducer的整体入口,会导入reducer1, reducer2
├── index.js # 项目的入口文件,会导入并渲染App.js
├── App.js # 根组件,引入模块1 和模块2 组件
├── 模块1.js # 模块1 组件
└── 模块2.js # 模块2 组件
示例代码
src/store/reducers/book.js
export default function book (state = ['射雕英雄传', '天龙八部'], action) {
console.log(action)
switch (action.type) {
case 'book/add':
return [state, action.payload]
default:
return state
}
}
src/store/reducers/index.js
import book from './book'
import user from './user'
import { combineReducers } from 'redux'
const rootReducer = combineReducers({
book,
user
})
export default rootReducer
Reducer 的分离与合并-实现
示意图
合并后,Redux 的状态会变为一个对象,对象的结构与 combineReducers 函数的参数结构相同
操作
- 获取状态。
useSelecter - 修改状态。
dispatch()
小结
- 多个reducer会合并在一起。
- 每一个action都去执行每个reducer
- action.type取名时,加上模块名
案例-todo-基本介绍
用function组件+hooks + react-redux
案例-todo-初始化
- store准备
- 组件划分
案例-todo-列表渲染
main.js
import React from 'react'
import { useSelector } from 'react-redux'
import Item from './Item'
export default function Main () {
const list = useSelector((state) => state.todos)
console.log(list)
return (
<section className="main">
<input id="toggle-all" className="toggle-all" type="checkbox" />
<label htmlFor="toggle-all">Mark all as complete</label>
<ul className="todo-list">
{list.map((it) => <Item key={it.id} todo={it} />)}
</ul>
</section>
)
}
item.js
/* eslint-disable react/prop-types */
import React from 'react'
export default function Item ({ todo }) {
return (
<li key={todo.id} className={todo.isDone ? 'completed' : ''}>
<div className="view">
<input className="toggle" type="checkbox" checked={todo.isDone} />
<label>{todo.name}</label>
<button className="destroy" />
</div>
<input
className="edit"
onChange={() => {}}
value="Create a TodoMVC template"
/>
</li>
)
}
案例-todo-删除任务
item.js
/* eslint-disable react/prop-types */
import React from 'react'
+ import { useDispatch } from 'react-redux'
export default function Item ({ todo }) {
+ const dispatch = useDispatch()
return (
<li key={todo.id} className={todo.isDone ? 'completed' : ''}>
<div className="view">
<input className="toggle" type="checkbox" checked={todo.isDone} />
<label>{todo.name}</label>
<button
className="destroy"
+ onClick={() => dispatch({ type: 'todos/del', payload: todo.id })}
/>
</div>
<input
className="edit"
onChange={() => {}}
value="Create a TodoMVC template"
/>
</li>
)
}
reducer/todos.js
const initState = [
{ id: 1, name: '吃饭', isDone: false },
{ id: 2, name: '睡觉', isDone: true }
]
function todos (state = initState, action) {
switch (action.type) {
case 'todos/add':
return [...state, action.payload]
case 'todos/del':
return state.filter((it) => it.id !== action.payload)
default:
return state
}
}
export default todos
案例-todo-修改任务状态
reducer
function todos (state = initState, action) {
switch (action.type) {
case 'todos/add':
return [...state, action.payload]
case 'todos/del':
return state.filter((it) => it.id !== action.payload)
case 'todos/changeState':
return state.map(
(it) =>
it.id === action.payload.id
? { ...it, isDone: action.payload.isDone }
: it
)
default:
return state
}
}
item.js
<input
className="toggle"
onClick={() => {
dispatch({
type: 'todos/changeState',
payload: { id: todo.id, isDone: !todo.isDone }
})
}}
type="checkbox"
checked={todo.isDone}
/>
案例-todo-添加新任务
header.js
import React, { useState } from 'react'
import { useDispatch } from 'react-redux'
export default function Header () {
const [name, setName] = useState('')
const dispatch = useDispatch()
return (
<header className="header">
<h1>todos</h1>
<input
onKeyUp={(e) => {
if (e.keyCode === 13) {
if (!name) return
dispatch({ type: 'todos/add', payload: name })
setName('')
}
}}
className="new-todo"
value={name}
onChange={(e) => {
setName(e.target.value.trim())
}}
placeholder="接下来打算做点啥?"
autoFocus
/>
</header>
)
}
reducer/todos.js
补充:'todos/add'
const initState = [
{ id: 1, name: '吃饭', isDone: false },
{ id: 2, name: '睡觉', isDone: true }
]
let idx = 2
function todos (state = initState, action) {
switch (action.type) {
case 'todos/add':
return [...state, { id: ++idx, name: action.payload, isDone: false }]
case 'todos/del':
return state.filter((it) => it.id !== action.payload)
case 'todos/changeState':
return state.map(
(it) =>
it.id === action.payload.id
? { ...it, isDone: action.payload.isDone }
: it
)
default:
return state
}
}
export default todos
案例-todo- 全选功能
main.js
export default function Main () {
const list = useSelector((state) => state.todos)
+ const isCheckAll = list.every((it) => it.isDone)
+ const dispatch = useDispatch()
return (
<section className="main">
<input
id="toggle-all"
className="toggle-all"
+ checked={isCheckAll}
+ onChange={() => {
dispatch({ type: 'todos/changeAll', payload: !isCheckAll })
}}
type="checkbox"
/>
reducer/todos.js
case 'todos/changeAll':
return state.map((it) => ({ ...it, isDone: action.payload }))
案例-todo-双击-开启编辑
背景:编辑时有一个特殊的类 editing
Item.js
- 双击进入编辑模式
- 失去焦点,放弃编辑
/* eslint-disable react/prop-types */
import React, { useState } from 'react'
import { useDispatch } from 'react-redux'
export default function Item ({ todo }) {
const dispatch = useDispatch()
const [isEdit, setIsEdit] = useState(false)
const [name, setName] = useState(todo.name)
const classes = [todo.isDone ? 'completed' : '', isEdit ? 'editing' : '']
return (
<li key={todo.id} className={classes.join(' ')}>
<div className="view">
<input
className="toggle"
onClick={() => {
dispatch({
type: 'todos/changeState',
payload: { id: todo.id, isDone: !todo.isDone }
})
}}
type="checkbox"
checked={todo.isDone}
/>
<label
onDoubleClick={() => {
setIsEdit(true)
}}>
{todo.name}
</label>
<button
className="destroy"
onClick={() => dispatch({ type: 'todos/del', payload: todo.id })}
/>
</div>
<input
onBlur={() => setIsEdit(false)}
className="edit"
onChange={(e) => {
setName(e.target.value.trim())
}}
value={name}
/>
</li>
)
}
案例-todo-双击-保存修改
main.js
<input
onKeyUp={(e) => {
if (e.keyCode === 13) {
dispatch({
type: 'todos/editName',
payload: { id: todo.id, newName: name }
})
}
}}
onBlur={() => setIsEdit(false)}
className="edit"
onChange={(e) => {
setName(e.target.value.trim())
}}
value={name}
/>
reducer/todos.js
case 'todos/changeName':
return state.map(
(it) => (it.id === payload.id ? { ...it, name: payload.newName } : it)
)
1
案例-todo-双击-自动获取焦点-问题
item.js
用副作用函数来获取焦点
const refTxt = useRef(null)
useEffect(
() => {
refTxt.current.focus()
},
[isEdit]
)
<input
onKeyUp={(e) => {
if (e.keyCode === 13) {
dispatch({
type: 'todos/editName',
payload: { id: todo.id, newName: name }
})
}
}}
ref={refTxt}
onBlur={() => setIsEdit(false)}
className="edit"
onChange={(e) => {
setName(e.target.value.trim())
}}
value={name}
/>
案例-todo-清空已完成任务
Footer.js
思路: dispatch({ type: 'todos/delDone' })
import React from 'react'
import { useSelector, useDispatch } from 'react-redux'
export default function Footer () {
const dispatch = useDispatch()
const list = useSelector((state) => state.todos)
const countOfUnDone = list.filter((it) => !it.isDone).length
return (
<footer className="footer">
<span className="todo-count">
<strong>{countOfUnDone}</strong> 未完成
</span>
<ul className="filters">
<li>
<a className="selected" href="#/">
所有
</a>
</li>
<li>
<a href="#/active">待办</a>
</li>
<li>
<a href="#/completed">已完成</a>
</li>
</ul>
<button
className="clear-completed"
onClick={() => {
+ dispatch({ type: 'todos/delDone' })
}}>
清空已完成
</button>
</footer>
)
}
reducer/todos.js
case 'todos/delDone':
return state.filter((it) => !it.isDone)
案例-todo-底部筛选
footer.js
- 点击不同类型时去修改redux中的state
- 根据当前的state来显示是否选中
import React from 'react'
import { useSelector, useDispatch } from 'react-redux'
export default function Footer () {
const dispatch = useDispatch()
const list = useSelector((state) => state.todos)
const filterType = useSelector((state) => state.filter)
const countOfUnDone = list.filter((it) => !it.isDone).length
return (
<footer className="footer">
<span className="todo-count">
<strong>{countOfUnDone}</strong> 未完成
</span>
<ul className="filters">
<li>
<a
className={filterType === 'all' ? 'selected' : ''}
href="#/"
onClick={() =>
dispatch({ type: 'filter/setType', payload: 'all' })}>
{' '}
所有
</a>
</li>
<li>
<a
className={filterType === 'active' ? 'selected' : ''}
href="#/active"
onClick={() =>
dispatch({ type: 'filter/setType', payload: 'active' })}>
待办
</a>
</li>
<li>
<a
className={filterType === 'completed' ? 'selected' : ''}
href="#/completed"
onClick={() =>
dispatch({ type: 'filter/setType', payload: 'completed' })}>
已完成
</a>
</li>
</ul>
<button
className="clear-completed"
onClick={() => {
dispatch({ type: 'todos/delDone' })
}}>
清空已完成
</button>
</footer>
)
}
reducer/filter.js
export default function filter (state = 'all', action) {
switch (action.type) {
case 'filter/setType':
return action.payload
default:
return state
}
}
main.js
const list = useSelector((state) => {
if (state.filter === 'all') {
return state.todos
} else if (state.filter === 'active') {
return state.todos.filter((it) => !it.isDone)
} else {
return state.todos.filter((it) => it.isDone)
}
})
拓展-纯函数
目标: 了解纯函数的特点
内容:
-
纯函数是函数式编程中的概念,对于纯函数来说,相同的输入总是得到相同的输出
var arr = [1,2,3,4,5] arr.slice(1,2) arr.splice(1,2) -
原则:
- 不得改写参数
- 不能调用 Date.now()或者 Math.random()等不纯的方法,因为每次会得到不一样的结果
- 不能使用全局变量
-
reducer 必须是一个纯函数
-
纯函数主要的含义就是它不可以修改影响输入值
-
没有副作用,副作用指的是例如函数中一些异步调用或者会影响函数作用域之外的变量一类的操作
纯函数的好处:
- 效率高,可以使用缓存来优化
拓展-在原生js中使用redux
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div>
数据: <div id="res">1</div>
<div>
<button>+1</button>
<button onclick="store.dispatch({type:'sub',data:1})">-1</button>
</div>
</div>
<script src="./redux.js"></script>
<!-- <script src="https://cdn.bootcdn.net/ajax/libs/redux/4.1.0/redux.js"></script> -->
<script>
const action = {type: 'add'}
const actionCreater = (data) =>({
type: 'add',
data
})
const reduer = (state=1, action) => {
switch (action.type) {
case "add":
return state + action.data
break;
case "sub":
return state + action.data
break;
default:
return state
break;
}
}
// 创建store
const { createStore } = Redux
// createStore(reducer函数,可选=>state初始值)
let store = createStore(reduer,1000)
// 打印初始状态
console.log(store.getState()) // {count: 1}
// 每次 state 更新时,打印日志
// 注意 subscribe() 返回一个函数用来注销监听器
const unsubscribe = store.subscribe(() => {
document.querySelector('#res').innerHTML = store.getState()
})
// 发起一系列 action
store.dispatch(actionCreater(2))
// store.dispatch(add(4))
// 停止监听 state 更新
// unsubscribe();
</script>
</body>
</html>