Skip to content

多一句没有,少一句不行,用更短时间,教会更实用的技术!

https://zh-hans.react.dev/learn/start-a-new-react-project

<!-- more -->

环境搭建

create-react-app

bash
npx create-react-app my-app
cd my-app
npm start

JSX

JSX 是 JavaScript 的语法扩展。React 使用 JSX 来替代常规的 JavaScript。浏览器不能直接解析JSX,需要使用babel进行编译

https://babeljs.io/repl/#?browsers=defaults%2C not ie 11%2C not ie_mob 11&build=&builtIns=false&corejs=3.21&spec=false&loose=false&code_lz=DwEwlgbgfAUAkAFwBZgM4AI3vBGwD0OUQA&debug=false&forceAllTransforms=false&modules=false&shippedProposals=false&evaluate=false&fileSize=false&timeTravel=false&sourceType=module&lineWrap=true&presets=env%2Creact%2Cstage-2&prettier=false&targets=&version=7.26.2&externalPlugins=&assumptions={}

20241108144358

在 jsx 中可以通过 {} 来使用 js 表达式,但是不能使用 if else 语句,可以使用三元运算符

if语句、switch语句、变量声明属于语句,不是表达式,不能出现在{}中

jsx 中可以使用map实现列表渲染

jsx
const list = [1, 2, 3, 4, 5];
const listItems = list.map((item) => <li>{item}</li>);

jsx 中实现条件渲染

在React中,可以通过逻辑与运算符&&、三元表达式(?:)实现基础的条件渲染

jsx
const isLogin = true;
const element = isLogin ? <div>登录成功</div> : <div>登录失败</div>;

jsx 实现复杂条件渲染

列表中需要根据文章状态适配三种情况,单图,三图,和无图三种模式

自定义函数 + if判断语句

jsx
function getArticleType(article) {
  if (article.images.length === 1) {
    return <div>单图</div>;
  } else if (article.images.length === 3) {
    return <div>三图</div>;
  } else {
    return <div>无图</div>;
  }
}

事件绑定

on + 事件名称 = { 事件处理程序 },整体上遵循驼峰命名法

使用时间对象参数

在事件回调函数中设置形参e

jsx
function App() {
  function handleClick(e) {
    console.log(e);
  }
  return (
    <div>
      <button onClick={handleClick}>点我</button>
    </div>
  );
}

传递自定义参数

事件绑定的位置改造成箭头函数的写法,在执行clickHandler实际处理业务函数的时候传递实参

jsx
function App() {
  function handleClick(name) {
    console.log(name);
  }
  return (
    <div>
      <button onClick={() => handleClick("wu")}>点我</button>
    </div>
  );
}

不能直接写函数调用,这里事件绑定需要一个函数引用,而不是函数调用,所以需要使用箭头函数

同时传递事件对象和自定义参数

jsx
function App() {
  function handleClick(e, name) {
    console.log(e, name);
  }
  return (
    <div>
      <button onClick={(e) => handleClick(e, "wu")}>点我</button>
    </div>
  );
}

useState

useState 是一个 React Hook(函数),它允许我们向组件添加一个状态变量, 从而控制影响组件的渲染结果.

本质:和普通JS变量不同的是,状态变量一旦发生变化组件的视图UI也会跟着变化(数据驱动视图)

jsx
const [count, setCount] = useState(0); // count 是状态变量,setCount 是修改状态变量的方法
  1. useState是一个函数,返回值是一个数组
  2. 数组中的第一个参数是状态变量,第二个参数是set函数用来修改状态变量
  3. useState的参数是状态变量的初始值

状态不可变

在React中,状态被认为是只读的,我们应该始终替换它而不是修改它,直接修改状态不能引发视图更新

jsx
const [count, setCount] = useState(0);
const handleClick = () => {
    count = count + 1; // 错误写法
    setCount(count + 1); // 正确写法
};

修改对象状态

jsx
const [person, setPerson] = useState({ name: "wu", age: 18 });
const handleClick = () => {
    person.age = "19"; // 错误写法
    setPerson({ ...person, age: 19 }); // 正确写法
};

组件的样式处理

React组件基础的样式控制有俩种方式

内联样式

jsx
<div style={{ color: "red" }}>hello world</div>

class样式

jsx
<div className="red">hello world</div>

组件通讯

父子

props 可传递任意的数据

数字、字符串、布尔值、数组、对象、函数、组件(jsx)

