Skip to content

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.reducer
ts
// 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 介绍。后续如果笔记补充了类型系统、编译方式或代码示例,再按“为什么需要→怎么想→怎么做→易错点”继续展开。

课堂画面 ​

课堂画面 1

课堂画面 2

课堂画面 3

课堂画面 4

课堂画面 5

课堂画面 6

代码示例 ​

示例一: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.reducer
jsx
// src/store/index.js
import { configureStore } from '@reduxjs/toolkit'
import counterReducer from './counterSlice'

const store = configureStore({
  reducer: {
    counter: counterReducer   // 属性名与下面 useSelector 里的取值对应
  }
})

export default store
jsx
// 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.reducer
jsx
// 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 是性能优化手段,不加也能实现功能,属于"能理解就理解"的范畴。

常见错误 ​

  1. 现象:点击异步加按钮后页面不更新,fulfilled 分支拿到 undefined。
    原因:createAsyncThunk 回调没有 return 成功值,或在 fulfilled 里取错字段。
    修法:异步回调明确 return 业务数据;成功值从 action.payload 取,失败信息从 action.error.message 取。

  2. 现象:把请求代码写进 reducers 后报错或不执行。
    原因:createSlice.reducers 只接收同步 reducer,异步逻辑不能直接放这里。
    修法:异步请求放 createAsyncThunk,结果在 extraReducers 的 builder.addCase 中处理。

  3. 现象:useCallback 包裹后,点击加数只加一次。
    原因:依赖数组为空,闭包捕获了初始状态值。
    修法:补齐依赖;或用 setNum(prev => prev + step) 避免读取旧闭包值。

  4. 现象:useMemo 在无关输入变化时仍重算,或该重算时不重算。
    原因:依赖数组漏项、错项,未覆盖计算实际读取的状态。
    修法:把计算函数实际依赖的状态全部列入依赖数组。

  5. 现象:PureComponent 子组件仍然频繁 render。
    原因:父组件每次传入新对象、数组或函数,浅比较发现引用变化。
    修法:稳定引用(useMemo/useCallback/常量),或使用 React.memo 自定义比较。

  6. 现象:useReducer 更新 num 后 msg 丢失;dispatch 不触发。
    原因:返回新 state 时漏写 ...state;action.type 与 case 不匹配。
    修法:先展开旧 state 再覆盖目标字段;type 统一命名或抽成常量。

  7. 现象:组件拿不到 store,或 Provider 导入报错。
    原因:Provider、useSelector、useDispatch 导入来源错误,或根组件未传 store。
    修法:从 react-redux 导入相关 API,Provider 包裹根组件并传入配置好的 store。

练习 ​

  1. 补全 createAsyncThunk('counter/asyncAdd', async (step) => { ... }):请求返回 total,在 extraReducers 中处理 pending/fulfilled/rejected,要求 fulfilled 时 state.num += action.payload,rejected 时记录 action.error.message。

  2. 修复代码:const add = useCallback(() => setNum(num + step), []); 说明两种修法,并写出修改后的关键代码。

  3. 已有 expensive(n) 计算,页面输入框修改 msg 时不应触发重算。请用 useMemo 改造,并写出依赖数组及理由。

  4. 用 useReducer 管理 { num, msg },实现 add(step)、dec(step)。要求写出 dispatch 调用方式,并保证更新 num 时 msg 不丢失。

原视频对照 ​

#视频时长
1redux复习07:36
2redux-异步操作-createAsyncThunk-extraReducers26:06
3reduxjs-toolkit小结11:47
4useCallback-缓存一个函数15:12
5PureComponent纯组件10:33
6useMemo缓存一个函数计算的结果09:44
7useReducer-函数组件状态进行集中管理12:03

小结 ​

  1. reducers 管同步,createAsyncThunk 管异步,extraReducers 管异步结果,三者职责不要混。
  2. 异步三态映射要记牢:fulfilled 取 payload,rejected 取 error.message,pending 只表示进行中。
  3. useCallback 的收益建立在正确依赖上,依赖写错会直接制造闭包 bug。
  4. useMemo 缓存的是计算结果,依赖必须覆盖计算输入。
  5. PureComponent/React.memo 只解决浅比较场景,引用类型不稳定时优化会失效。
  6. useReducer 适合集中管理,但模板代码多,更新时必须保留未修改字段。
  7. 性能优化 Hook 先保证行为正确,再按实际瓶颈使用,不要为缓存而缓存。
  8. RTK 中 action creator 与 type 命名统一,能减少 dispatch 和 case 不匹配问题。