主题
TypeScript 介绍(Day10 1/2)
本页对应 React + TypeScript + 医通项目,共 7 段视频,约 1 小时 33 分。
学习目标
- 能够独立完成 Redux Toolkit 基础配置:创建 slice、定义同步 reducer、配置 store,并通过 Provider、useSelector、useDispatch 实现组件与仓库的数据读写。
- 能够使用 createAsyncThunk 创建异步 action creator,并在 extraReducers 中处理 pending、fulfilled、rejected 三种状态,正确读取 action.payload 和 action.error.message。
- 能够概括 Redux Toolkit 中 slice、store、Provider、同步/异步 action creator 及 reducer 的职责与暴露方式,并说明异步代码应写在何处。
- 能够使用 useCallback 缓存函数,并通过依赖项控制缓存函数的更新时机,解释其解决重复创建函数的作用。
- 能够使用 PureComponent 和 useMemo 分别优化类组件渲染与昂贵计算结果缓存,并说明二者在何种数据变化时触发更新或重新计算。
- 能够使用 useReducer 对函数组件内部状态进行集中管理,编写 reducer、定义 action.type 分支,并通过 dispatch 触发状态更新。
知识点
先看整体:Redux Toolkit 的同步链路
为什么需要:项目里多个组件要共享状态时,用 useState 一层层传 props 很麻烦,Redux Toolkit 把状态放进 store,组件通过 useSelector 取、通过 dispatch 改。转写里的“starstar 目录”应理解为 store 目录,“框/夸”按示例中的 count 理解。
怎么想:先建切片 slice,切片里放 initialState 和同步 reducers;再建仓库 store,把切片的 reducer 注册进去;最后用 Provider 包住根组件。
怎么做:
ts
// store/slice/countSlice.ts
import { createSlice } from '@reduxjs/toolkit'
const countSlice = createSlice({
name: 'count',
initialState: { num: 88 },
reducers: {
addNum(state, action) {
state.num += action.payload
},
decNum(state, action) {
state.num -= action.payload
}
}
})
export const { addNum, decNum } = countSlice.actions
export default countSlice.reducerts
// store/index.ts
import { configureStore } from '@reduxjs/toolkit'
import countReducer from './slice/countSlice'
export const store = configureStore({
reducer: { count: countReducer }
})tsx
// 入口
import { Provider } from 'react-redux'
import { store } from './store'
<Provider store={store}>
<App />
</Provider>tsx
// App.tsx
import { useDispatch, useSelector } from 'react-redux'
import { addNum, decNum } from './store/slice/countSlice'
const num = useSelector((state: any) => state.count.num)
const dispatch = useDispatch()
<button onClick={() => dispatch(addNum(3))}>加3</button>
<button onClick={() => dispatch(decNum(5))}>减5</button>易错点:切片 name、仓库里的 reducer 键名、useSelector 里的路径要对应;同步 action creator 是 createSlice 自动生成的,不要手写;改状态只能 dispatch,不要在组件里直接改 store。
异步修改:createAsyncThunk 与 extraReducers
为什么需要:reducers 里定义的都是同步 reducer,发请求、定时器等异步操作不能直接写进去。转写里反复强调:同步 action creator 自动生成,异步 action creator 要手动用 createAsyncThunk 创建。
怎么想:异步 action creator 的回调里写请求;请求返回成功值后,RTK 会自动派发 pending → fulfilled/rejected 三种 action,再在 extraReducers 里用 builder.addCase 接住。
怎么做:
ts
import { createAsyncThunk } from '@reduxjs/toolkit'
import axios from 'axios'
export const asyncAddNum = createAsyncThunk(
'count/asyncAddNum',
async (payload: number) => {
const res = await axios.get('https://api.github.com/users/xxx')
return res.data.public_repos // 示例:成功返回值
}
)ts
extraReducers: (builder) => {
builder
.addCase(asyncAddNum.pending, (state, action) => {
console.log('pending', action)
})
.addCase(asyncAddNum.fulfilled, (state, action) => {
console.log('fulfilled', action)
state.num += action.payload
})
.addCase(asyncAddNum.rejected, (state, action) => {
console.log('rejected', action.error.message)
})
}调试过程:转写里先只写 thunk、在组件里 dispatch(asyncAddNum(1)),打印请求回来的 data,确认异步函数能跑通;再补 extraReducers,观察 pending 一定先触发,fulfilled 的 action.payload 是成功返回值,rejected 的失败信息在 action.error.message。
易错点:异步代码写在 thunk 回调里,不写在 extraReducers;fulfilled 里拿 payload,失败值不要从 payload 取;pending 不是“成功”,只是请求中。
RTK 小结:一个切片暴露什么
为什么需要:切片内容多,暴露错一个名字,组件里就取不到或派发不了。
怎么想:默认暴露 reducer 给 store;分别暴露同步 actions;异步 action creator 手动暴露。仓库只配置 reducer。
易错点:export default countSlice.reducer 不是 reducers;countSlice.actions 里是同步 action creator;异步 thunk 不在 actions 里。
useCallback:缓存一个函数
为什么需要:函数组件每次渲染都会重新创建函数。转写中 addCount 每次渲染都新建一份,浪费内存。
怎么做:
tsx
const addCount = useCallback(() => {
setCount(n => n + 3)
}, [])调试过程:转写里先写空依赖,发现点击只能加到 91,因为闭包拿到旧 count;改成 setCount(n => n + 3) 后可持续累加。易错点:空依赖里读旧 state 会形成旧闭包;依赖数组别漏。
PureComponent:类组件的浅比较
为什么需要:父组件更新时,子组件即使没用变化的数据也会重新 render。
怎么做:类组件继承 PureComponent,当 props 和 state 浅比较不变时不重新渲染。函数组件对应 React.memo。易错点:浅比较只看引用,新对象/新数组即使内容一样也会渲染。
useMemo:缓存计算结果
为什么需要:昂贵计算只依赖 count,但输入框改 val 也会让组件重渲染,导致计算重复执行。
怎么做:
tsx
const total = useMemo(() => expensive(count), [count])调试过程:转写里在 expensive 内打印,发现改 val 也触发;加 useMemo 后,val 变化不再重算,count 变化才重算。易错点:依赖数组要写全;useMemo 缓存结果,useCallback 缓存函数。
useReducer:组件内状态集中管理
为什么需要:组件内多个 useState 分散,想集中成一个对象管理。
怎么做:
tsx
const initialState = { num: 88, msg: 'hello' }
function reducer(state: any, action: any) {
switch (action.type) {
case 'addNum':
return { ...state, num: state.num + action.payload }
case 'decNum':
return { ...state, num: state.num - action.payload }
default:
return state
}
}
const [state, dispatch] = useReducer(reducer, initialState)
dispatch({ type: 'addNum', payload: 3 })易错点:返回新对象时必须先 ...state,否则其他字段会丢;dispatch 的 action 对象要手写,比 useState 繁琐,业务复杂时才考虑。
先说明本节材料边界
本节笔记只有一句话:“TypeScript 简称『TS』,是微软开发的一个开源的编程语言。”笔记没有附图片,也没有附代码,因此正文不插入图片和代码块。需要特别说明:提供的转写稿内容主要是 Redux Toolkit 的 store、slice、createAsyncThunk、extraReducers,以及 useCallback、PureComponent、useMemo、useReducer 等 React 内容,并不是“TypeScript 介绍”。其中能识别到不少同音字,例如“starstar”应为 store,“框”多指 count,“程序员”多指 reducer,“产品经理”多指 action creator,“义部”应为异步,“判”应为 pending,“猪 field”应为 fulfilled,“reject”应为 rejected,“actl reduces”应为 extraReducers,“优资 call back”应为 useCallback。但这些内容与本节主题不一致,若强行拿来补 TypeScript 的推导和调试过程,就会变成编造。因此本文只围绕笔记中已经给出的定义做保守扩写,不替笔记补写未出现的 TypeScript 语法、编译配置和类型系统细节。
TypeScript 是什么:先抓住定义中的四个信息
为什么需要先抓定义? 因为初学者最容易把 TypeScript 当成某个框架、某个插件,或者把它和 JavaScript 混为一谈。笔记第一句虽然短,但已经给出了四个关键信息:简称是“TS”;由微软开发;是开源的;是一门编程语言。先把这四个信息记住,后面学习才不会把工具、语言、运行环境混在一起。
怎么想? 可以按“名称—来源—授权方式—类别”来理解。名称上,TypeScript 简称 TS,这是日常交流中的叫法;来源上,它由微软开发,说明它背后有明确的组织维护;授权方式上,它是开源的,说明源码和生态是开放的;类别上,它是编程语言,不是编辑器、不是浏览器插件,也不是某个 UI 框架。
怎么做? 学习时先用自己的话复述这句话,不要急着背复杂概念。可以记成:“TS 是微软开发的开源编程语言,TS 是它的简称。”如果看到别人说“TS”,要知道指的就是 TypeScript。笔记没有展开类型系统、编译流程、安装方式,所以本教程在这里不额外补写,避免超出材料。
易错点。 第一,把 TS 说成“微软的框架”不准确,笔记明确说它是编程语言。第二,把“开源”理解成“没有公司维护”也不准确,开源和由微软开发并不矛盾。第三,把 TS 和 JavaScript 直接对立起来也不合适;笔记没有展开二者关系,初学阶段先不要自行下结论。第四,看到转写稿里大量 Redux 和 React Hooks 内容,就误以为本节在讲这些,这是材料错位,不是本节知识点。
为什么需要知道“谁开发、是否开源、是什么类别”
为什么需要? 因为这三个信息会影响你后续查资料和判断学习路径。知道它由微软开发,查官方资料时会更关注官方文档和官方生态;知道它开源,遇到问题时可以看社区资料和源码;知道它是编程语言,就不会把它当成某个项目里的临时工具。
怎么想? 可以把它和“学一门语言”这件事对应起来:先确认语言名称和简称,再确认维护方和开源属性,最后再进入语法学习。笔记只完成了“确认身份”这一步,所以本节的目标不是教你写 TS 代码,而是让你能准确介绍 TS 是什么。
怎么做? 在笔记旁做一条注释:TypeScript=TS;微软开发;开源;编程语言。以后看到“TS”这个缩写,能还原为 TypeScript;看到“微软开发”不要误写成其他公司;看到“开源”不要误写成闭源商业软件。
易错点。 不要把“简称 TS”写成“TS 是 TypeScript 的框架”;不要把“开源”写成“免费但不开源”;不要把“编程语言”写成“编程软件”。这些都不是笔记原意。本节没有代码和图片,也不需要为了凑内容编造代码示例。
本节小结与学习提醒
本节真正可确认的知识点只有一个:TypeScript 简称 TS,是微软开发的开源编程语言。围绕这句话,学习时做到三点即可:第一,能准确说出全称、简称、开发方、开源属性和语言类别;第二,不把 TS 与框架、工具、运行环境混淆;第三,遇到与笔记主题不一致的转写稿内容时,先判断材料是否错位,不把 Redux 或 React Hooks 的内容硬说成 TypeScript 介绍。后续如果笔记补充了类型系统、编译方式或代码示例,再按“为什么需要→怎么想→怎么做→易错点”继续展开。
课堂画面