jsx
<Child name="wu" age={18} />

props 是只读的,不能直接修改

jsx
props.name = "wu2"; // 错误写法

特殊的父传子

当我们把内容嵌套在子组件中时,子组件的props中会自动接收到父组件传递过来的内容,放到props.children中

jsx
<Child>
    <div>hello world</div>
</Child>

console.log(props.children); // 打印 hello world

子父

子组件通过调用父组件传递过来的方法,来修改父组件中的状态变量

jsx
function Parent() {
    const [count, setCount] = useState(0);
  return <Child setCount={setCount} count={count} />;
}

function Child(props) {
    return <button onClick={() => props.setCount(props.count + 1)}>点我</button>
}

兄弟

兄弟组件之间不能直接通讯,需要通过共同的父组件来实现通讯

20241111173353

使用状态提升实现兄弟组件通讯

  1. A组件先通过子传父的方式把数据传给父组件App
  2. App拿到数据后通过父传子的方式再传递给B组件

使用context机制实现跨层级组件通讯

20241111173500

  1. 使用createContext方法创建一个上下文对象Ctx
  2. 在顶层组件(App)中通过 Ctx.Provider 组件提供数据
  3. 在后代组件中通过 Ctx.Consumer 组件消费数据

useEffect

useEffect 是一个 React Hook(函数),用于在React组件中创建不是由事件引起而是由渲染本身引起的操作(副作用),比如发送AJAX请求,更改DOM等等。

20241111173702

上面的组件中没有发生任何的用户事件,组件渲染完毕之后就需要和服务器要数据,整个过程属于“只由渲染引起的操作”,所以需要使用useEffect来实现

基础使用

在组件渲染完毕之后,立刻从服务端获取频道列表数据并显示到页面中

jsx
useEffect(() => {
    axios.get("https://api.shenzjd.com").then((res) => {
        console.log(res);
    });
}, []);

参数1是一个函数,可以把它叫做副作用函数,在函数内部可以放置要执行的操作 参数2是一个数组(可选参),在数组里放置依赖项,不同依赖项会影响第一个参数函数的执行,当是一个空数组的时候,副作用函数只会在组件渲染完毕之后执行一次

useEffect 依赖项参数说明

依赖项副作用函数执行情况
没有依赖项组件初始渲染 + 组件更新时执行
空数组只在初始渲染时执行一次
添加特定依赖项组件初始渲染 + 特性依赖项变化时执行

清除副作用

在useEffect中编写的由渲染本身引起的对接组件外部的操作,社区也经常把它叫做副作用操作,比如在useEffect中开启了一个定时器,我们想在组件卸载时把这个定时器再清理掉,这个过程就是清理副作用

jsx
useEffect(() => {
    const timer = setInterval(() => {
        console.log("hello world");
    }, 1000);
    return () => {
        clearInterval(timer);
    };
}, []);

说明:清除副作用的函数最常见的执行时机是在组件卸载时自动执行

自定义hook

自定义Hook是以 use 打头的函数,通过自定义Hook函数可以用来实现逻辑的封装和复用

20241111174416

React Hooks 使用规则

  1. 只能在组件中或者其他自定义Hook函数中调用
  2. 只能在组件的顶层调用,不能嵌套在 if、for、其他函数中

20241111174804

redux 快速上手

Redux 是React最常用的集中状态管理工具,类似于Vue中的Pinia(Vuex),可以独立于框架运行作用:通过集中管理的方式管理应用的状态

20241111174859

为了职责清晰,数据流向明确,Redux把整个数据修改的流程分成了三个核心概念,分别是:state、action和reducer

  1. state - 一个对象 存放着我们管理的数据状态
  2. action - 一个对象 用来描述你想怎么改数据
  3. reducer - 一个函数 更具action的描述生成一个新的state

20241111174941

Redux 基本使用

https://cn.redux.js.org/

https://redux-toolkit.js.org/

redux-toolkit

Redux Toolkit(RTK)- 官方推荐编写Redux逻辑的方式,是一套工具的集合集,简化书写方式

bash
pnpm install @reduxjs/toolkit
  • 简化store的配置方式
  • 内置immer支持可变式状态修改
  • 内置thunk更好的异步创建

react-redux

react-redux - 用来 链接 Redux 和 React组件 的中间件

bash
pnpm install react-redux

20241112103216

配置基础环境

  1. 使用 CRA 生成项目
