diff --git a/docs/.vitepress/const/links.ts b/docs/.vitepress/const/links.ts
index af07b0c2..5deb5b9c 100644
--- a/docs/.vitepress/const/links.ts
+++ b/docs/.vitepress/const/links.ts
@@ -17,5 +17,7 @@ export const notes = [
{ text: '前端工程化', link: '/note/Front-end Engineering' },
{ text: '服务端渲染', link: '/note/SSR' },
{ text: 'React基础', link: '/note/React' },
+ { text: 'Redux', link: '/note/Redux' },
+ { text: 'React Router', link: '/note/React Router' },
{ text: 'MySQL', link: '/note/MySQL' }
]
diff --git a/docs/note/React Router.md b/docs/note/React Router.md
new file mode 100644
index 00000000..5991a6cb
--- /dev/null
+++ b/docs/note/React Router.md
@@ -0,0 +1,482 @@
+## React Router
+
+三大框架都有各自的路由实现
+
+- Angular ngRouter
+- React ReactRouter
+- Vue VueRouter
+
+React Router在最近两年的版本更新较快,并且在最新的React Router6发生了较大的变化
+
+- Web开发只需要安装`react-router-dom`
+- `react-router`包含一些ReactNative的内容
+
+```bash
+npm i react-router-dom
+```
+
+从`react-router-dom`中导出`BrowserRouter` 或 `HashRouter`,二者分别对应history模式与哈希模式
+
+将App用二者之一包裹,即可启用路由:
+
+```tsx {5,10,12}
+// index.js
+import React, { StrictMode } from 'react'
+import ReactDOM from 'react-dom/client'
+import App from './App'
+import { HashRouter } from 'react-router-dom'
+
+const root = ReactDOM.createRoot(document.getElementById('root'))
+root.render(
+
+
+
+
+
+)
+```
+
+路由的本质是路径与组件的映射关系(`path <==> component`)
+
+ReactRouter不像VueRouter,它的路由映射关系是书写在组件中的:
+
+下面的例子中使用到了几个**组件**:`Routes` `Route` `Navigate` `NavLink`
+
+- `Routes` `Route`用来描述路径与组件的映射关系
+ - 通过为`path`和`element`传入路径和相对应的组件,将其包裹在`Routes`内即可完成路由的描述
+- `Navigate` 导航组件(在react-router5版本中是Redirect)
+ - 可以帮我们完成重定向操作,将想要重定向的路径传递给组件的`to`属性
+ - **当组件出现时,就会自动执行跳转**,属于功能性组件
+ - 当访问根路径`/`时就会自动跳转到`/home`页
+- `NavLink`用来实现路由的跳转
+ - 特殊组件,其`className` `style`这些属性都可以传递一个函数
+ - 可以从函数参数中解构出`isActive`属性来动态绑定样式(实际场景应用较少)
+
+```tsx
+// App.js
+import React, { PureComponent } from 'react'
+import { Routes, Route, Navigate, NavLink } from 'react-router-dom'
+import Home from './views/Home'
+import About from './views/About'
+import NotFound from './views/NotFound'
+
+export default class App extends PureComponent {
+ render() {
+ return (
+
+
App
+ (isActive ? 'link-active' : '')}>
+ Home
+
+ ({ color: isActive ? 'red' : '' })}>
+ About
+
+
+ }>
+ }>
+ }>
+ }>
+
+
+ )
+ }
+}
+```
+
+另外,这里还有一个小技巧,在最末一个路由指定一个path为`*`的路由匹配规则,可以为路由匹配添加fallback策略,当未匹配到其之前的任何域名时,会展示NotFound页面
+
+## 嵌套路由
+
+嵌套路由可以通过在`Route`组件内部嵌套新的`Route`组件来实现
+
+再通过`Outlet`组件来指定嵌套路由的占位元素(类似于VueRouter中的router-view)
+
+我们在之前的例子的基础上,为Home页面添加两个子页面HomeRanking和HomeRecommand
+
+同时,我们也应该为Home组件添加默认跳转,就像根路径默认重定向到Home组件那样,进入到Home组件后也应该默认重定向一个子页面中,这里我们仍然使用到了Navigate组件
+
+::: code-group
+```tsx [App.jsx ]
+// App.jsx
+import React, { PureComponent } from 'react'
+import { Routes, Route, Navigate, NavLink } from 'react-router-dom'
+import Home from './views/Home'
+import HomeRanking from './views/HomeRanking'
+import HomeRecommand from './views/HomeRecommand'
+
+export default class App extends PureComponent {
+ render() {
+ return (
+
+
+ }>
+ }>
+ }>
+ }>
+
+
+
+ )
+ }
+}
+```
+```tsx [Home.jsx]
+// Home.jsx
+import React, { PureComponent } from 'react'
+import { NavLink, Outlet } from 'react-router-dom'
+
+export default class Home extends PureComponent {
+ render() {
+ return (
+
+ )
+ }
+}
+```
+:::
+
+## 编程式导航(高阶组件)
+
+之前使用的ReactRouter提供的路由跳转的组件,无论是`Link`还是`NavLink`可定制化能力都比较差,无法实现“点击按钮后跳转路由”这样的需求,那么我们就需要通过编程式导航,使用JS来完成路由的跳转
+
+ReactRouter提供了编程式导航的API:`useNavigate`
+
+自ReactRouter6起,编程式导航的API不再支持ClassComponent,全面拥抱Hooks。
+
+我们将在后续的学习中开启Hooks的写法,那么目前如何在类组件中也能使用Hooks呢?答案是高阶组件
+
+封装一个高阶组件`withRouter`,经过高阶组件处理的类组件的props将会携带router对象,上面包含一些我们需要的属性和方法:
+
+::: code-group
+```tsx [withRouter.js]
+// withRouter.js
+import { useNavigate } from 'react-router-dom'
+
+export function withRouter(WrapperComponent) {
+ return (props) => {
+ const navigate = useNavigate()
+ const router = { navigate }
+ return
+ }
+}
+```
+```tsx [Home.jsx]
+// Home.jsx
+import React, { PureComponent } from 'react'
+import { Outlet } from 'react-router-dom'
+import { withRouter } from '../hoc/withRouter'
+
+export default withRouter(
+ class Home extends PureComponent {
+ render() {
+ return (
+
+
Home
+
this.props.router.navigate('/home/ranking')}>Ranking
+
this.props.router.navigate('/home/recommand')}>Recommand
+
+
+ )
+ }
+ }
+)
+```
+:::
+
+我们使用`withRouter`高阶组件对Home组件进行了增强,可以通过编程式导航来实现二级路由跳转
+
+这里只是展示了编程式导航的用法和高阶组件的能力,目前还是尽可能使用Hooks写法编写新项目
+
+## 动态路由(路由传参)
+
+传递参数由两种方式:
+
+- 动态路由的方式
+- 查询字符串传递参数
+
+动态路由是指:路由中的**路径**信息并不会固定
+
+- 比如匹配规则为`/detail/:id`时,`/detail/123` `detail/888`都会被匹配上,并将`123/888`作为id参数传递
+- 其中`/detail/:id`这个匹配规则被称为动态路由
+
+动态路由常见于嵌套路由跳转,比如:从歌曲列表页面点击后跳转到歌曲详情页,可以通过路由传递歌曲的ID,访问到不同歌曲的详情页
+
+我们在之前的HomeRanking榜单中加入列表和点击跳转功能,并编写一个新的组件Detail来接收来自路由的参数
+
+同样地,`react-router-dom`为我们提供了从路由获取参数的API:`useParams`,它是一个Hooks,我们将它应用到之前编写的高级组件`withRouter`中
+
+- 在使用了`withRouter`的组件中,就可以通过`this.props.router.params.xxx`获取到当前路由中传递的参数
+- 使用动态匹配路由时,传递给Route组件的`path`属性为`:xxx`,这里是`/detail/:id`
+
+::: code-group
+```tsx [withRouter.js]
+// withRouter.js
+import { useNavigate, useParams } from 'react-router-dom'
+
+export function withRouter(WrapperComponent) {
+ return (props) => {
+ const navigate = useNavigate()
+ const params = useParams()
+ const router = { navigate, params }
+ return
+ }
+}
+```
+```tsx [HomeRanking.jsx]
+// HomeRanking.jsx
+import React, { PureComponent } from 'react'
+import { withRouter } from '../hoc/withRouter'
+
+export default withRouter(
+ class HomeRanking extends PureComponent {
+ render() {
+ const list = Array.from(Array(10), (x, i) => ({
+ id: ++i,
+ name: `Music ${i}`
+ }))
+ return (
+
+
HomeRanking
+
+ {list.map((item, index) => (
+ this.props.router.navigate(`/detail/${item.id}`)}>
+ {item.name}
+
+ ))}
+
+
+ )
+ }
+ }
+)
+```
+```tsx [Detail.jsx]
+// Detail.jsx
+import React, { PureComponent } from 'react'
+import { withRouter } from '../hoc/withRouter'
+
+export default withRouter(
+ class Detail extends PureComponent {
+ render() {
+ return (
+
+
Detail
+ Current Music ID: {this.props.router.params.id}
+
+ )
+ }
+ }
+)
+```
+:::
+
+### 查询字符串的参数
+
+之前传递的是路径参数,那么查询字符串参数应该如何获取?
+
+可以通过`useLocation`这个Hooks拿到当前地址详细信息:
+
+```tsx
+const location = useLocation()
+location.search // ?name=ziu&age=18
+```
+
+需要自行完成数据的解析,不太方便
+
+还有一个Hooks:`useSearchParams`,可以在获取到查询字符串信息的同时帮我们解析成`URLSearchParams`对象
+
+要从`URLSearchParams`类型的对象中取值,需要通过标准方法`get`
+
+```tsx
+const [ searchParams, setSearchParams ] = useSearchParams()
+searchParams.get('name') // 'ziu'
+searchParams.get('age') // 18
+```
+
+当然,我们在实际使用中也可以通过`Object.fromEntries`将它转为普通对象,这样我们使用`useSearchParams`来对之前编写的高阶组件`withRouter`做一次增强:
+
+```tsx {8,9}
+// withRouter.js
+import { useNavigate, useParams, useSearchParams } from 'react-router-dom'
+
+export function withRouter(WrapperComponent) {
+ return (props) => {
+ const navigate = useNavigate()
+ const params = useParams()
+ const [searchParams] = useSearchParams()
+ const query = Object.fromEntries(searchParams)
+ const router = { navigate, params, query }
+ return
+ }
+}
+```
+
+::: tip
+需要注意的是,这里的`useSearchParams`是一个Hooks的常见形态
+
+它返回一个数组,数组的首位为值,数组的次位为改变值的方法
+
+与对象解构不同的是,数组结构是对位解构:保证位置一致则值一致,命名随意
+
+而对象解构恰恰相反,不必保证位置,而需要保证命名一致
+:::
+
+## 路由的配置方式
+
+至此为止,路由的配置是耦合在`App.jsx`中的,我们可以将Routes这部分代码抽离出单独的组件,也可以通过配置的方式来完成路由映射关系的编写
+
+- 在ReactRouter5版本中,我们可以将路由的映射规则写为JS对象,需要引入第三方库`react-router-config`
+- 在ReactRouter6版本中,允许我们将其写为配置文件,不需要安装其他内容
+
+6版本为我们提供了一个API:`useRoutes`,将我们编写的配置文件传入此函数,可以将其转化为之前编写的组件结构,本质上也是一种语法糖
+
+需要注意的是,Hooks只能在函数式组件中使用,这里我们将App组件改用FunctionComponent书写了
+
+::: code-group
+```tsx [index.js]
+// router/index.js
+import { Navigate } from 'react-router-dom'
+import Home from '../views/Home'
+import HomeRanking from '../views/HomeRanking'
+import HomeRecommand from '../views/HomeRecommand'
+import About from '../views/About'
+import Detail from '../views/Detail'
+import NotFound from '../views/NotFound'
+
+export const routes = [
+ {
+ path: '/',
+ element:
+ },
+ {
+ path: '/home',
+ element: ,
+ children: [
+ {
+ path: '',
+ element:
+ },
+ {
+ path: 'ranking',
+ element:
+ },
+ {
+ path: 'recommand',
+ element:
+ }
+ ]
+ },
+ {
+ path: '/about',
+ element:
+ },
+ {
+ path: '/detail/:id',
+ element:
+ },
+ {
+ path: '*',
+ element:
+ }
+]
+```
+```tsx [App.jsx]
+import React from 'react'
+import { NavLink, useRoutes } from 'react-router-dom'
+import { routes } from './router'
+
+export default function App() {
+ return (
+
+
App
+ (isActive ? 'link-active' : '')}>
+ Home
+
+ ({ color: isActive ? 'red' : '' })}>
+ About
+
+ {useRoutes(routes)}
+
+ )
+}
+```
+:::
+
+## 懒加载
+
+针对某些场景的首屏优化,我们可以根据路由对代码进行分包,只有需要访问到某些页面时才从服务器请求对应的JS代码块
+
+可以使用`React.lazy(() => import( ... ))`对某些代码进行懒加载
+
+结合之前使用到的配置式路由映射规则,我们使用懒加载对代码进行分包
+
+```tsx {6,7,18,24}
+// router/index.js
+import { lazy } from 'react'
+// import HomeRecommand from '../views/HomeRecommand'
+// import About from '../views/About'
+
+const HomeRecommand = lazy(() => import('../views/HomeRecommand'))
+const About = lazy(() => import('../views/About'))
+
+export const routes = [
+ ...
+ {
+ path: '/home',
+ element: ,
+ children: [
+ ...
+ {
+ path: 'recommand',
+ element:
+ }
+ ]
+ },
+ {
+ path: '/about',
+ element:
+ },
+ ...
+]
+```
+
+这时在终端执行`pnpm build`可以发现,构建产物为我们执行了分包,`About`和`HomeRecommand`这两个次级页面被打进了两个单独的包中
+
+> 在Vue中默认为我们完成了代码分包,第三方包的代码都被打包到了`vendors`中,业务代码放到了单独的JS文件中
+
+> 只有当我们访问到这些页面时,才会发起网络请求,请求这些次级页面的JS代码
+
+然而如果你在react-app的构建产物`index.html`开启本地预览服务器,会发现切换到对应页面后项目会crash(本地开发也会crash)
+
+```bash
+# 使用 serve 开启本地预览服务器
+pnpm add serve -g
+serve -s build # 将 build 作为根目录
+```
+
+这是因为React默认没有为异步组件做额外处理,我们需要使用`Suspense`组件来额外处理懒加载的组件
+
+```tsx
+// index.js
+import React, { StrictMode, Suspense } from 'react'
+import ReactDOM from 'react-dom/client'
+import App from './App'
+import { HashRouter } from 'react-router-dom'
+
+const root = ReactDOM.createRoot(document.getElementById('root'))
+root.render(
+
+
+ Loading...}>
+
+
+
+
+)
+```
+
+当根组件内部有组件处于异步加载状态时,都会在页面上展示`Loading...`而不是崩溃掉
diff --git a/docs/note/React.md b/docs/note/React.md
index 9342e251..f027847a 100644
--- a/docs/note/React.md
+++ b/docs/note/React.md
@@ -3726,1622 +3726,3 @@ classNames('foo', 'bar')
classNames('foo', { bar: true })
classNames(...['foo', 'bar'])
```
-
-## Redux
-
-- Redux的核心思想
-- Redux的基本使用
-- React结合Redux
-- Redux的异步操作
-- redux-devtool
-- reducer的模块拆分
-
-### 理解JavaScript的纯函数
-
-- 函数式编程中有一个非常重要的概念 **纯函数**,JavaScript符合函数式编程的范式,所以也有纯函数的概念
- - 在React开发中,纯函数被多次提及:
- - React组件被要求像一个纯函数(为什么是像,因为还有类组件)
- - Redux中有一个reducer的概念,同样是要求必须是一个纯函数
-- 掌握纯函数对于理解很多框架的设计都是有帮助的
-
-一个纯函数必然具备以下特征:
-
-- 确定的输入一定产生确定的输出
-- 函数的执行过程中,不能产生副作用
-
-
-### 为什么需要Redux
-
-- JS需要管理的状态越来越多,越来越复杂
-- 状态不断发生变化之间又相互依赖,这要求视图层也能同步更新
-- React提供了自动更新视图的方法,但状态仍需要手动管理
-- Redux可以帮我们管理状态,提供了**可预测的状态管理**
-- 框架无关,体积只有2KB大小
-
-### Redux的核心理念
-
-Redux的核心理念 Store
-
-- 定义一个统一的规范来操作数据,这样就可以做到对数据的跟踪
-- `list.push()` `list[0].age = 18`
-
-Redux的核心理念 Action
-
-- Redux要求:要修改数据,必须通过Action来修改
-- 所有数据的变化,必须通过派发(Patch)Action来更新
-- Action是一个普通的JS对象,用来描述此次更新的type与content
-- `const action = { type: 'ADD_ITEM', item: { name: 'Ziu', age: 18 } }`
-
-Redux的核心理念 Reducer
-
-- 如何将Store和Action联系在一起?
-- reducer是一个纯函数
-- 完成的工作就是:将传入的state和action结合起来,生成一个新的state
-- `patch` => `reducer` => `newState` => `Store`
-
-### Redux Demo
-
-下例中,通过`createStore`创建了一个Store(已经不推荐了)
-
-- initialState用于在调用`createStore`时作为默认值传入`reducer`
-- 后续每次`store.dispatch`都会调用`reducer`
-- 通过`reducer`更新state中的数据
-
-在React中,可以通过`store.subscribe`注册State变化的监听回调
-
-- 当state发生变化时,通过调用`this.forceUpdate`触发组件的更新
-- 一般情况下,我们在`componentDidMount`注册监听回调,在`componentWillUnmount`解除监听
-
-::: code-group
-```tsx [App.jsx]
-// App.jsx
-import React, { PureComponent } from 'react'
-import store from './store'
-
-export default class App extends PureComponent {
- componentDidMount() {
- // Subscribe to the store
- store.subscribe(() => {
- console.log('subscribe', store.getState())
- this.forceUpdate()
- })
- }
-
- componentWillUnmount() {
- store.unsubscribe()
- }
-
- render() {
- return (
-
-
App
-
Count: {store.getState().count}
-
Name: {store.getState().name}
-
store.dispatch({ type: 'INCREMENT' })}> +1
-
store.dispatch({ type: 'DECREMENT' })}> -1
-
store.dispatch({ type: 'CHANGE_NAME', name: 'ZIU' })}>
- {' '}
- CHANGE_NAME{' '}
-
-
- )
- }
-}
-```
-```tsx [index.js]
-// store/index.js
-import { createStore } from 'redux'
-
-// The initial application state
-// This is the same as the state argument we passed to the createStore function
-const initialState = {
- count: 0,
- name: 'Ziu'
-}
-
-// Reducer: a pure function that takes the previous state and an action, and returns the next state.
-// (previousState, action) => newState
-function reducer(state = initialState, action) {
- console.log('reducer', state, action)
-
- switch (action.type) {
- case 'INCREMENT':
- // NOTE: Keep functions pure - do not mutate the original state.
- // Desctructure the state object and return a **new object** with the updated count
- // Instead of `return state.count++`
- return {
- ...state,
- count: state.count + 1
- }
- case 'DECREMENT':
- return {
- ...state,
- count: state.count - 1
- }
- case 'CHANGE_NAME':
- return {
- ...state,
- name: action.name
- }
- default:
- return state
- }
-}
-
-const store = createStore(reducer)
-
-export default store
-```
-:::
-
-
-
-### 进一步封装
-
-可以将耦合在一起的代码拆分到不同文件中
-
-- 将`reducer`抽取出来`reducer.js`,简化`store/index.js`内容
-- 将`action.type`抽取为常量`constants.js`,使用时做导入,以保证一致性
-- 将`action`抽取出来`actionFactory.js`,用于外部dispatch时规范类型
-
-::: code-group
-```tsx [index.js]
-// store/index.js
-import { createStore } from 'redux'
-import reducer from './reducer'
-
-const store = createStore(reducer)
-
-export default store
-```
-```tsx [constants.js]
-// constants.js
-export const INCREMENT = 'INCREMENT'
-export const DECREMENT = 'DECREMENT'
-export const CHANGE_NAME = 'CHANGE_NAME'
-```
-```tsx [reducer.js]
-// reducer.js
-import * as actionType from './constants'
-
-const initialState = {
- count: 0,
- name: 'Ziu'
-}
-
-export default function reducer(state = initialState, action) {
- switch (action.type) {
- case actionType.INCREMENT:
- return {
- ...state,
- count: state.count + 1
- }
- case actionType.DECREMENT:
- return {
- ...state,
- count: state.count - 1
- }
- case actionType.CHANGE_NAME:
- return {
- ...state,
- name: action.name
- }
- default:
- return state
- }
-}
-```
-```tsx [actionFactory.js]
-// actionFactory.js
-import * as actionType from './constants'
-
-export const increment = () => ({
- type: actionType.INCREMENT
-})
-
-export const decrement = () => ({
- type: actionType.DECREMENT
-})
-
-export const changeName = (name) => ({
- type: actionType.CHANGE_NAME,
- name
-})
-```
-:::
-
-```tsx
-// App.jsx
-import React, { PureComponent } from 'react'
-import store from './store'
-import { increment, decrement, changeName } from './store/actionFactory'
-
-export default class App extends PureComponent {
- componentDidMount() {
- store.subscribe(() => this.forceUpdate())
- }
- componentWillUnmount() {
- store.unsubscribe()
- }
- render() {
- return (
-
-
App
-
Count: {store.getState().count}
-
Name: {store.getState().name}
-
store.dispatch(increment())}> +1
-
store.dispatch(decrement())}> -1
-
store.dispatch(changeName('ZIU'))}>CHANGE_NAME
-
- )
- }
-}
-```
-
-### Redux的三大原则
-
-单一数据源
-
-- 整个应用程序的状态都被存储在一棵Object Tree上
-- 且这个Object Tree只存储在一个Store中
-- 但Redux并不强制限制创建多Store,不利于数据维护
-- 单一数据源有利于整个应用程序的维护、追踪、修改
-
-State属性是只读的
-
-- 允许修改State的方法只有patch action,不要直接修改State
-- 确保了View或网络请求都不能修改State
-- 保证所有的修改都能被追踪、按照严格的顺序执行,不用担心竞态(race condition)的问题
-
-使用纯函数来执行修改
-
-- 通过reducer将旧State与新State联系在一起,并且返回一个**新的State**
-- 随着应用程序复杂程度增加,可以将reducer拆分为多个小的reducer,分别用于操作不同State Tree的某一部分
-- 所有的reducer都应该是纯函数,不能产生任何的副作用
-
-### 优化重复代码
-
-当编写了一些案例的时候会发现,React结合Redux时会编写很多重复的代码
-
-在每个需要用到Redux中状态的组件中,都需要在不同生命周期做添加订阅/解除订阅的处理,组件初始化时还要从store中取最新的状态
-
-针对重复代码的问题,可以使用之前学到的高阶组件来做优化
-
-Redux官方提供的库`react-redux`,可以让我们更方便的在React中使用Redux
-
-```bash
-npm i react-redux
-```
-
-在Profile组件中,通过高阶函数`connect`实现的
-
-将store中需要的状态通过`mapStoreToProps`转为props,并将需要使用store中状态的组件传入调用connect返回的函数中
-
-在`Profile`组件中就可以从props中获取到store中的状态
-
-::: code-group
-```tsx [App.jsx]
-// App.jsx
-import React, { PureComponent } from 'react'
-import { Provider } from 'react-redux'
-import store from './store'
-import Profile from './Profile'
-
-export default class App extends PureComponent {
- render() {
- return (
-
-
-
- )
- }
-}
-```
-```tsx [Profile.jsx]
-// Profile.jsx
-import React, { Component } from 'react'
-import { connect } from 'react-redux'
-
-// mapStateToProps is a function that
-// takes the state of the store as an argument
-// and returns an object with the data that the component needs from the store.
-// component will receive the data as props.
-const mapStateToProps = (state) => ({
- count: state.count
-})
-
-export default connect(mapStateToProps)(
- class Profile extends Component {
- render() {
- return (
-
-
Profile
-
Count: {this.props.count}
-
- )
- }
- }
-)
-```
-:::
-
-我们刚刚只是完成了对State的映射,将Store中保存的全局状态state映射到了Profile组件的props中
-
-connect还可以传入第二个参数,用于将action也映射到props中:
-
-```tsx {4,10-13,17,25,26}
-// Profile.jsx
-import React, { Component } from 'react'
-import { connect } from 'react-redux'
-import { INCREMENT, DECREMENT } from './store/constants'
-
-const mapStateToProps = (state) => ({
- count: state.count
-})
-
-const mapDispatchToProps = (dispatch) => ({
- increment: () => dispatch({ type: INCREMENT }),
- decrement: () => dispatch({ type: DECREMENT })
-})
-
-export default connect(
- mapStateToProps,
- mapDispatchToProps
-)(
- class Profile extends Component {
- render() {
- return (
-
-
Profile
-
Count: {this.props.count}
-
+1
-
-1
-
- )
- }
- }
-)
-```
-
-本质上是`connect`内部对操作进行了封装,把逻辑隐藏起来了:
-
-- 调用`connect`这个**高阶函数**,返回一个**高阶组件**
-- 为高阶组件传入映射目标组件,最后高阶组件返回一个新组件
-- 新组件的props包含了来自Store中状态/dispatch的映射
-
-### 异步Action
-
-有些场景下,我们希望组件能够直接调用Store中的action来触发网络请求,并且获取到数据
-
-但是dispatch只允许派发对象类型的Action,不能通过dispatch派发函数
-
-可以通过中间件`redux-thunk`来对Redux做增强,让dispatch能够对函数进行派发
-
-```bash
-npm i redux-thunk
-```
-
-通过`applyMiddleware`引入`redux-thunk`这个中间件:
-
-::: code-group
-```tsx [index.js] {2,3,6}
-// store/index.js
-import { createStore, applyMiddleware } from 'redux'
-import thunk from 'redux-thunk'
-import reducer from './reducer'
-
-const store = createStore(reducer, applyMiddleware(thunk))
-
-export default store
-```
-```tsx [actionFactory.js]
-// actionFactory.js
-export const fetchPostList = () => {
- return (dispatch, getState) => {
- fetch('https://jsonplaceholder.typicode.com/posts')
- .then((res) => res.json())
- .then((res) => {
- dispatch({
- type: actionType.FETCH_POST_LIST,
- list: res
- })
- })
- }
-}
-```
-```tsx [list.jsx]
-// list.jsx
-import React, { Component } from 'react'
-import { connect } from 'react-redux'
-import { fetchPostList } from './store/actionFactory'
-
-const mapStateToProps = (state) => ({
- list: state.list
-})
-
-const mapDispatchToProps = (dispatch) => ({
- fetchList: () => dispatch(fetchPostList())
-})
-
-export default connect(
- mapStateToProps,
- mapDispatchToProps
-)(
- class Profile extends Component {
- render() {
- return (
-
-
List
-
this.props.fetchList()}>Fetch List
- {this.props.list.length && (
-
- {this.props.list.map((item) => (
- {item.title}
- ))}
-
- )}
-
- )
- }
- }
-)
-```
-:::
-
-- 这样就可以将网络请求的具体逻辑代码隐藏到Redux中
-- 将网络请求归于状态管理的一部分
-- 而不是书写在组件内,不利于维护,耦合度太高
-
-`redux-thunk`是如何做到可以让我们发送异步请求的?
-
-- 默认情况下`dispatch(action)`的action必须为一个JS对象
-- `redux-thunk`允许我们传入一个函数作为`action`
-- 函数会被调用,并且将`dispatch`函数和`getState`函数作为入参传递给这个函数action
- - `dispatch` 允许我们在这之后再次派发`action`
- - `getState` 允许我们之后的一些操作依赖原来的状态,可以获取到之前的状态
-
-下图展示了从组件调用方法,触发patch到Redux接收patch、发送网络请求、更新state的全过程:
-
-
-
-### 拆分Store
-
-拆分Store带来的益处很多,便于多人协作、不同业务逻辑解耦等
-
-在Redux中,拆分Store的本质是拆分不同的`reducer`函数,之前在使用`createStore`时,传入的就是`reducer`函数
-
-之前的Store写法与用法:
-
-```tsx {8-9}
-// store/index.js
-import { createStore } from 'redux'
-import reducer from './reducer'
-
-const store = createStore(reducer)
-
-// App.jsx
-store.getState().count
-store.getState().list
-```
-
-拆分Store后的写法与用法:
-
-```tsx {7-8,14-15}
-// store/index.js
-import { createStore, combineReducers } from 'redux'
-import counterReducer from './counter'
-import postListReducer from './postList'
-
-const reducer = combineReducers({
- counter: counterReducer,
- postList: postListReducer
-})
-
-const store = createStore(reducer)
-
-// App.jsx
-store.getState().counter.count
-store.getState().postList.count
-```
-
-拆分为多个Reducer之后,需要首先`getState()`获取到整个状态树,随后指定获取到不同的模块中的状态
-
-拆分后,不同模块下的文件是保持一致的:
-
-```sh
-- store/ # Store根目录
- - index.js # 导出 store 位置
- - counter/ # Counter模块
- - actionFactory.js
- - constants.js
- - index.js # 统一导出
- - reducer.js
- - postList/ # PostList模块
- - actionFactory.js
- - constants.js
- - index.js
- - reducer.js
- - ...
-```
-
-#### combineReducer函数
-
-前面拆分Store时用到了`combineReducer`函数,将多个模块reducer组合到一起,函数内部是如何处理的?
-
-- 将传入的reducers合并到一个对象中,最终返回一个`combination`函数(相当于未拆分时传给`createStore`的`reducer`函数)
-- 在执行`combination`函数的过程中,它会通过判断前后返回的数据是否相同来决定返回之前的state还是新的state
-- 新state会触发订阅者发生对应更新,而旧state可以有效地组织订阅者发生刷新
-
-下面简单写了一下`combineReducer`的实现原理
-
-```ts
-// 使用
-combineReducer({
- counter: combineReducer,
- postList: postListReducer
-})
-
-// 创建一个新的reducer
-function reducer(state = {}, action) {
- // 返回一个对象 是Store的state
- return {
- counter: counterReducer(state.counter, action),
- postList: postListReducer(state.postList, action)
- }
-}
-```
-
-## ReduxToolkit
-
-- ReduxToolkit重构
-- ReduxToolkit异步
-- connect高阶组件
-- 中间件的实现原理
-- React状态管理选择
-
-### 认识ReduxToolkit
-
-之前在使用`createStore`创建Store时会出现deprecated标识,推荐我们使用`@reduxjs/toolkit`包中的`configureStore`函数
-
-Redux Toolkit是官方推荐编写Redux逻辑的方法
-
-- 在前面学习Redux时已经发现,Redux的逻辑编写过于繁琐、麻烦
-- 代码分拆在不同模块中,存在大量重复代码
-- Redux Toolkit旨在成为编写Redux逻辑的标准方式,从而解决上面提到的问题
-- 这个包常被称为:RTK
-
-### 使用ReduxToolkit重写Store
-
-Redux Toolkit依赖于react-redux包,所以需要同时安装这二者
-
-```bash
-npm i @reduxjs/toolkit react-redux
-```
-
-Redux Toolkit的核心API主要是下述几个:
-
-- `configureStore` 包装createStore以提供简化的配置选项和良好的默认值
- - 可以自动组合你的slice reducer 添加你提供的任何Redux中间件
- - 默认包含redux-thunk,并启用Redux DevTools Extension
-- `createSlice` 创建切片 片段
- - 接受reducer函数的对象、切片名称和初始状态值,并自动生成切片reducer,并带有actions
-- `createAsyncThunk`
- - 接受一个动作类型字符串和一个返回Promise的函数
- - 并生成一个`pending / fullfilled / rejected`基于该承诺分派动作类型的thunk
-
-写一个Demo:
-
-::: code-group
-```tsx [index.js]
-// store/index.js
-import { configureStore } from '@reduxjs/toolkit'
-import counterSlice from './features/counter'
-
-const store = configureStore({
- reducer: {
- counter: counterSlice
- }
-})
-
-export default store
-```
-```tsx [counter.js]
-// store/features/counter.js
-import { createSlice } from '@reduxjs/toolkit'
-
-const counterSlice = createSlice({
- name: 'counter',
- initialState: {
- count: 0
- },
- reducers: {
- addCount(state, action) {
- const { payload } = action
- state.count += payload
- },
- subCount(state, action) {
- const { payload } = action
- state.count -= payload
- }
- }
-})
-
-const { actions, reducer } = counterSlice
-
-export const { addCount, subCount } = actions
-
-export default reducer
-```
-```tsx [Counter.jsx]
-// Counter.jsx
-import React, { Component } from 'react'
-import { connect } from 'react-redux'
-import { addCount, subCount } from '../store/features/counter'
-
-const mapStateToProps = (state) => ({
- count: state.counter.count
-})
-
-const mapDispatchToProps = (dispatch) => ({
- increment: (count) => {
- const action = addCount(count)
- return dispatch(action)
- },
- decrement: (count) => {
- const action = subCount(count)
- return dispatch(action)
- }
-})
-
-export default connect(
- mapStateToProps,
- mapDispatchToProps
-)(
- class Counter extends Component {
- render() {
- const { count } = this.props
-
- return (
-
-
Counter
-
count: {count}
-
this.props.increment(1)}>+1
-
this.props.decrement(1)}>-1
-
- )
- }
- }
-)
-```
-:::
-
-`createSlice` 函数参数解读
-
-- `name` 标记Slice 展示在dev-tool中
-- `initialState` 初始化状态
-- `reducers` 对象 对应之前的reducer函数
-- 返回值: 一个对象 包含所有actions
-
-`configureStore` 解读
-
-- `reducer` 将slice中的reducer组成一个对象,传入此参数
-- `middleware` 额外的中间件
- - RTK已经为我们集成了`redux-thunk`和`redux-devtool`两个中间件
-- `devTools` 布尔值 是否启用开发者工具
-
-### 使用RTK执行异步dispatch
-
-实际场景中都是在组件中发起网络请求,并且将状态更新到Store中
-
-之前的开发中,我们通过`redux-thunk`这个中间件,让dispatch中可以进行异步操作
-
-ReduxToolkit默认已经给我们集成了Thunk相关的功能:`createAsyncThunk`
-
-下面我们使用RTK实现一下这个场景:在Profile中请求postList数据并保存在Store中,并展示出来
-
-::: code-group
-```tsx [postList.js] {4-8,20}
-// store/features/postList.js
-import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'
-
-export const fetchPostList = createAsyncThunk('fetch/postList', async () => {
- const url = 'https://jsonplaceholder.typicode.com/posts'
- const data = await fetch(url).then((res) => res.json())
- return data
-})
-
-const postListSlice = createSlice({
- name: 'postList',
- initialState: {
- postList: []
- },
- reducers: {
- setPostList(state, { payload }) {
- state.postList = payload
- }
- },
- extraReducers: {
- [fetchPostList.fulfilled]: (state, { payload }) => {
- console.log('payload', payload)
- state.postList = payload
- },
- [fetchPostList.pending]: (state, { payload }) => {
- console.log('fetchPostList.pending', payload)
- },
- [fetchPostList.rejected]: (state, { payload }) => {
- console.log('fetchPostList.rejected', payload)
- }
- }
-})
-
-export const { setPostList } = postListSlice.actions
-
-export default postListSlice.reducer
-```
-```tsx [index.js]
-// store/index.js
-import { configureStore } from '@reduxjs/toolkit'
-import counterReducer from './features/counter'
-import postListReducer from './features/postList'
-
-export default configureStore({
- reducer: {
- counter: counterReducer,
- postList: postListReducer
- }
-})
-```
-```tsx [Profile.jsx]
-// Profile.jsx
-import React, { Component } from 'react'
-import { connect } from 'react-redux'
-import { fetchPostList } from '../store/features/postList'
-
-const mapStateToProps = (state) => ({
- postList: state.postList.postList
-})
-
-const mapDispatchToProps = (dispatch) => ({
- fetchPostList: () => dispatch(fetchPostList())
-})
-
-export default connect(
- mapStateToProps,
- mapDispatchToProps
-)(
- class Profile extends Component {
- render() {
- return (
-
- Profile
-
this.props.fetchPostList()}>Fetch Data
-
- {this.props.postList.map((item, index) => (
- {item.title}
- ))}
-
-
- )
- }
- }
-)
-```
-:::
-
-当`createAsyncThunk`创建出来的action被dispatch时,会存在三种状态:
-
-- pending: action被发出,但是还没有最终的结果
-- fulfilled: 获取到最终的结果(有返回值的结果)
-- rejected: 执行过程中又错误或者抛出了异常
-
-我们可以在`createSlice`的`entraReducer`中监听这些结果,根据派发action后的状态添加不同的逻辑进行处理
-
-除了上述的写法,还可以为`extraReducer`传入一个函数,函数接收一个`builder`作为参数,在函数体内添加不同的case来监听异步操作的结果:
-
-```ts
-// postList.js
-...
-extraReducers: (builder) => {
- builder.addCase(fetchPostList.fulfilled, (state, { payload }) => {
- state.postList = payload
- })
- builder.addCase(fetchPostList.pending, (state, { payload }) => {
- console.log('fetchPostList.pending', payload)
- })
- builder.addCase(fetchPostList.rejected, (state, { payload }) => {
- console.log('fetchPostList.rejected', payload)
- })
-}
-...
-```
-
-在之前的代码中,我们都是通过触发action后置的回调来更新state,那么有没有可能在请求完毕时确定性地更新store中的state?
-
-可以当请求有结果了,在请求成功的回调中直接dispatch设置state的action
-
-当我们通过dispatch触发异步action时可以传递额外的参数,这些参数可以在传入createAsyncThunk的回调函数的参数中获取到,同时也可以从函数的参数中获取到`dispatch`与`getState`函数,这样就可以在请求到数据后直接通过派发action的方式更新store中的state,下面是修改后的例子:
-
-```ts {6}
-// postList.js
-import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'
-
-export const fetchPostList = createAsyncThunk(
- 'fetch/postList',
- async (extraInfo, { dispatch, getState }) => {
- const url = 'https://jsonplaceholder.typicode.com/posts'
- const data = await fetch(url).then((res) => res.json())
- dispatch(setPostList(data))
- }
-)
-
-const postListSlice = createSlice({
- name: 'postList',
- initialState: {
- postList: []
- },
- reducers: {
- setPostList(state, { payload }) {
- state.postList = payload
- }
- }
-})
-
-export const { setPostList } = postListSlice.actions
-
-export default postListSlice.reducer
-```
-
-当然,此时异步action的状态已经不那么重要了,也就不必再`return data`了,除非你需要对异常状态做额外处理,仍然可以在`extraReducers`中添加异常处理回调
-
-### Redux Toolkit的数据不可变性
-
-Redux Toolkit本质是对之前繁琐的操作进行的一次封装
-
-我们注意到:在之前reducer对state进行更新时,必须返回一个新的state才能触发修改`state = { ...state, count: count + 1 }`,但是经过Redux Toolkit的封装,我们只需要`state.count += 1`,直接对状态进行赋值就可以完成状态的更新
-
-这是因为在RTK内部使用了`immutable.js`,数据不可变性
-
-- 在React开发中,我们总是强调数据的不可变性
- - 无论是类组件中的state还是redux中管理的state
- - JS的编码过程里,数据的不可变性都是非常重要的
-- 所以在之前我们更新state时都是通过浅拷贝来完成的
- - 但是浅拷贝也存在它的缺陷:
- - 当对象过大时,进行浅拷贝会造成性能的浪费
- - 浅拷贝后的新对象,其深层属性仍然是旧对象的引用
-
-Redux Toolkit底层使用了`immerjs`库来保证数据的不可变性
-
-immutablejs库的底层原理和使用方法:[React系列十八 - Redux(四)state如何管理](https://mp.weixin.qq.com/s/hfeCDCcodBCGS5GpedxCGg)
-
-为了节约内存,出现了新的算法`Persistent Data Structure`持久化数据结构/一致性数据结构
-
-- 用一种数据结构来保存数据
-- 当数据被修改时,会返回一个新的对象,但是新的对象会尽可能复用之前的数据结构而不会对内存进行浪费
-- 比如有一棵引用层级较深的树,当我们对其深层某个节点进行修改时,不会完全拷贝整棵树,而是在尽可能复用旧树结构的同时创建一棵新的树
-
-一图胜千言:
-
-
-
-### connect的实现原理
-
-connect函数是`react-redux`提供的一个高阶函数,它返回一个高阶组件,用于将store中的state/dispatch映射为组件的props
-
-下面一步一步手写一个connect函数,实现和库提供的connect一样的映射功能:
-
-首先完成基本的代码搭建,connect函数接收两个参数`mapStateToProps` `mapDispatchToProps`返回一个高阶组件
-
-所谓高阶组件,就是传入一个类组件,返回一个增强后的新的类组件:
-
-```tsx
-// connect.js
-import { PureComponent } from 'react'
-
-export default function connect(mapStateToProps, mapDispatchToProps) {
- return (WrapperComponent) =>
- class InnerComponent extends PureComponent {
- render() {
- return
- }
- }
-}
-```
-
-其中,`mapStateToProps`和`mapDispatchToProps`都是函数,函数入参是`state`与`dispatch`,返回一个对象,键值对对应`prop <=> state/dispatch调用`
-
-我们导入store,并且从store中获取到state和dispatch传入`mapStateToProps`和`mapDispatchToProps`,随后将得到的键值对以props形式传递给WrapperComponent
-
-这样新组件就可以拿到这些状态与dispatch方法,我们可以在`componentDidMount`中监听整个store,当store中的状态发生改变时,强制执行re-render
-
-```tsx {9}
-// connect.js
-import { PureComponent } from 'react'
-import store from '../store'
-
-export default function connect(mapStateToProps, mapDispatchToProps) {
- return (WrapperComponent) =>
- class InnerComponent extends PureComponent {
- componentDidMount() {
- store.subscribe(() => this.forceUpdate())
- }
-
- render() {
- const state = mapStateToProps(store.getState())
- const dispatch = mapDispatchToProps(store.dispatch)
- return
- }
- }
-}
-```
-
-上述代码能够正常工作,但是显然每次store内state发生改变都re-render是不明智的,因为组件可能只用到了store中的某些状态
-
-那些组件没有用到的其他状态发生改变时,组件不应该也跟着re-render,这里可以做一些优化
-
-```ts {10,14-16}
-// connect.js
-import { PureComponent } from 'react'
-import store from '../store'
-
-export default function connect(mapStateToProps, mapDispatchToProps) {
- return (WrapperComponent) =>
- class InnerComponent extends PureComponent {
- constructor(props) {
- super(props)
- this.state = mapStateToProps(store.getState())
- }
-
- componentDidMount() {
- store.subscribe(() => {
- this.setState(mapStateToProps(store.getState()))
- })
- }
-
- render() {
- const state = mapStateToProps(store.getState())
- const dispatch = mapDispatchToProps(store.dispatch)
-
- return
- }
- }
-}
-```
-
-经过优化后,每次store.state发生变化会触发setState,由React内部的机制来决定组件是否应当重新渲染
-
-如果组件依赖的state发生变化了,那么React会替我们执行re-render,而不是每次都强制执行re-render
-
-进一步地,我们可以补充更多细节:
-
-- 当组件卸载时解除监听
- - `store.subscribe`会返回一个`unsubscribe`函数 用于解除监听
-- 解除与业务代码store的耦合
- - 目前的store来自业务代码 更优的做法是从context中动态获取到store
- - 应当提供一个context Provider供用户使用
- - 就像`react-redux`一样,使用connect前需要将App用Provider包裹并传入store
-
-至此就基本完成了一个connect函数
-
-::: code-group
-```tsx [connect.js]
-// connect.js
-import { PureComponent } from 'react'
-import { StoreContext } from './storeContext'
-
-export function connect(mapStateToProps, mapDispatchToProps) {
- return (WrapperComponent) => {
- class InnerComponent extends PureComponent {
- constructor(props, context) {
- super(props)
- this.state = mapStateToProps(context.getState())
- }
-
- componentDidMount() {
- this.unsubscribe = this.context.subscribe(() => {
- this.setState(mapStateToProps(this.context.getState()))
- })
- }
-
- componentWillUnmount() {
- this.unsubscribe()
- }
-
- render() {
- const state = mapStateToProps(this.context.getState())
- const dispatch = mapDispatchToProps(this.context.dispatch)
-
- return
- }
- }
-
- InnerComponent.contextType = StoreContext
-
- return InnerComponent
- }
-}
-```
-```tsx [storeContext.js]
-// storeContext.js
-import { createContext } from 'react'
-
-export const StoreContext = createContext(null)
-
-export const Provider = StoreContext.Provider
-```
-```tsx [App.jsx]
-// App.jsx
-import React, { Component } from 'react'
-import store from './store'
-import Counter from './cpns/Counter'
-import { Provider } from './hoc'
-
-export default class App extends Component {
- render() {
- return (
-
-
-
React Redux
-
-
-
- )
- }
-}
-```
-:::
-
-### 实现日志中间件logger
-
-设想现在有需求:设计一个Redux中间件,当我们每次通过dispatch派发action时都能够在控制台输出:派发了哪个action,传递的数据是怎样的
-
-最终实现:拦截dispatch,并且在控制台打印派发的action
-
-```ts
-// logger.js
-function logger(store) {
- const next = store.dispatch // 保留原始的dispatch函数
- function dispatchWithLog(action) {
- console.group(action.type)
- console.log('dispatching', action)
- const res = next.dispatch(action)
- console.log('next state', store.getState())
- console.groupEnd()
- return res
- }
- store.dispatch = dispatchWithLog
-}
-
-logger(store) // 应用中间件
-```
-
-通过`monkey patch`对原始dispatch函数进行了修改,为函数添加额外的副作用
-
-### 实现redux-thunk
-
-`redux-thunk`这个库帮我们提供了派发异步函数的功能
-
-回顾一下`redux-thunk`的功能:
-
-- 默认情况下dispatch(action)的action必须为一个JS对象
-- redux-thunk允许我们传入一个函数作为action
-- 函数会被调用,并且将dispatch函数和getState函数作为入参传递给这个函数action
- - dispatch 允许我们在这之后再次派发action
- - getState 允许我们之后的一些操作依赖原来的状态,可以获取到之前的状态
-
-```ts {7}
-// thunk.js
-function thunk(store) {
- const next = store.dispatch
- function dispatchWithThunk(action) {
- if (typeof action === 'function') {
- // pass dispatch and getState to the thunk
- return action(store.dispatch, store.getState)
- }
- return next(action)
- }
- return dispatchWithThunk
-}
-
-thunk(store) // 应用中间件
-```
-
-需要注意的是,传递给函数action的第一个参数是经过更新后的新的`dispatch`函数,这是从细节考虑:如果在函数中又派发了函数
-
-### 实现applyMiddleware
-
-当我们需要同时应用多个中间件时,可以用`applyMiddleware`来对多个中间件进行组合,统一进行注册
-
-```ts
-// applyMiddleware.js
-function applyMiddleware(store, ...fns) {
- fns.forEach(fn => fn(store))
-}
-
-applyMiddleware(store, logger, thunk) // 使用applyMiddleware
-```
-
-## React Router
-
-三大框架都有各自的路由实现
-
-- Angular ngRouter
-- React ReactRouter
-- Vue VueRouter
-
-React Router在最近两年的版本更新较快,并且在最新的React Router6发生了较大的变化
-
-- Web开发只需要安装`react-router-dom`
-- `react-router`包含一些ReactNative的内容
-
-```bash
-npm i react-router-dom
-```
-
-从`react-router-dom`中导出`BrowserRouter` 或 `HashRouter`,二者分别对应history模式与哈希模式
-
-将App用二者之一包裹,即可启用路由:
-
-```tsx {5,10,12}
-// index.js
-import React, { StrictMode } from 'react'
-import ReactDOM from 'react-dom/client'
-import App from './App'
-import { HashRouter } from 'react-router-dom'
-
-const root = ReactDOM.createRoot(document.getElementById('root'))
-root.render(
-
-
-
-
-
-)
-```
-
-路由的本质是路径与组件的映射关系(`path <==> component`)
-
-ReactRouter不像VueRouter,它的路由映射关系是书写在组件中的:
-
-下面的例子中使用到了几个**组件**:`Routes` `Route` `Navigate` `NavLink`
-
-- `Routes` `Route`用来描述路径与组件的映射关系
- - 通过为`path`和`element`传入路径和相对应的组件,将其包裹在`Routes`内即可完成路由的描述
-- `Navigate` 导航组件(在react-router5版本中是Redirect)
- - 可以帮我们完成重定向操作,将想要重定向的路径传递给组件的`to`属性
- - **当组件出现时,就会自动执行跳转**,属于功能性组件
- - 当访问根路径`/`时就会自动跳转到`/home`页
-- `NavLink`用来实现路由的跳转
- - 特殊组件,其`className` `style`这些属性都可以传递一个函数
- - 可以从函数参数中解构出`isActive`属性来动态绑定样式(实际场景应用较少)
-
-```tsx
-// App.js
-import React, { PureComponent } from 'react'
-import { Routes, Route, Navigate, NavLink } from 'react-router-dom'
-import Home from './views/Home'
-import About from './views/About'
-import NotFound from './views/NotFound'
-
-export default class App extends PureComponent {
- render() {
- return (
-
-
App
- (isActive ? 'link-active' : '')}>
- Home
-
- ({ color: isActive ? 'red' : '' })}>
- About
-
-
- }>
- }>
- }>
- }>
-
-
- )
- }
-}
-```
-
-另外,这里还有一个小技巧,在最末一个路由指定一个path为`*`的路由匹配规则,可以为路由匹配添加fallback策略,当未匹配到其之前的任何域名时,会展示NotFound页面
-
-### 嵌套路由
-
-嵌套路由可以通过在`Route`组件内部嵌套新的`Route`组件来实现
-
-再通过`Outlet`组件来指定嵌套路由的占位元素(类似于VueRouter中的router-view)
-
-我们在之前的例子的基础上,为Home页面添加两个子页面HomeRanking和HomeRecommand
-
-同时,我们也应该为Home组件添加默认跳转,就像根路径默认重定向到Home组件那样,进入到Home组件后也应该默认重定向一个子页面中,这里我们仍然使用到了Navigate组件
-
-::: code-group
-```tsx [App.jsx ]
-// App.jsx
-import React, { PureComponent } from 'react'
-import { Routes, Route, Navigate, NavLink } from 'react-router-dom'
-import Home from './views/Home'
-import HomeRanking from './views/HomeRanking'
-import HomeRecommand from './views/HomeRecommand'
-
-export default class App extends PureComponent {
- render() {
- return (
-
-
- }>
- }>
- }>
- }>
-
-
-
- )
- }
-}
-```
-```tsx [Home.jsx]
-// Home.jsx
-import React, { PureComponent } from 'react'
-import { NavLink, Outlet } from 'react-router-dom'
-
-export default class Home extends PureComponent {
- render() {
- return (
-
- )
- }
-}
-```
-:::
-
-### 编程式导航(高阶组件)
-
-之前使用的ReactRouter提供的路由跳转的组件,无论是`Link`还是`NavLink`可定制化能力都比较差,无法实现“点击按钮后跳转路由”这样的需求,那么我们就需要通过编程式导航,使用JS来完成路由的跳转
-
-ReactRouter提供了编程式导航的API:`useNavigate`
-
-自ReactRouter6起,编程式导航的API不再支持ClassComponent,全面拥抱Hooks。
-
-我们将在后续的学习中开启Hooks的写法,那么目前如何在类组件中也能使用Hooks呢?答案是高阶组件
-
-封装一个高阶组件`withRouter`,经过高阶组件处理的类组件的props将会携带router对象,上面包含一些我们需要的属性和方法:
-
-::: code-group
-```tsx [withRouter.js]
-// withRouter.js
-import { useNavigate } from 'react-router-dom'
-
-export function withRouter(WrapperComponent) {
- return (props) => {
- const navigate = useNavigate()
- const router = { navigate }
- return
- }
-}
-```
-```tsx [Home.jsx]
-// Home.jsx
-import React, { PureComponent } from 'react'
-import { Outlet } from 'react-router-dom'
-import { withRouter } from '../hoc/withRouter'
-
-export default withRouter(
- class Home extends PureComponent {
- render() {
- return (
-
-
Home
-
this.props.router.navigate('/home/ranking')}>Ranking
-
this.props.router.navigate('/home/recommand')}>Recommand
-
-
- )
- }
- }
-)
-```
-:::
-
-我们使用`withRouter`高阶组件对Home组件进行了增强,可以通过编程式导航来实现二级路由跳转
-
-这里只是展示了编程式导航的用法和高阶组件的能力,目前还是尽可能使用Hooks写法编写新项目
-
-### 动态路由(路由传参)
-
-传递参数由两种方式:
-
-- 动态路由的方式
-- 查询字符串传递参数
-
-动态路由是指:路由中的**路径**信息并不会固定
-
-- 比如匹配规则为`/detail/:id`时,`/detail/123` `detail/888`都会被匹配上,并将`123/888`作为id参数传递
-- 其中`/detail/:id`这个匹配规则被称为动态路由
-
-动态路由常见于嵌套路由跳转,比如:从歌曲列表页面点击后跳转到歌曲详情页,可以通过路由传递歌曲的ID,访问到不同歌曲的详情页
-
-我们在之前的HomeRanking榜单中加入列表和点击跳转功能,并编写一个新的组件Detail来接收来自路由的参数
-
-同样地,`react-router-dom`为我们提供了从路由获取参数的API:`useParams`,它是一个Hooks,我们将它应用到之前编写的高级组件`withRouter`中
-
-- 在使用了`withRouter`的组件中,就可以通过`this.props.router.params.xxx`获取到当前路由中传递的参数
-- 使用动态匹配路由时,传递给Route组件的`path`属性为`:xxx`,这里是`/detail/:id`
-
-::: code-group
-```tsx [withRouter.js]
-// withRouter.js
-import { useNavigate, useParams } from 'react-router-dom'
-
-export function withRouter(WrapperComponent) {
- return (props) => {
- const navigate = useNavigate()
- const params = useParams()
- const router = { navigate, params }
- return
- }
-}
-```
-```tsx [HomeRanking.jsx]
-// HomeRanking.jsx
-import React, { PureComponent } from 'react'
-import { withRouter } from '../hoc/withRouter'
-
-export default withRouter(
- class HomeRanking extends PureComponent {
- render() {
- const list = Array.from(Array(10), (x, i) => ({
- id: ++i,
- name: `Music ${i}`
- }))
- return (
-
-
HomeRanking
-
- {list.map((item, index) => (
- this.props.router.navigate(`/detail/${item.id}`)}>
- {item.name}
-
- ))}
-
-
- )
- }
- }
-)
-```
-```tsx [Detail.jsx]
-// Detail.jsx
-import React, { PureComponent } from 'react'
-import { withRouter } from '../hoc/withRouter'
-
-export default withRouter(
- class Detail extends PureComponent {
- render() {
- return (
-
-
Detail
- Current Music ID: {this.props.router.params.id}
-
- )
- }
- }
-)
-```
-:::
-
-#### 查询字符串的参数
-
-之前传递的是路径参数,那么查询字符串参数应该如何获取?
-
-可以通过`useLocation`这个Hooks拿到当前地址详细信息:
-
-```tsx
-const location = useLocation()
-location.search // ?name=ziu&age=18
-```
-
-需要自行完成数据的解析,不太方便
-
-还有一个Hooks:`useSearchParams`,可以在获取到查询字符串信息的同时帮我们解析成`URLSearchParams`对象
-
-要从`URLSearchParams`类型的对象中取值,需要通过标准方法`get`
-
-```tsx
-const [ searchParams, setSearchParams ] = useSearchParams()
-searchParams.get('name') // 'ziu'
-searchParams.get('age') // 18
-```
-
-当然,我们在实际使用中也可以通过`Object.fromEntries`将它转为普通对象,这样我们使用`useSearchParams`来对之前编写的高阶组件`withRouter`做一次增强:
-
-```tsx {8,9}
-// withRouter.js
-import { useNavigate, useParams, useSearchParams } from 'react-router-dom'
-
-export function withRouter(WrapperComponent) {
- return (props) => {
- const navigate = useNavigate()
- const params = useParams()
- const [searchParams] = useSearchParams()
- const query = Object.fromEntries(searchParams)
- const router = { navigate, params, query }
- return
- }
-}
-```
-
-::: tip
-需要注意的是,这里的`useSearchParams`是一个Hooks的常见形态
-
-它返回一个数组,数组的首位为值,数组的次位为改变值的方法
-
-与对象解构不同的是,数组结构是对位解构:保证位置一致则值一致,命名随意
-
-而对象解构恰恰相反,不必保证位置,而需要保证命名一致
-:::
-
-### 路由的配置方式
-
-至此为止,路由的配置是耦合在`App.jsx`中的,我们可以将Routes这部分代码抽离出单独的组件,也可以通过配置的方式来完成路由映射关系的编写
-
-- 在ReactRouter5版本中,我们可以将路由的映射规则写为JS对象,需要引入第三方库`react-router-config`
-- 在ReactRouter6版本中,允许我们将其写为配置文件,不需要安装其他内容
-
-6版本为我们提供了一个API:`useRoutes`,将我们编写的配置文件传入此函数,可以将其转化为之前编写的组件结构,本质上也是一种语法糖
-
-需要注意的是,Hooks只能在函数式组件中使用,这里我们将App组件改用FunctionComponent书写了
-
-::: code-group
-```tsx [index.js]
-// router/index.js
-import { Navigate } from 'react-router-dom'
-import Home from '../views/Home'
-import HomeRanking from '../views/HomeRanking'
-import HomeRecommand from '../views/HomeRecommand'
-import About from '../views/About'
-import Detail from '../views/Detail'
-import NotFound from '../views/NotFound'
-
-export const routes = [
- {
- path: '/',
- element:
- },
- {
- path: '/home',
- element: ,
- children: [
- {
- path: '',
- element:
- },
- {
- path: 'ranking',
- element:
- },
- {
- path: 'recommand',
- element:
- }
- ]
- },
- {
- path: '/about',
- element:
- },
- {
- path: '/detail/:id',
- element:
- },
- {
- path: '*',
- element:
- }
-]
-```
-```tsx [App.jsx]
-import React from 'react'
-import { NavLink, useRoutes } from 'react-router-dom'
-import { routes } from './router'
-
-export default function App() {
- return (
-
-
App
- (isActive ? 'link-active' : '')}>
- Home
-
- ({ color: isActive ? 'red' : '' })}>
- About
-
- {useRoutes(routes)}
-
- )
-}
-```
-:::
-
-### 懒加载
-
-针对某些场景的首屏优化,我们可以根据路由对代码进行分包,只有需要访问到某些页面时才从服务器请求对应的JS代码块
-
-可以使用`React.lazy(() => import( ... ))`对某些代码进行懒加载
-
-结合之前使用到的配置式路由映射规则,我们使用懒加载对代码进行分包
-
-```tsx {6,7,18,24}
-// router/index.js
-import { lazy } from 'react'
-// import HomeRecommand from '../views/HomeRecommand'
-// import About from '../views/About'
-
-const HomeRecommand = lazy(() => import('../views/HomeRecommand'))
-const About = lazy(() => import('../views/About'))
-
-export const routes = [
- ...
- {
- path: '/home',
- element: ,
- children: [
- ...
- {
- path: 'recommand',
- element:
- }
- ]
- },
- {
- path: '/about',
- element:
- },
- ...
-]
-```
-
-这时在终端执行`pnpm build`可以发现,构建产物为我们执行了分包,`About`和`HomeRecommand`这两个次级页面被打进了两个单独的包中
-
-> 在Vue中默认为我们完成了代码分包,第三方包的代码都被打包到了`vendors`中,业务代码放到了单独的JS文件中
-
-> 只有当我们访问到这些页面时,才会发起网络请求,请求这些次级页面的JS代码
-
-然而如果你在react-app的构建产物`index.html`开启本地预览服务器,会发现切换到对应页面后项目会crash(本地开发也会crash)
-
-```bash
-# 使用 serve 开启本地预览服务器
-pnpm add serve -g
-serve -s build # 将 build 作为根目录
-```
-
-这是因为React默认没有为异步组件做额外处理,我们需要使用`Suspense`组件来额外处理懒加载的组件
-
-```tsx
-// index.js
-import React, { StrictMode, Suspense } from 'react'
-import ReactDOM from 'react-dom/client'
-import App from './App'
-import { HashRouter } from 'react-router-dom'
-
-const root = ReactDOM.createRoot(document.getElementById('root'))
-root.render(
-
-
- Loading...}>
-
-
-
-
-)
-```
-
-当根组件内部有组件处于异步加载状态时,都会在页面上展示`Loading...`而不是崩溃掉
diff --git a/docs/note/React.assets/redux-async-action.svg b/docs/note/Redux.assets/redux-async-action.svg
similarity index 100%
rename from docs/note/React.assets/redux-async-action.svg
rename to docs/note/Redux.assets/redux-async-action.svg
diff --git a/docs/note/React.assets/redux-usage.svg b/docs/note/Redux.assets/redux-usage.svg
similarity index 100%
rename from docs/note/React.assets/redux-usage.svg
rename to docs/note/Redux.assets/redux-usage.svg
diff --git a/docs/note/Redux.md b/docs/note/Redux.md
new file mode 100644
index 00000000..e3bb6824
--- /dev/null
+++ b/docs/note/Redux.md
@@ -0,0 +1,1135 @@
+# Redux
+
+- Redux的核心思想
+- Redux的基本使用
+- React结合Redux
+- Redux的异步操作
+- redux-devtool
+- reducer的模块拆分
+
+## 理解JavaScript的纯函数
+
+- 函数式编程中有一个非常重要的概念 **纯函数**,JavaScript符合函数式编程的范式,所以也有纯函数的概念
+ - 在React开发中,纯函数被多次提及:
+ - React组件被要求像一个纯函数(为什么是像,因为还有类组件)
+ - Redux中有一个reducer的概念,同样是要求必须是一个纯函数
+- 掌握纯函数对于理解很多框架的设计都是有帮助的
+
+一个纯函数必然具备以下特征:
+
+- 确定的输入一定产生确定的输出
+- 函数的执行过程中,不能产生副作用
+
+
+## 为什么需要Redux
+
+- JS需要管理的状态越来越多,越来越复杂
+- 状态不断发生变化之间又相互依赖,这要求视图层也能同步更新
+- React提供了自动更新视图的方法,但状态仍需要手动管理
+- Redux可以帮我们管理状态,提供了**可预测的状态管理**
+- 框架无关,体积只有2KB大小
+
+## Redux的核心理念
+
+Redux的核心理念 Store
+
+- 定义一个统一的规范来操作数据,这样就可以做到对数据的跟踪
+- `list.push()` `list[0].age = 18`
+
+Redux的核心理念 Action
+
+- Redux要求:要修改数据,必须通过Action来修改
+- 所有数据的变化,必须通过派发(Patch)Action来更新
+- Action是一个普通的JS对象,用来描述此次更新的type与content
+- `const action = { type: 'ADD_ITEM', item: { name: 'Ziu', age: 18 } }`
+
+Redux的核心理念 Reducer
+
+- 如何将Store和Action联系在一起?
+- reducer是一个纯函数
+- 完成的工作就是:将传入的state和action结合起来,生成一个新的state
+- `patch` => `reducer` => `newState` => `Store`
+
+## Redux Demo
+
+下例中,通过`createStore`创建了一个Store(已经不推荐了)
+
+- initialState用于在调用`createStore`时作为默认值传入`reducer`
+- 后续每次`store.dispatch`都会调用`reducer`
+- 通过`reducer`更新state中的数据
+
+在React中,可以通过`store.subscribe`注册State变化的监听回调
+
+- 当state发生变化时,通过调用`this.forceUpdate`触发组件的更新
+- 一般情况下,我们在`componentDidMount`注册监听回调,在`componentWillUnmount`解除监听
+
+::: code-group
+```tsx [App.jsx]
+// App.jsx
+import React, { PureComponent } from 'react'
+import store from './store'
+
+export default class App extends PureComponent {
+ componentDidMount() {
+ // Subscribe to the store
+ store.subscribe(() => {
+ console.log('subscribe', store.getState())
+ this.forceUpdate()
+ })
+ }
+
+ componentWillUnmount() {
+ store.unsubscribe()
+ }
+
+ render() {
+ return (
+
+
App
+
Count: {store.getState().count}
+
Name: {store.getState().name}
+
store.dispatch({ type: 'INCREMENT' })}> +1
+
store.dispatch({ type: 'DECREMENT' })}> -1
+
store.dispatch({ type: 'CHANGE_NAME', name: 'ZIU' })}>
+ {' '}
+ CHANGE_NAME{' '}
+
+
+ )
+ }
+}
+```
+```tsx [index.js]
+// store/index.js
+import { createStore } from 'redux'
+
+// The initial application state
+// This is the same as the state argument we passed to the createStore function
+const initialState = {
+ count: 0,
+ name: 'Ziu'
+}
+
+// Reducer: a pure function that takes the previous state and an action, and returns the next state.
+// (previousState, action) => newState
+function reducer(state = initialState, action) {
+ console.log('reducer', state, action)
+
+ switch (action.type) {
+ case 'INCREMENT':
+ // NOTE: Keep functions pure - do not mutate the original state.
+ // Desctructure the state object and return a **new object** with the updated count
+ // Instead of `return state.count++`
+ return {
+ ...state,
+ count: state.count + 1
+ }
+ case 'DECREMENT':
+ return {
+ ...state,
+ count: state.count - 1
+ }
+ case 'CHANGE_NAME':
+ return {
+ ...state,
+ name: action.name
+ }
+ default:
+ return state
+ }
+}
+
+const store = createStore(reducer)
+
+export default store
+```
+:::
+
+
+
+## 进一步封装
+
+可以将耦合在一起的代码拆分到不同文件中
+
+- 将`reducer`抽取出来`reducer.js`,简化`store/index.js`内容
+- 将`action.type`抽取为常量`constants.js`,使用时做导入,以保证一致性
+- 将`action`抽取出来`actionFactory.js`,用于外部dispatch时规范类型
+
+::: code-group
+```tsx [index.js]
+// store/index.js
+import { createStore } from 'redux'
+import reducer from './reducer'
+
+const store = createStore(reducer)
+
+export default store
+```
+```tsx [constants.js]
+// constants.js
+export const INCREMENT = 'INCREMENT'
+export const DECREMENT = 'DECREMENT'
+export const CHANGE_NAME = 'CHANGE_NAME'
+```
+```tsx [reducer.js]
+// reducer.js
+import * as actionType from './constants'
+
+const initialState = {
+ count: 0,
+ name: 'Ziu'
+}
+
+export default function reducer(state = initialState, action) {
+ switch (action.type) {
+ case actionType.INCREMENT:
+ return {
+ ...state,
+ count: state.count + 1
+ }
+ case actionType.DECREMENT:
+ return {
+ ...state,
+ count: state.count - 1
+ }
+ case actionType.CHANGE_NAME:
+ return {
+ ...state,
+ name: action.name
+ }
+ default:
+ return state
+ }
+}
+```
+```tsx [actionFactory.js]
+// actionFactory.js
+import * as actionType from './constants'
+
+export const increment = () => ({
+ type: actionType.INCREMENT
+})
+
+export const decrement = () => ({
+ type: actionType.DECREMENT
+})
+
+export const changeName = (name) => ({
+ type: actionType.CHANGE_NAME,
+ name
+})
+```
+:::
+
+```tsx
+// App.jsx
+import React, { PureComponent } from 'react'
+import store from './store'
+import { increment, decrement, changeName } from './store/actionFactory'
+
+export default class App extends PureComponent {
+ componentDidMount() {
+ store.subscribe(() => this.forceUpdate())
+ }
+ componentWillUnmount() {
+ store.unsubscribe()
+ }
+ render() {
+ return (
+
+
App
+
Count: {store.getState().count}
+
Name: {store.getState().name}
+
store.dispatch(increment())}> +1
+
store.dispatch(decrement())}> -1
+
store.dispatch(changeName('ZIU'))}>CHANGE_NAME
+
+ )
+ }
+}
+```
+
+## Redux的三大原则
+
+单一数据源
+
+- 整个应用程序的状态都被存储在一棵Object Tree上
+- 且这个Object Tree只存储在一个Store中
+- 但Redux并不强制限制创建多Store,不利于数据维护
+- 单一数据源有利于整个应用程序的维护、追踪、修改
+
+State属性是只读的
+
+- 允许修改State的方法只有patch action,不要直接修改State
+- 确保了View或网络请求都不能修改State
+- 保证所有的修改都能被追踪、按照严格的顺序执行,不用担心竞态(race condition)的问题
+
+使用纯函数来执行修改
+
+- 通过reducer将旧State与新State联系在一起,并且返回一个**新的State**
+- 随着应用程序复杂程度增加,可以将reducer拆分为多个小的reducer,分别用于操作不同State Tree的某一部分
+- 所有的reducer都应该是纯函数,不能产生任何的副作用
+
+## 优化重复代码
+
+当编写了一些案例的时候会发现,React结合Redux时会编写很多重复的代码
+
+在每个需要用到Redux中状态的组件中,都需要在不同生命周期做添加订阅/解除订阅的处理,组件初始化时还要从store中取最新的状态
+
+针对重复代码的问题,可以使用之前学到的高阶组件来做优化
+
+Redux官方提供的库`react-redux`,可以让我们更方便的在React中使用Redux
+
+```bash
+npm i react-redux
+```
+
+在Profile组件中,通过高阶函数`connect`实现的
+
+将store中需要的状态通过`mapStoreToProps`转为props,并将需要使用store中状态的组件传入调用connect返回的函数中
+
+在`Profile`组件中就可以从props中获取到store中的状态
+
+::: code-group
+```tsx [App.jsx]
+// App.jsx
+import React, { PureComponent } from 'react'
+import { Provider } from 'react-redux'
+import store from './store'
+import Profile from './Profile'
+
+export default class App extends PureComponent {
+ render() {
+ return (
+
+
+
+ )
+ }
+}
+```
+```tsx [Profile.jsx]
+// Profile.jsx
+import React, { Component } from 'react'
+import { connect } from 'react-redux'
+
+// mapStateToProps is a function that
+// takes the state of the store as an argument
+// and returns an object with the data that the component needs from the store.
+// component will receive the data as props.
+const mapStateToProps = (state) => ({
+ count: state.count
+})
+
+export default connect(mapStateToProps)(
+ class Profile extends Component {
+ render() {
+ return (
+
+
Profile
+
Count: {this.props.count}
+
+ )
+ }
+ }
+)
+```
+:::
+
+我们刚刚只是完成了对State的映射,将Store中保存的全局状态state映射到了Profile组件的props中
+
+connect还可以传入第二个参数,用于将action也映射到props中:
+
+```tsx {4,10-13,17,25,26}
+// Profile.jsx
+import React, { Component } from 'react'
+import { connect } from 'react-redux'
+import { INCREMENT, DECREMENT } from './store/constants'
+
+const mapStateToProps = (state) => ({
+ count: state.count
+})
+
+const mapDispatchToProps = (dispatch) => ({
+ increment: () => dispatch({ type: INCREMENT }),
+ decrement: () => dispatch({ type: DECREMENT })
+})
+
+export default connect(
+ mapStateToProps,
+ mapDispatchToProps
+)(
+ class Profile extends Component {
+ render() {
+ return (
+
+
Profile
+
Count: {this.props.count}
+
+1
+
-1
+
+ )
+ }
+ }
+)
+```
+
+本质上是`connect`内部对操作进行了封装,把逻辑隐藏起来了:
+
+- 调用`connect`这个**高阶函数**,返回一个**高阶组件**
+- 为高阶组件传入映射目标组件,最后高阶组件返回一个新组件
+- 新组件的props包含了来自Store中状态/dispatch的映射
+
+## 异步Action
+
+有些场景下,我们希望组件能够直接调用Store中的action来触发网络请求,并且获取到数据
+
+但是dispatch只允许派发对象类型的Action,不能通过dispatch派发函数
+
+可以通过中间件`redux-thunk`来对Redux做增强,让dispatch能够对函数进行派发
+
+```bash
+npm i redux-thunk
+```
+
+通过`applyMiddleware`引入`redux-thunk`这个中间件:
+
+::: code-group
+```tsx [index.js] {2,3,6}
+// store/index.js
+import { createStore, applyMiddleware } from 'redux'
+import thunk from 'redux-thunk'
+import reducer from './reducer'
+
+const store = createStore(reducer, applyMiddleware(thunk))
+
+export default store
+```
+```tsx [actionFactory.js]
+// actionFactory.js
+export const fetchPostList = () => {
+ return (dispatch, getState) => {
+ fetch('https://jsonplaceholder.typicode.com/posts')
+ .then((res) => res.json())
+ .then((res) => {
+ dispatch({
+ type: actionType.FETCH_POST_LIST,
+ list: res
+ })
+ })
+ }
+}
+```
+```tsx [list.jsx]
+// list.jsx
+import React, { Component } from 'react'
+import { connect } from 'react-redux'
+import { fetchPostList } from './store/actionFactory'
+
+const mapStateToProps = (state) => ({
+ list: state.list
+})
+
+const mapDispatchToProps = (dispatch) => ({
+ fetchList: () => dispatch(fetchPostList())
+})
+
+export default connect(
+ mapStateToProps,
+ mapDispatchToProps
+)(
+ class Profile extends Component {
+ render() {
+ return (
+
+
List
+
this.props.fetchList()}>Fetch List
+ {this.props.list.length && (
+
+ {this.props.list.map((item) => (
+ {item.title}
+ ))}
+
+ )}
+
+ )
+ }
+ }
+)
+```
+:::
+
+- 这样就可以将网络请求的具体逻辑代码隐藏到Redux中
+- 将网络请求归于状态管理的一部分
+- 而不是书写在组件内,不利于维护,耦合度太高
+
+`redux-thunk`是如何做到可以让我们发送异步请求的?
+
+- 默认情况下`dispatch(action)`的action必须为一个JS对象
+- `redux-thunk`允许我们传入一个函数作为`action`
+- 函数会被调用,并且将`dispatch`函数和`getState`函数作为入参传递给这个函数action
+ - `dispatch` 允许我们在这之后再次派发`action`
+ - `getState` 允许我们之后的一些操作依赖原来的状态,可以获取到之前的状态
+
+下图展示了从组件调用方法,触发patch到Redux接收patch、发送网络请求、更新state的全过程:
+
+
+
+## 拆分Store
+
+拆分Store带来的益处很多,便于多人协作、不同业务逻辑解耦等
+
+在Redux中,拆分Store的本质是拆分不同的`reducer`函数,之前在使用`createStore`时,传入的就是`reducer`函数
+
+之前的Store写法与用法:
+
+```tsx {8-9}
+// store/index.js
+import { createStore } from 'redux'
+import reducer from './reducer'
+
+const store = createStore(reducer)
+
+// App.jsx
+store.getState().count
+store.getState().list
+```
+
+拆分Store后的写法与用法:
+
+```tsx {7-8,14-15}
+// store/index.js
+import { createStore, combineReducers } from 'redux'
+import counterReducer from './counter'
+import postListReducer from './postList'
+
+const reducer = combineReducers({
+ counter: counterReducer,
+ postList: postListReducer
+})
+
+const store = createStore(reducer)
+
+// App.jsx
+store.getState().counter.count
+store.getState().postList.count
+```
+
+拆分为多个Reducer之后,需要首先`getState()`获取到整个状态树,随后指定获取到不同的模块中的状态
+
+拆分后,不同模块下的文件是保持一致的:
+
+```sh
+- store/ # Store根目录
+ - index.js # 导出 store 位置
+ - counter/ # Counter模块
+ - actionFactory.js
+ - constants.js
+ - index.js # 统一导出
+ - reducer.js
+ - postList/ # PostList模块
+ - actionFactory.js
+ - constants.js
+ - index.js
+ - reducer.js
+ - ...
+```
+
+### combineReducer函数
+
+前面拆分Store时用到了`combineReducer`函数,将多个模块reducer组合到一起,函数内部是如何处理的?
+
+- 将传入的reducers合并到一个对象中,最终返回一个`combination`函数(相当于未拆分时传给`createStore`的`reducer`函数)
+- 在执行`combination`函数的过程中,它会通过判断前后返回的数据是否相同来决定返回之前的state还是新的state
+- 新state会触发订阅者发生对应更新,而旧state可以有效地组织订阅者发生刷新
+
+下面简单写了一下`combineReducer`的实现原理
+
+```ts
+// 使用
+combineReducer({
+ counter: combineReducer,
+ postList: postListReducer
+})
+
+// 创建一个新的reducer
+function reducer(state = {}, action) {
+ // 返回一个对象 是Store的state
+ return {
+ counter: counterReducer(state.counter, action),
+ postList: postListReducer(state.postList, action)
+ }
+}
+```
+
+## ReduxToolkit
+
+- ReduxToolkit重构
+- ReduxToolkit异步
+- connect高阶组件
+- 中间件的实现原理
+- React状态管理选择
+
+## 认识ReduxToolkit
+
+之前在使用`createStore`创建Store时会出现deprecated标识,推荐我们使用`@reduxjs/toolkit`包中的`configureStore`函数
+
+Redux Toolkit是官方推荐编写Redux逻辑的方法
+
+- 在前面学习Redux时已经发现,Redux的逻辑编写过于繁琐、麻烦
+- 代码分拆在不同模块中,存在大量重复代码
+- Redux Toolkit旨在成为编写Redux逻辑的标准方式,从而解决上面提到的问题
+- 这个包常被称为:RTK
+
+## 使用ReduxToolkit重写Store
+
+Redux Toolkit依赖于react-redux包,所以需要同时安装这二者
+
+```bash
+npm i @reduxjs/toolkit react-redux
+```
+
+Redux Toolkit的核心API主要是下述几个:
+
+- `configureStore` 包装createStore以提供简化的配置选项和良好的默认值
+ - 可以自动组合你的slice reducer 添加你提供的任何Redux中间件
+ - 默认包含redux-thunk,并启用Redux DevTools Extension
+- `createSlice` 创建切片 片段
+ - 接受reducer函数的对象、切片名称和初始状态值,并自动生成切片reducer,并带有actions
+- `createAsyncThunk`
+ - 接受一个动作类型字符串和一个返回Promise的函数
+ - 并生成一个`pending / fullfilled / rejected`基于该承诺分派动作类型的thunk
+
+写一个Demo:
+
+::: code-group
+```tsx [index.js]
+// store/index.js
+import { configureStore } from '@reduxjs/toolkit'
+import counterSlice from './features/counter'
+
+const store = configureStore({
+ reducer: {
+ counter: counterSlice
+ }
+})
+
+export default store
+```
+```tsx [counter.js]
+// store/features/counter.js
+import { createSlice } from '@reduxjs/toolkit'
+
+const counterSlice = createSlice({
+ name: 'counter',
+ initialState: {
+ count: 0
+ },
+ reducers: {
+ addCount(state, action) {
+ const { payload } = action
+ state.count += payload
+ },
+ subCount(state, action) {
+ const { payload } = action
+ state.count -= payload
+ }
+ }
+})
+
+const { actions, reducer } = counterSlice
+
+export const { addCount, subCount } = actions
+
+export default reducer
+```
+```tsx [Counter.jsx]
+// Counter.jsx
+import React, { Component } from 'react'
+import { connect } from 'react-redux'
+import { addCount, subCount } from '../store/features/counter'
+
+const mapStateToProps = (state) => ({
+ count: state.counter.count
+})
+
+const mapDispatchToProps = (dispatch) => ({
+ increment: (count) => {
+ const action = addCount(count)
+ return dispatch(action)
+ },
+ decrement: (count) => {
+ const action = subCount(count)
+ return dispatch(action)
+ }
+})
+
+export default connect(
+ mapStateToProps,
+ mapDispatchToProps
+)(
+ class Counter extends Component {
+ render() {
+ const { count } = this.props
+
+ return (
+
+
Counter
+
count: {count}
+
this.props.increment(1)}>+1
+
this.props.decrement(1)}>-1
+
+ )
+ }
+ }
+)
+```
+:::
+
+`createSlice` 函数参数解读
+
+- `name` 标记Slice 展示在dev-tool中
+- `initialState` 初始化状态
+- `reducers` 对象 对应之前的reducer函数
+- 返回值: 一个对象 包含所有actions
+
+`configureStore` 解读
+
+- `reducer` 将slice中的reducer组成一个对象,传入此参数
+- `middleware` 额外的中间件
+ - RTK已经为我们集成了`redux-thunk`和`redux-devtool`两个中间件
+- `devTools` 布尔值 是否启用开发者工具
+
+## 使用RTK执行异步dispatch
+
+实际场景中都是在组件中发起网络请求,并且将状态更新到Store中
+
+之前的开发中,我们通过`redux-thunk`这个中间件,让dispatch中可以进行异步操作
+
+ReduxToolkit默认已经给我们集成了Thunk相关的功能:`createAsyncThunk`
+
+下面我们使用RTK实现一下这个场景:在Profile中请求postList数据并保存在Store中,并展示出来
+
+::: code-group
+```tsx [postList.js] {4-8,20}
+// store/features/postList.js
+import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'
+
+export const fetchPostList = createAsyncThunk('fetch/postList', async () => {
+ const url = 'https://jsonplaceholder.typicode.com/posts'
+ const data = await fetch(url).then((res) => res.json())
+ return data
+})
+
+const postListSlice = createSlice({
+ name: 'postList',
+ initialState: {
+ postList: []
+ },
+ reducers: {
+ setPostList(state, { payload }) {
+ state.postList = payload
+ }
+ },
+ extraReducers: {
+ [fetchPostList.fulfilled]: (state, { payload }) => {
+ console.log('payload', payload)
+ state.postList = payload
+ },
+ [fetchPostList.pending]: (state, { payload }) => {
+ console.log('fetchPostList.pending', payload)
+ },
+ [fetchPostList.rejected]: (state, { payload }) => {
+ console.log('fetchPostList.rejected', payload)
+ }
+ }
+})
+
+export const { setPostList } = postListSlice.actions
+
+export default postListSlice.reducer
+```
+```tsx [index.js]
+// store/index.js
+import { configureStore } from '@reduxjs/toolkit'
+import counterReducer from './features/counter'
+import postListReducer from './features/postList'
+
+export default configureStore({
+ reducer: {
+ counter: counterReducer,
+ postList: postListReducer
+ }
+})
+```
+```tsx [Profile.jsx]
+// Profile.jsx
+import React, { Component } from 'react'
+import { connect } from 'react-redux'
+import { fetchPostList } from '../store/features/postList'
+
+const mapStateToProps = (state) => ({
+ postList: state.postList.postList
+})
+
+const mapDispatchToProps = (dispatch) => ({
+ fetchPostList: () => dispatch(fetchPostList())
+})
+
+export default connect(
+ mapStateToProps,
+ mapDispatchToProps
+)(
+ class Profile extends Component {
+ render() {
+ return (
+
+ Profile
+
this.props.fetchPostList()}>Fetch Data
+
+ {this.props.postList.map((item, index) => (
+ {item.title}
+ ))}
+
+
+ )
+ }
+ }
+)
+```
+:::
+
+当`createAsyncThunk`创建出来的action被dispatch时,会存在三种状态:
+
+- pending: action被发出,但是还没有最终的结果
+- fulfilled: 获取到最终的结果(有返回值的结果)
+- rejected: 执行过程中又错误或者抛出了异常
+
+我们可以在`createSlice`的`entraReducer`中监听这些结果,根据派发action后的状态添加不同的逻辑进行处理
+
+除了上述的写法,还可以为`extraReducer`传入一个函数,函数接收一个`builder`作为参数,在函数体内添加不同的case来监听异步操作的结果:
+
+```ts
+// postList.js
+...
+extraReducers: (builder) => {
+ builder.addCase(fetchPostList.fulfilled, (state, { payload }) => {
+ state.postList = payload
+ })
+ builder.addCase(fetchPostList.pending, (state, { payload }) => {
+ console.log('fetchPostList.pending', payload)
+ })
+ builder.addCase(fetchPostList.rejected, (state, { payload }) => {
+ console.log('fetchPostList.rejected', payload)
+ })
+}
+...
+```
+
+在之前的代码中,我们都是通过触发action后置的回调来更新state,那么有没有可能在请求完毕时确定性地更新store中的state?
+
+可以当请求有结果了,在请求成功的回调中直接dispatch设置state的action
+
+当我们通过dispatch触发异步action时可以传递额外的参数,这些参数可以在传入createAsyncThunk的回调函数的参数中获取到,同时也可以从函数的参数中获取到`dispatch`与`getState`函数,这样就可以在请求到数据后直接通过派发action的方式更新store中的state,下面是修改后的例子:
+
+```ts {6}
+// postList.js
+import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'
+
+export const fetchPostList = createAsyncThunk(
+ 'fetch/postList',
+ async (extraInfo, { dispatch, getState }) => {
+ const url = 'https://jsonplaceholder.typicode.com/posts'
+ const data = await fetch(url).then((res) => res.json())
+ dispatch(setPostList(data))
+ }
+)
+
+const postListSlice = createSlice({
+ name: 'postList',
+ initialState: {
+ postList: []
+ },
+ reducers: {
+ setPostList(state, { payload }) {
+ state.postList = payload
+ }
+ }
+})
+
+export const { setPostList } = postListSlice.actions
+
+export default postListSlice.reducer
+```
+
+当然,此时异步action的状态已经不那么重要了,也就不必再`return data`了,除非你需要对异常状态做额外处理,仍然可以在`extraReducers`中添加异常处理回调
+
+## Redux Toolkit的数据不可变性
+
+Redux Toolkit本质是对之前繁琐的操作进行的一次封装
+
+我们注意到:在之前reducer对state进行更新时,必须返回一个新的state才能触发修改`state = { ...state, count: count + 1 }`,但是经过Redux Toolkit的封装,我们只需要`state.count += 1`,直接对状态进行赋值就可以完成状态的更新
+
+这是因为在RTK内部使用了`immutable.js`,数据不可变性
+
+- 在React开发中,我们总是强调数据的不可变性
+ - 无论是类组件中的state还是redux中管理的state
+ - JS的编码过程里,数据的不可变性都是非常重要的
+- 所以在之前我们更新state时都是通过浅拷贝来完成的
+ - 但是浅拷贝也存在它的缺陷:
+ - 当对象过大时,进行浅拷贝会造成性能的浪费
+ - 浅拷贝后的新对象,其深层属性仍然是旧对象的引用
+
+Redux Toolkit底层使用了`immerjs`库来保证数据的不可变性
+
+immutablejs库的底层原理和使用方法:[React系列十八 - Redux(四)state如何管理](https://mp.weixin.qq.com/s/hfeCDCcodBCGS5GpedxCGg)
+
+为了节约内存,出现了新的算法`Persistent Data Structure`持久化数据结构/一致性数据结构
+
+- 用一种数据结构来保存数据
+- 当数据被修改时,会返回一个新的对象,但是新的对象会尽可能复用之前的数据结构而不会对内存进行浪费
+- 比如有一棵引用层级较深的树,当我们对其深层某个节点进行修改时,不会完全拷贝整棵树,而是在尽可能复用旧树结构的同时创建一棵新的树
+
+一图胜千言:
+
+
+
+## connect的实现原理
+
+connect函数是`react-redux`提供的一个高阶函数,它返回一个高阶组件,用于将store中的state/dispatch映射为组件的props
+
+下面一步一步手写一个connect函数,实现和库提供的connect一样的映射功能:
+
+首先完成基本的代码搭建,connect函数接收两个参数`mapStateToProps` `mapDispatchToProps`返回一个高阶组件
+
+所谓高阶组件,就是传入一个类组件,返回一个增强后的新的类组件:
+
+```tsx
+// connect.js
+import { PureComponent } from 'react'
+
+export default function connect(mapStateToProps, mapDispatchToProps) {
+ return (WrapperComponent) =>
+ class InnerComponent extends PureComponent {
+ render() {
+ return
+ }
+ }
+}
+```
+
+其中,`mapStateToProps`和`mapDispatchToProps`都是函数,函数入参是`state`与`dispatch`,返回一个对象,键值对对应`prop <=> state/dispatch调用`
+
+我们导入store,并且从store中获取到state和dispatch传入`mapStateToProps`和`mapDispatchToProps`,随后将得到的键值对以props形式传递给WrapperComponent
+
+这样新组件就可以拿到这些状态与dispatch方法,我们可以在`componentDidMount`中监听整个store,当store中的状态发生改变时,强制执行re-render
+
+```tsx {9}
+// connect.js
+import { PureComponent } from 'react'
+import store from '../store'
+
+export default function connect(mapStateToProps, mapDispatchToProps) {
+ return (WrapperComponent) =>
+ class InnerComponent extends PureComponent {
+ componentDidMount() {
+ store.subscribe(() => this.forceUpdate())
+ }
+
+ render() {
+ const state = mapStateToProps(store.getState())
+ const dispatch = mapDispatchToProps(store.dispatch)
+ return
+ }
+ }
+}
+```
+
+上述代码能够正常工作,但是显然每次store内state发生改变都re-render是不明智的,因为组件可能只用到了store中的某些状态
+
+那些组件没有用到的其他状态发生改变时,组件不应该也跟着re-render,这里可以做一些优化
+
+```ts {10,14-16}
+// connect.js
+import { PureComponent } from 'react'
+import store from '../store'
+
+export default function connect(mapStateToProps, mapDispatchToProps) {
+ return (WrapperComponent) =>
+ class InnerComponent extends PureComponent {
+ constructor(props) {
+ super(props)
+ this.state = mapStateToProps(store.getState())
+ }
+
+ componentDidMount() {
+ store.subscribe(() => {
+ this.setState(mapStateToProps(store.getState()))
+ })
+ }
+
+ render() {
+ const state = mapStateToProps(store.getState())
+ const dispatch = mapDispatchToProps(store.dispatch)
+
+ return
+ }
+ }
+}
+```
+
+经过优化后,每次store.state发生变化会触发setState,由React内部的机制来决定组件是否应当重新渲染
+
+如果组件依赖的state发生变化了,那么React会替我们执行re-render,而不是每次都强制执行re-render
+
+进一步地,我们可以补充更多细节:
+
+- 当组件卸载时解除监听
+ - `store.subscribe`会返回一个`unsubscribe`函数 用于解除监听
+- 解除与业务代码store的耦合
+ - 目前的store来自业务代码 更优的做法是从context中动态获取到store
+ - 应当提供一个context Provider供用户使用
+ - 就像`react-redux`一样,使用connect前需要将App用Provider包裹并传入store
+
+至此就基本完成了一个connect函数
+
+::: code-group
+```tsx [connect.js]
+// connect.js
+import { PureComponent } from 'react'
+import { StoreContext } from './storeContext'
+
+export function connect(mapStateToProps, mapDispatchToProps) {
+ return (WrapperComponent) => {
+ class InnerComponent extends PureComponent {
+ constructor(props, context) {
+ super(props)
+ this.state = mapStateToProps(context.getState())
+ }
+
+ componentDidMount() {
+ this.unsubscribe = this.context.subscribe(() => {
+ this.setState(mapStateToProps(this.context.getState()))
+ })
+ }
+
+ componentWillUnmount() {
+ this.unsubscribe()
+ }
+
+ render() {
+ const state = mapStateToProps(this.context.getState())
+ const dispatch = mapDispatchToProps(this.context.dispatch)
+
+ return
+ }
+ }
+
+ InnerComponent.contextType = StoreContext
+
+ return InnerComponent
+ }
+}
+```
+```tsx [storeContext.js]
+// storeContext.js
+import { createContext } from 'react'
+
+export const StoreContext = createContext(null)
+
+export const Provider = StoreContext.Provider
+```
+```tsx [App.jsx]
+// App.jsx
+import React, { Component } from 'react'
+import store from './store'
+import Counter from './cpns/Counter'
+import { Provider } from './hoc'
+
+export default class App extends Component {
+ render() {
+ return (
+
+
+
React Redux
+
+
+
+ )
+ }
+}
+```
+:::
+
+## 实现日志中间件logger
+
+设想现在有需求:设计一个Redux中间件,当我们每次通过dispatch派发action时都能够在控制台输出:派发了哪个action,传递的数据是怎样的
+
+最终实现:拦截dispatch,并且在控制台打印派发的action
+
+```ts
+// logger.js
+function logger(store) {
+ const next = store.dispatch // 保留原始的dispatch函数
+ function dispatchWithLog(action) {
+ console.group(action.type)
+ console.log('dispatching', action)
+ const res = next.dispatch(action)
+ console.log('next state', store.getState())
+ console.groupEnd()
+ return res
+ }
+ store.dispatch = dispatchWithLog
+}
+
+logger(store) // 应用中间件
+```
+
+通过`monkey patch`对原始dispatch函数进行了修改,为函数添加额外的副作用
+
+## 实现redux-thunk
+
+`redux-thunk`这个库帮我们提供了派发异步函数的功能
+
+回顾一下`redux-thunk`的功能:
+
+- 默认情况下dispatch(action)的action必须为一个JS对象
+- redux-thunk允许我们传入一个函数作为action
+- 函数会被调用,并且将dispatch函数和getState函数作为入参传递给这个函数action
+ - dispatch 允许我们在这之后再次派发action
+ - getState 允许我们之后的一些操作依赖原来的状态,可以获取到之前的状态
+
+```ts {7}
+// thunk.js
+function thunk(store) {
+ const next = store.dispatch
+ function dispatchWithThunk(action) {
+ if (typeof action === 'function') {
+ // pass dispatch and getState to the thunk
+ return action(store.dispatch, store.getState)
+ }
+ return next(action)
+ }
+ return dispatchWithThunk
+}
+
+thunk(store) // 应用中间件
+```
+
+需要注意的是,传递给函数action的第一个参数是经过更新后的新的`dispatch`函数,这是从细节考虑:如果在函数中又派发了函数
+
+## 实现applyMiddleware
+
+当我们需要同时应用多个中间件时,可以用`applyMiddleware`来对多个中间件进行组合,统一进行注册
+
+```ts
+// applyMiddleware.js
+function applyMiddleware(store, ...fns) {
+ fns.forEach(fn => fn(store))
+}
+
+applyMiddleware(store, logger, thunk) // 使用applyMiddleware
+```