代码示例
示例一:Redux Toolkit 同步切片 + Store + Provider + useSelector/useDispatch
jsx
// src/store/counterSlice.js
import { createSlice } from '@reduxjs/toolkit'
const counterSlice = createSlice({
name: 'counter', // 切片名,会拼进 action.type
initialState: { num: 88 }, // 状态数据
reducers: { // 同步"程序员",只能写同步代码
addNum(state, action) {
state.num += action.payload // 内部直接用 += 修改
},
decNum(state, action) {
state.num -= action.payload
}
}
})
// 自动生成的 action creator 在 slice.actions 上,与 reducer 同名
export const { addNum, decNum } = counterSlice.actions
// 默认暴露 reducer
export default counterSlice.reducerjsx
// src/store/index.js
import { configureStore } from '@reduxjs/toolkit'
import counterReducer from './counterSlice'
const store = configureStore({
reducer: {
counter: counterReducer // 属性名与下面 useSelector 里的取值对应
}
})
export default storejsx
// src/index.js
import React from 'react'
import ReactDOM from 'react-dom/client'
import { Provider } from 'react-redux' // 注意:来自 react-redux
import store from './store'
import App from './App'
const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(
<Provider store={store}>
<App />
</Provider>
)jsx
// src/App.jsx
import { useSelector, useDispatch } from 'react-redux'
import { addNum, decNum } from './store/counterSlice'
function App() {
const num = useSelector(state => state.counter.num) // 先取切片,再取字段
const dispatch = useDispatch()
return (
<div>
<p>num: {num}</p>
<button onClick={() => dispatch(addNum(3))}>同步加3</button>
<button onClick={() => dispatch(decNum(5))}>同步减5</button>
</div>
)
}
export default App关键行与易错点
createSlice里配置三样:name、initialState、reducers;addNum/decNum是同步程序员,写法是直接state.num += action.payload。- 同步 action creator 不需要手写,
counterSlice.actions上自动就有同名函数;导出的addNum与 reducer 同名,但身份是 action creator。 - 易错点:
Provider要从react-redux引入,不是从@reduxjs/toolkit引入。 - 易错点:
useSelector里要写全路径state.counter.num,只写state.num拿不到。 - 修改状态只能通过
dispatch(actionCreator(payload)),不能直接改。
示例二:createAsyncThunk + extraReducers 实现异步修改
jsx
// src/store/counterSlice.js
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'
import axios from 'axios'
// 异步 action creator 必须手动创建
export const asyncAddNum = createAsyncThunk(
'counter/asyncAddNum', // 一般写成 "切片名/方法名"
async (payload) => { // 异步代码写在这个回调里
const res = await axios.get('https://api.github.com/users/octocat')
return res.data.public_repos // 返回的是成功结果值
}
)
const counterSlice = createSlice({
name: 'counter',
initialState: { num: 88 },
reducers: {
addNum(state, action) {
state.num += action.payload
}
},
// 异步程序员配置在 extraReducers 里
extraReducers: (builder) => {
builder
.addCase(asyncAddNum.pending, (state, action) => {
console.log('pending action:', action) // 必然触发
})
.addCase(asyncAddNum.fulfilled, (state, action) => {
// 成功结果值在 action.payload
state.num += action.payload
})
.addCase(asyncAddNum.rejected, (state, action) => {
console.log('rejected:', action.error.message) // 失败值在 error.message
})
}
})
export const { addNum } = counterSlice.actions
export default counterSlice.reducerjsx
// src/App.jsx
import { useSelector, useDispatch } from 'react-redux'
import { addNum, asyncAddNum } from './store/counterSlice'
function App() {
const num = useSelector(state => state.counter.num)
const dispatch = useDispatch()
return (
<div>
<p>num: {num}</p>
<button onClick={() => dispatch(addNum(3))}>同步加3</button>
<button onClick={() => dispatch(asyncAddNum(1))}>异步加</button>
</div>
)
}
export default App关键行与易错点
- 同步 action creator 自动生成,异步 action creator 必须用
createAsyncThunk手动创建;异步代码写在它的回调函数里,extraReducers里写的仍是同步代码。 extraReducers的值是一个函数,形参是builder,通过builder.addCase(...)挂载分支,由 Redux Toolkit 自动调用,不需要自己调。- 一个异步 action 对应三个分支:
pending→fulfilled→rejected,因为底层是 Promise,一定从 pending 出发。 - 易错点:成功的结果值只进
action.payload;失败的结果值不进 payload,而是进action.error.message。 pending分支拿不到成功结果,只有action.arg(你 dispatch 时传的参数);实际开发中通常只写fulfilled。
示例三:useCallback 缓存一个函数
jsx
// src/App.jsx
import { useState, useCallback } from 'react'
function App() {
const [count, setCount] = useState(88)
// 只有挂载时创建一次,之后不再重新创建
const addCount = useCallback(() => {
// 用函数式更新,才能拿到最新的 count
setCount(c => c + 3)
}, [])
return (
<div>
<p>count: {count}</p>
<button onClick={addCount}>加3</button>
</div>
)
}
export default App关键行与易错点
useCallback(fn, deps)的语法与useEffect很像:依赖数组为[]时,函数只在组件挂载时创建一份,后续每次渲染都不再新建,节省内存。- 易错点(转写稿重点演示的坑):如果写成
setCount(count + 3)且依赖是[],函数里形成闭包,count永远是初始的88,点一次变成91之后再也加不上去。这恰恰说明函数只有一份。 - 解决办法不是加
[count]依赖(那样等于每次重建,跟不加useCallback一样),而是用函数式更新setCount(c => c + 3)。 useCallback是性能优化手段,不加也能实现功能,属于"能理解就理解"的范畴。
常见错误
现象:点击异步加按钮后页面不更新,
fulfilled分支拿到undefined。
原因:createAsyncThunk回调没有return成功值,或在fulfilled里取错字段。
修法:异步回调明确return业务数据;成功值从action.payload取,失败信息从action.error.message取。现象:把请求代码写进
reducers后报错或不执行。
原因:createSlice.reducers只接收同步 reducer,异步逻辑不能直接放这里。
修法:异步请求放createAsyncThunk,结果在extraReducers的builder.addCase中处理。现象:
useCallback包裹后,点击加数只加一次。
原因:依赖数组为空,闭包捕获了初始状态值。
修法:补齐依赖;或用setNum(prev => prev + step)避免读取旧闭包值。现象:
useMemo在无关输入变化时仍重算,或该重算时不重算。
原因:依赖数组漏项、错项,未覆盖计算实际读取的状态。
修法:把计算函数实际依赖的状态全部列入依赖数组。现象:
PureComponent子组件仍然频繁render。
原因:父组件每次传入新对象、数组或函数,浅比较发现引用变化。
修法:稳定引用(useMemo/useCallback/常量),或使用React.memo自定义比较。现象:
useReducer更新num后msg丢失;dispatch不触发。
原因:返回新state时漏写...state;action.type与case不匹配。
修法:先展开旧state再覆盖目标字段;type统一命名或抽成常量。现象:组件拿不到 store,或
Provider导入报错。
原因:Provider、useSelector、useDispatch导入来源错误,或根组件未传store。
修法:从react-redux导入相关 API,Provider包裹根组件并传入配置好的store。
练习
补全
createAsyncThunk('counter/asyncAdd', async (step) => { ... }):请求返回total,在extraReducers中处理pending/fulfilled/rejected,要求fulfilled时state.num += action.payload,rejected时记录action.error.message。修复代码:
const add = useCallback(() => setNum(num + step), []);说明两种修法,并写出修改后的关键代码。已有
expensive(n)计算,页面输入框修改msg时不应触发重算。请用useMemo改造,并写出依赖数组及理由。用
useReducer管理{ num, msg },实现add(step)、dec(step)。要求写出dispatch调用方式,并保证更新num时msg不丢失。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | redux复习 | 07:36 |
| 2 | redux-异步操作-createAsyncThunk-extraReducers | 26:06 |
| 3 | reduxjs-toolkit小结 | 11:47 |
| 4 | useCallback-缓存一个函数 | 15:12 |
| 5 | PureComponent纯组件 | 10:33 |
| 6 | useMemo缓存一个函数计算的结果 | 09:44 |
| 7 | useReducer-函数组件状态进行集中管理 | 12:03 |
小结
reducers管同步,createAsyncThunk管异步,extraReducers管异步结果,三者职责不要混。- 异步三态映射要记牢:
fulfilled取payload,rejected取error.message,pending只表示进行中。 useCallback的收益建立在正确依赖上,依赖写错会直接制造闭包 bug。useMemo缓存的是计算结果,依赖必须覆盖计算输入。PureComponent/React.memo只解决浅比较场景,引用类型不稳定时优化会失效。useReducer适合集中管理,但模板代码多,更新时必须保留未修改字段。- 性能优化 Hook 先保证行为正确,再按实际瓶颈使用,不要为缓存而缓存。
- RTK 中 action creator 与
type命名统一,能减少dispatch和case不匹配问题。