bash
npx create-react-app my-app
// 或者
pnpm dlx create-react-app my-app
  1. 安装@reduxjs/toolkit和react-redux
bash
pnpm install react-redux @reduxjs/toolkit
  1. 启动项目
bash
pnpm run start

store 目录设计

20241112103437

  1. 通常集中状态管理的部分都会单独创建一个单独的 store 目录
  2. 应用通常会有很多个子store模块,所以创建一个 modules 目录,在内部编写业务分类的子store
  3. store中的入口文件 index.js 的作用是组合modules中所有的子模块,并导出store

20241112103614

使用redux-toolkit创建counterStore

jsx
// counterStore.js
import { createSlice } from '@reduxjs/toolkit'

const counterStore = createSlice({
  name: 'counter',
  // 初始状态数据
  initialState: {
    count: 0
  },
  // 修改数据的方法 同步方法 支持直接修改
  reducers: {
    increment(state) {
      state.count++
    },
    decrement(state) {
      state.count--
    }
  }
})

// 解构出来 actionCreater函数
const { increment, decrement } = counterStore.actions

// 获取reducer
const counterReducer = counterStore.reducer

// 导出
export { increment, decrement }
export default counterReducer
jsx
// store/index.js
import { configureStore } from '@reduxjs/toolkit'
import counterReducer from './modules/counterStore'

const store = configureStore({
  reducer: { 
    counter: counterReducer
  }
})

为react注入store

react-redux 负责把Redux和React 链接 起来,内置 Provider组件 通过 store 参数把创建好的store实例注入到应用中,链接正式建立

jsx
import { Provider } from 'react-redux'
import { store } from './store'

const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(
  <Provider store={store}>
    <App />
  </Provider>
)

react组件使用store中的数据

在React组件中使用store中的数据,需要用到一个 钩子函数 - useSelector,它的作用是把store中的数据映射到组件中

jsx
import { useSelector } from 'react-redux'

const count = useSelector(state => state.counter)

20241112105330

react组件修改store中的数据

React组件中修改store中的数据需要借助另外一个hook函数 - useDispatch,它的作用是生成提交action对象的dispatch函数

jsx
import { useDispatch, useSelector } from 'react-redux'
import { increment, decrement } from './store/modules/counterStore'

function App() {
  const count = useSelector(state => state.counter)
  const dispatch = useDispatch()
  return (
    <div className='App'>
      <button onClick={() => dispatch(increment())}>+</button>
      <span>{count.count}</span>
      <button onClick={() => dispatch(decrement())}>-</button>
    </div>
  )
}

异步状态操作

20241112105909

  1. 创建store的写法保持不变,配置好同步修改状态的方法
  2. 单独封装一个函数,在函数内部return一个新函数,在新函数中 2.1 封装异步请求获取数据 2.2 调用同步actionCreater传入异步数据生成一个action对象,并使用dispatch提交
  3. 组件中dispatch的写法保持不变
jsx
const channelStore = createSlice({
  name: 'channel',
  initialState: {
    channelList: []
  },
  reducers: {
    getChannelList(state, action) {
      state.channelList = action.payload
    }
  }
})
jsx
const { setChannelList } = channelStore.actions

const fetchChannelList = () => {
  return async dispatch => {
    const res = await axios.get('https://api.shenzjd.com')
    dispatch(setChannelList(res.data.data.channels))
  }
}

export { fetchChannelList }
jsx
import { useDispatch } from 'react-redux'
import { fetchChannelList } from './store/modules/channelStore'

function App() {
  const dispatch = useDispatch()
  useEffect(() => {
    dispatch(fetchChannelList())
  }, [dispatch])
}

react-router

bash
pnpm install react-router-dom

路由导航

路由系统中的多个路由之间需要进行路由跳转,并且在跳转的同时有可能需要传递参数进行通信

声明式导航

jsx
<Link to="/">首页</Link>

编程式导航

jsx
import { useNavigate } from 'react-router-dom'

const Login = () => {
    const navigate = useNavigate()
    return (
      <div onClick={() => navigate('/')}>登录</div>
    )
}

路由传参

searchParams

js
navigate('/login?name=wu&age=18')
jsx
import { useSearchParams } from 'react-router-dom'
const [searchParams, setSearchParams] = useSearchParams()
console.log(searchParams.get('name'))

params

jsx
navigate('/login/wu/18')
jsx
import { useParams } from 'react-router-dom'
const params = useParams()
console.log(params.name)

嵌套路由配置

在一级路由中又内嵌了其他路由,这种关系就叫做嵌套路由,嵌套至一级路由内的路由又称作二级路由

20241112163222

  1. 使用 children 属性配置路由嵌套关系
  2. 使用 <Outlet/> 组件配置二级路由渲染位置
jsx
const routes = [
    {
        path: '/',
        element: <Layout />,
        children: [
          { path: 'login', element: <Login /> },
          { path: 'register', element: <Register /> }
        ]
    }
]
jsx
const Layout = () => {
    return (
        <div>
            <h1>Layout</h1>
            <div>
                <Link to="/login">登录</Link>
                <Link to="/register">注册</Link>
            </div>
            <Outlet />
        </div>
    )
}

默认二级路由

当访问的是一级路由时,默认的二级路由组件可以得到渲染,只需要在二级路由的位置去掉path,设置index属性为true

jsx
{ 
  path: '/', 
  element: <Layout />, 
  children: [
    { index: true, element: <Home /> },
    { path: 'login', element: <Login /> },
    { path: 'register', element: <Register /> }
  ]
}

404 路由配置

当访问的路由在配置中不存在时,会自动匹配到404路由,并渲染404组件

jsx
{ 
  path: '*', 
  element: <NotFound /> 
}

两种路由模式

history模式和hash模式, ReactRouter分别由createBrowerRouter和createHashRouter 函数负责创建

模式url实现底层原理是否需要后端支持
historyurl/loginhistory对象 + pushState事件需要后端支持
hashurl#login监听 hashchange 事件不需要后端支持

路由懒加载

路由懒加载是指路由的JS资源只有在被访问时才会动态获取,目的是为了优化项目首次打开的时间

jsx
import { lazy } from 'react'

const Login = lazy(() => import('./views/Login'))

路径别名

bash
pnpm install @craco/craco

craco.config.js

js
const path = require('path')

module.exports = {
    // plugins: [
    //   { plugin: require('@craco/craco') }
    // ],
    webpack: {
      alias: {
        '@': path.resolve(__dirname, './src')
      }
    }
}

修改package.json

json
"scripts": {
  "start": "craco start",
  "build": "craco build"
}

联想路径配置

VsCode的联想配置,需要我们在项目目录下添加 jsconfig.json 文件,加入配置之后VsCode会自动读取配置帮助我们自动联想提示

根目录下新增配置文件 - jsconfig.json

json
{
  "compilerOptions": {
    "baseUrl": "./",
    "paths": { 
      "@/*": ["src/*"] 
    }
  }
}

打包体积优化

bash
pnpm install source-map-explorer

性能相关优化Api

useReducer

useReducer 和 useState 的区别

  • useState 适合管理简单状态数据
  • useReducer 适合管理复杂状态数据

20241112165321

作用:和useState的作用类似,用来管理相对复杂的状态数据

  1. 定义一个reducer函数(根据不同的action返回不同的新状态)
  2. 在组件中调用useReducer,并传入reducer函数和状态的初始值
  3. 事件发生时,通过dispatch函数分派一个action对象(通知reducer要返回哪个新状态并渲染UI)
jsx
function reducer(state, action) {
    switch (action.type) {
        case 'INC': return state + 1
        case 'DEC': return state - 1
        default: return state
    }
}
jsx
const [state, dispatch] = useReducer(reducer, 0)
jsx
dispatch({ type: 'INC' })

useReducer 分派action时传参

20241112165252

useMemo

在组件每次重新渲染的时候缓存计算的结果

jsx
const result = useMemo(() => {
    // 根据count1返回计算的结果
}, [count1])

使用useMemo做缓存之后可以保证只有count1依赖项发生变化时才会重新计算

作用:允许组件在Props没有改变的情况下跳过渲染

React组件默认的渲染机制:只要父组件重新渲染子组件就会重新渲染

20241112170222

如果Son组件本身并不需要做渲染更新,是不是存在浪费?

jsx
const MemoComponent = memo(function SomeComponent(props) {
    return <div>SomeComponent</div>
})

经过memo函数包裹生成的缓存组件只有在props发生变化的时候才会重新渲染

memo 和 props 的比较机制

在使用memo缓存组件之后,React会对每一个 prop 使用 Object.is 比较新值和老值,返回true,表示没有变化

prop是简单类型

Object.is(3, 3) => true 没有变化

prop是引用类型(对象 / 数组)

Object([], []) => false 有变化,React只关心引用是否变化

useCallback

在组件多次重新渲染的时候缓存函数

jsx
const Input = memo(function Input({onChange}) {
    console.log('Input组件重新渲染')
    return <input type="text" onChange={e => onChange(e.target.value)} />
})

function App() {
    // 传递给子组件的函数
    const changeHandler = useCallback(value => {
        console.log(value)
    }, [])
    // 触发父组件重新渲染的函数
    const [value, setValue] = useState(0)
    return (
        <div className='App'>
            <Input onChange={changeHandler} />
            <button onClick={() => setValue(value + 1)}>+</button>
        </div>
    )
}

使用useCallback包裹函数之后,函数可以保证在App重新渲染的时候保持引用稳定

React.forwardRef

使用ref暴露DOM节点给父组件

20241112170925

jsx
const Input = forwardRef(function Input(props, ref) {
    return <input {...props} ref={ref} />
})

function App() {
    const inputRef = useRef(null)
    return <Input ref={inputRef} />
}

useImperativeHandle

通过ref暴露子组件中的方法

20241112171037

jsx
const Input = forwardRef((props, ref) => {
    const inputRef = useRef(null)
    const focus = () => {
        inputRef.current.focus()
    }
    useImperativeHandle(ref, () => ({
        focus
    }))
    return <input {...props} ref={inputRef} />
})

Class

类组件的写法

类组件就是通过JS中的类来组织组件的代码

  1. 通过类属性state定义状态数据
  2. 通过setState方法来修改状态数据
  3. 通过render来写UI模版(JSX语法一致)
jsx
class Counter extends Component {
    state = {
        count: 0
    }
    clickHandler = () => {
        this.setState({ count: this.state.count + 1 })
    }
    render() {
        return <div onClick={this.clickHandler}>{this.state.count}</div>
    }
}

类组件的生命周期

组件从创建到销毁的各个阶段自动执行的函数就是生命周期函数

20241112171715

  1. componentDidMount:组件挂载完毕自动执行 - 异步数据获取
  2. componentWillUnmount: 组件卸载时自动执行 - 清理副作用

zustand

极简的状态管理工具

20241112171825

异步支持

对于异步的支持不需要特殊的操作,直接在函数中编写异步逻辑,最后只需要调用set方法传入新状态即可

jsx
const useStore = create(set => {
  return {
    channelList: [],
    fetchChannelList: async () => {
      const res = await axios.get('https://api.shenzjd.com')
      set({ channelList: res.data.data.channels })
    }
  }
})

切片模式

当单个store比较大的时候,可以采用 切片模式 进行模块拆分组合,类似于模块化

20241112172027

vite

bash
pnpm create vite

# 使用react模板
pnpm create vite my-app -- --template react-ts

useState与TS

useState本身是一个泛型函数,可以传入具体的自定义类型

jsx
type User = {
  name: string
  age: number
}
const [user, setUser] = useState<User>({ name: 'wu', age: 18 })
  1. 限制useState函数参数的初始值必须满足类型为: User | ()=> User
  2. 限制setUser函数的参数必须满足类型为:User | ()=> User | undefined
  3. user状态数据具备User类型相关的类型提示

useState-初始值为null

当我们不知道状态的初始值是什么,将useState的初始值为null是一个常见的做法,可以通过具体类型联合null来做显式注解

jsx
type User = {
  name: string
  age: number
}
const [user, setUser] = useState<User | null>(null)
  1. 限制useState函数参数的初始值可以是 User | null
  2. 限制setUser函数的参数类型可以是 User | null

props与TypeScript

为组件prop添加类型,本质是给函数的参数做类型注解,可以使用type对象类型或者interface接口来做注解

jsx
type Props = {
  name: string
}

function Button(props: Props) {
  return <button>{props.name}</button>
}

props与TypeScript - 为children添加类型

children是一个比较特殊的prop, 支持多种不同类型数据的传入,需要通过一个内置的ReactNode类型来做注解

jsx
type Props = {
  children: ReactNode
}

function Button(props: Props) {
  return <button>{props.children}</button>
}

注解之后,children可以是多种类型,包括:React.ReactElement 、string、number、React.ReactFragment 、React.ReactPortal 、boolean、 null 、undefined

资源

我用夸克网盘分享了「黑马程序员-react教程.pdf」,点击链接即可保存。打开「夸克APP」在线查看,支持多种文档格式转换。 链接:https://pan.quark.cn/s/d29ef5ff01f9 提取码:X7bX