React18学习笔记

207 阅读11分钟

一、脚手架搭建项目

基于vite搭建的react的项目

image.png

其中react模块react-dom模块有什么区别

  • react模块:核心功能、组件
  • react-dom模块:操作浏览器的dom,分为客户端react-dom/client和服务端react-dom/server

二、JSX

概念:JSXJavaScript语法拓展,可以让你在js文件中书写html标签

JSX语法与HTML语法的写法区别

  1. 标签要小写
  2. 标签要闭合
  3. 不能用classfor关键字,classNamehtmlFor(一般用于lable下拉框中匹配)代替
  4. 属性驼峰式命名
  5. JSX通过大括号使用JavaScript
  6. 属性使用大括号
  7. 唯一根元素
  8. 添加注释的区别

image.png

三、css部分

3.1 react中书写css的方式

react中,有三种方式书写css样式

  1. 行内样式
  2. 全局样式
  3. 局部样式
import './styles/全局样式.css'
// 局部样式,引入时需要命名
import style from './styles/局部样式.module.css'

function App() {
  const myStyle = { color: 'gray' }

  return (
    <>
      {/* 行内样式 */}
      <h1 style={myStyle}>呵呵</h1>
      <h1 style={{ color: 'red' }}>哈哈</h1>

      {/* 全局样式 */}
      <div className='box'></div>

      {/* 局部样式 */}
      <div className={style.box2}></div>
      {/* 当css中为短横线命名时,不能直接点,需要写成中括号的形式 */}
      <div className={style['head-title']}>短横线</div>
      {/* 但是可以在vite中配置,这样就可以使用小驼峰了 */}
      <div className={style.headTitle}>小驼峰</div>
    </>
  )
}

vite.config.js中配置局部样式可使用小驼峰写法

image.png

3.2 react中使用sass预处理器

安装:pnpm i sass ,然后使用方法与上面一致

3.3 classnames优化类名控制

image.png

四、添加事件操作

要点

  1. event为合成事件,与原生的有区别
  2. 所有事件委托到容器元素
  3. 传参处理:箭头函数(推荐)、高阶函数
function App() {
  // 高阶函数穿参
  // handleClick(1)相当于直接调用了,也就是把return后面的值写在onClick的{}中
  const handleClick = (num) => {
    return (e) => {
      console.log(num, e)
    }
  }
  // 箭头函数传参
  const handleClick2 = (e,num) => {
    console.log(e,num)
  }

  return (
    <>
      <button onClick={handleClick(1)}>高阶函数传参</button>
      <button onClick={(e) => handleClick2(e, 1)}>箭头函数传参</button>
      // 箭头函数传参写法:传参加箭头,不传参正常写
      <button onClick={handleClick2}>不传参也可接收e</button>
    </>
  )
}

export default App

函数加小括号和不加小括号的区别

  • handleClick:表示一个函数表达式
  • handleClick():表示函数执行完的结果,return后面的表达式

五、条件渲染

  1. 条件语句ifswitch
  2. 三目运算符
  3. 逻辑运算符&&||
  • react{}中,哪些值不会被渲染:布尔值、空字符串、null、undefined、对象、函数
  • 如何对不渲染的值进行输出:JSON.stringify(){undefined+''}

image.png

六、列表渲染

要点

  • 通过 JavaScript 的 map() 方法从数组中生成组件
  • 通过 JavaScript 的 filter() 筛选需要渲染的组件
  • 何时以及为何使用 React 中的 key

image.png

七、组件相关

7.1 组件的点标记写法

  1. 对象形式
  2. 函数形式
// 组件的点标记写法:对象形式
const Qf = {
  Welcome() {
    return <div>小明</div>
  },
}
// 解构赋值
const { Welcome } = Qf

function App() {
  return (
    <div>
      <Qf.Welcome></Qf.Welcome>
      <Welcome></Welcome>
    </div>
  )
}

export default App

image.png


// 组件的点标记写法:函数形式
const Af = () => {
  return <div>小强</div>
}
Af.Hello = () => {
  return <div>你好小强</div>
}
//解构赋值
const { Hello } = Af

function App() {
  return (
    <div>
      <Af></Af>
      <Af.Hello></Af.Hello>
      <Hello></Hello>
    </div>
  )
}

export default App

image.png

7.2 组件通信

  • props传递值:可以通过整体接收和解构接收
  • 通过{...}批量传输数据
  • props传递事件/函数
// 传递值
const ChildItem = (props) => {
  return (
    <div>
      <div>{props.name}</div>
      <div>{props.age}</div>
    </div>
  )
}

function App() {
  const userInfo = {
    name: 'zs',
    age: 18,
  }
  return (
    <div>
      <ChildItem name={userInfo.name} age={userInfo.age}></ChildItem>
      // 通过{...}批量传输数据
      <ChildItem {...userInfo}></ChildItem>
    </div>
  )
}

export default App

// 传递函数
const ChildItem = ({ onClick, getData }) => {
  getData('我是子组件的数据')
  return (
    <div>
      <button onClick={onClick}>点击</button>
    </div>
  )
}

function App() {
  const handleClick = () => {
    console.log(111)
  }
  const getData = (data) => {
    console.log(data)
  }
  return (
    <div>
      // 传递函数
      <ChildItem onClick={handleClick} getData={getData}></ChildItem>
    </div>
  )
}

export default App

7.3 组件组合

要点

  1. propschildren属性
  2. 如何分别传递多组内容

  • 在组件标签之间写东西时(类似于插槽),会向下传递一个props,其中有children属性
function Father() {
  const count = 123
  return (
    <div>
      <div>我是father</div>
      <Son>
        <GrandSon count={count}></GrandSon>
      </Son>
    </div>
  )
}

function Son({ children }) {
  const count = 456
  return (
    <div>
      <div>我是son</div>
      {children}
    </div>
  )
}

function GrandSon({ count }) {
  return (
    <div>
      <div>我是GrandSon</div>
      <div>传值{count}</div>
    </div>
  )
}

export default Father

image.png


  • 如何分别传递多组内容(利用一个特性:只要是大括号能接收的,都能进行传递)
function Father() {
  return (
    <div>
      <div>我是father</div>
      <Son top={<div>aaa</div>} bottom={<div>bbb</div>}></Son>
    </div>
  )
}

function Son({ top, bottom }) {
  return (
    <div>
      {top}
      <div>我是son</div>
      {bottom}
    </div>
  )
}

export default Father

image.png

7.4 组件通信的默认值

两种方式

  1. es6的默认参数
  2. react提供的组件defaultProps属性
function Father() {
  return (
    <div>
      <div>我是father</div>
      <Son></Son>
    </div>
  )
}

// 通过defaultProps属性添加默认值
Son.defaultProps = {
  name: 'zs',
}

function Son({ name }) {
  return (
    <div>
      <div>我是son</div>
      <div>{name}</div>
    </div>
  )
}

export default Father

7.5 限定组件通信的类型

有两种方法

  1. ts
  2. 组件的propTypes属性

其中,组件的propTypes属性配合 prop-types插件 可以做更细致的校验

image.png

7.6 组件必须是纯函数

纯函数的特点

  1. 只负责自己的任务,不更改在函数调用前就已存在的对象或变量
  2. 输入相同,则输出相同。给定相同的输入,总是返回相同的结果

8. 状态管理

8.1 什么是组件的状态

  • 随时间变化的数据称之为状态(state),状态可以进行数据驱动视图,而普通变量不行
  • useState可创建状态和修改状态的方法
import { useState } from 'react'

function App() {
  // 可以有记忆功能
  const [count, setCount] = useState(0)
  const addCount = () => {
    // 可以重新触发函数组件的执行
    setCount(count + 1)
  }
  // 每次重新执行函数的count值都是不一样的,所以return的内容不一样
  console.log(count)
  return (
    <div>
      <button onClick={addCount}>点击+1</button>
      <div>{count}</div>
    </div>
  )
}

export default App

8.2 状态是如何改变视图的

  • 普通函数为什么不行:无法重新渲染JSX
  • state状态为什么可行:重新触发函数组件,并且state状态具备组件的记忆

状态是如何改变视图的(渲染与提交的过程:三个步骤)

  1. 触发一次渲染:组件的初次渲染,createRoot().render(),内部状态更新,触发渲染送入队列
  2. 渲染您的组件:在进行初次渲染时,React会调用根组件内部状态更新,会渲染对应的函数组件
  3. 提交到DOM上:初次渲染,通过appendChild()使得内部状态更新,更新差异的DOM节点

8.3 如何记忆多状态

  • 在同一组件的每次渲染中,useState都依托于一个稳定的调用顺序。

React内部,为每个组件保存了一个数组,其中每一项都是一个state对,它维护当前state对的索引值,在渲染之前将其设置为0,每次调用useState时,React都会提供一个state对,并增加索引值

  • 不要在逻辑中调用useState,会改变内部的顺序

8.4 什么是状态的快照以及快照的陷阱

  • 虽然state变量看起来和一般的可读写的JavaScript变量类似,但state的特性更像是一张快照。设置它不会更改已有的state变量,但会触发重新渲染
  • React会使state的值始终固定在一次渲染的各个事件处理函数内部,你无需担心代码运行到state是否发生了变化,其实这就是闭包的特性
import { useState } from 'react'

function App() {
  const [count, setCount] = useState(0)

  const handleClick = () => {
    setCount(count + 1)
    setCount(count + 1)
    setCount(count + 1)
    console.log(count) // 0
  }

  return (
    <div>
      <button onClick={handleClick}>点击我+1</button>
      <div>{count}</div>
    </div>
  )
}
export default App

image.png

8.5 状态队列与自动批处理

  • React会等到事件处理函数中的所有代码运行完毕再处理你的state更新,队列都执行完毕后,再进行UI更新,这种特性就是自动批处理

  • 更新函数的写法:setState(x)实际上会像setState((n)=>x)一样运行,只是没有使用n

import { useState } from 'react'

function App() {
  const [count, setCount] = useState(0)

  const handleClick = () => {
    // setCount(count + 1) // 0+1
    // setCount(count + 1) // 0+1
    // setCount(count + 1) // 0+1
    // setCount(count+1)相当于setCount((c)=>count+1)
    console.log(count) // 0
    setCount((c) => c + 1) // 0+1
    setCount((c) => c + 1) // 1+1
    setCount((c) => c + 1) // 2+1
  }

  return (
    <div>
      <button onClick={handleClick}>点击我+1</button>
      <div>{count}</div>
    </div>
  )
}
export default App

8.6 状态不可变

默认情况下,修改状态的值跟上一次相同的情况下,不会触发重新渲染(注意内部可能会自检)

import { useState } from 'react'

function App() {
  const [list, setList] = useState([
    { id: 1, text: 'aaa' },
    { id: 2, text: 'bbb' },
    { id: 3, text: 'ccc' },
  ])

  const handleClick = () => {
    // 错误的做法,对于引用数据而言,并没有改变,所以不会触发重新渲染
    list.push({id:4,text:'ddd'})
    setList(list)
    // 正确的做法
    setList([...list, { id: 4, text: 'ddd' }])
  }

  return (
    <div>
      <button onClick={handleClick}>点击我添加一项</button>
      <ul>
        {list.map((item) => (
          <li key={item.id}>{item.text}</li>
        ))}
      </ul>
    </div>
  )
}
export default App

8.7 常见的对象和数组的解决方案

数组

image.png

import { useState } from 'react'
import { cloneDeep } from 'lodash'

function App() {
  const [list, setList] = useState([
    { id: 1, text: 'aaa' },
    { id: 2, text: 'bbb' },
    { id: 3, text: 'ccc' },
  ])

  const handleClick = () => {
    // 添加到最后一项
    setList([...list, { id: 4, text: 'ddd' }])
    // 添加到第二项
    setList([...list.slice(0, 1), { id: 4, text: 'ddd' }, ...list.slice(1)])
    // 删除id为3的项
    setList(list.filter((item) => item.id !== 3))
    // 将bbb替换为ddd
      setList(
        list.map((item) => {
          if (item.id === 2) {
            return { ...item, text: 'ddd' }
          } else {
            return item
          }
        })
      )
    // }
    // 倒序,需要先将数组复制一份
    // 拓展运算符,浅拷贝,只适用于一层的引用数据类型
    const cloneList = [...list]
    cloneList.reverse()
    setList(cloneList)
    // lodash库实现深拷贝
    const cloneList = cloneDeep(list)
    cloneList.reverse()
    setList(cloneList)
  }

  return (
    <div>
      <button onClick={handleClick}>点击按钮</button>
      <ul>
        {list.map((item) => (
          <li key={item.id}>{item.text}</li>
        ))}
      </ul>
    </div>
  )
}
export default App

对象

  • 整体修改
  • 利用拓展运算符,可以不用修改整体,但是如果想更新一个嵌套的属性,得多次使用(可以使用lodash库的深拷贝,但是比较耗费性能)
import { useState } from 'react'
import { cloneDeep } from 'lodash'

function App() {
  const [info, setInfo] = useState({
    name: {
      first: 'zhang',
      last: 'san',
    },
    age: 19,
    gender: '男',
  })

  const handleClick = () => {
    // 1.多个拓展运算符
    setInfo({
      ...info,
      name: {
        ...info.name,
        first: 'li',
      },
    })
    // 2.lodash库
   const cloneInfo = cloneDeep(info)
   cloneInfo.name.first = 'li'
   setInfo(cloneInfo)
  }

  return (
    <div>
      <button onClick={handleClick}>点击按钮</button>
      <div>{JSON.stringify(info)}</div>
    </div>
  )
}
export default App

8.8 immer插件简化不可变数据结构

安装:pnpm i immer use-immer

  • 如上面所示,对于多层嵌套的对象,处理起来很麻烦,且lodash库的深拷贝性能差,因此可以用 immer 来简化操作
  • immer是一个第三方模块,可以让你以更方便的方式处理不可变状态(相对于深拷贝,它使拷贝相对便宜,不需要复制数据树的未更改部分,并且在内存中与相同状态的旧版本共享)

操作对象

import { useImmer } from 'use-immer'

function App() {
  const [info, setInfo] = useImmer({
    name: {
      first: 'zhang',
      last: 'san',
    },
    age: 19,
    gender: '男',
  })

  const handleClick = () => {
    // draft相当于当前状态的一个副本
    setInfo((draft) => {
      draft.name.first = 'li'
    })
  }

  return (
    <div>
      <button onClick={handleClick}>点击按钮</button>
      <div>{JSON.stringify(info)}</div>
    </div>
  )
}
export default App

操作数组

import { useImmer } from 'use-immer'

function App() {
  const [info, setInfo] = useImmer([
    { id: 1, text: 'aaa' },
    { id: 2, text: 'bbb' },
    { id: 3, text: 'ccc' },
  ])

  const handleClick = () => {
    // draft相当于当前状态的一个副本,这样就可以使用数组的各种方法了
    setInfo((draft) => {
      draft.pop()
      draft.push({ id: 4, text: 'ddd' })
      draft.splice(1, 1, { id: 4, text: 'ddd' })
    })
  }

  return (
    <div>
      <button onClick={handleClick}>点击按钮</button>
      <ul>
        {info.map((item) => (
          <li key={item.id}>{item.text}</li>
        ))}
      </ul>
    </div>
  )
}
export default App

8.9 惰性初始化值

当状态的初始值需要经过复杂运算得到时,可以对其进行惰性初始化操作

import { useState } from 'react'

function computed(n) {
  console.log(123)
  return n + 1 + 2 + 3
}

function App() {
  const [count, setCount] = useState(computed(3))        // 每次都会打印123
  const [count, setCount] = useState(() => computed(3))  // 惰性初始化值
  const handleClick = () => {
    return setCount(count + 1)
  }
  return (
    <div>
      <button onClick={handleClick}>点击+1</button>
      <div>{count}</div>
    </div>
  )
}

export default App

8.10 状态提升来解决共享问题

多次渲染同一个组件,每个组件都会拥有自己的state。状态独立且不共享

image.png

  • 状态独立
import { useState } from 'react'

function Button() {
  const [count, setCount] = useState(0)
  const handleClick = () => {
    setCount(count + 1)
  }
  return (
    <div>
      <button onClick={handleClick}>按钮</button>
      // 行内样式小驼峰,需要vite配置后才能用
      <span style={{ marginLeft: '10px' }}>{count}</span>
    </div>
  )
}

function App() {
  return (
    <div>
      // 此时两个组件的状态state是独立的
      <Button></Button>
      <Button></Button>
    </div>
  )
}

export default App

image.png


  • 状态提升
import { useState } from 'react'

function Button({ count, onClick }) {
  return (
    <div>
      <button onClick={onClick}>按钮</button>
      <span style={{ marginLeft: '10px' }}>{count}</span>
    </div>
  )
}

function App() {
  // 状态提升到App组件中,实现状态共享
  const [count, setCount] = useState(0)
  const handleClick = () => {
    setCount(count + 1)
  }
  return (
    <div>
      <Button count={count} onClick={handleClick}></Button>
      <Button count={count} onClick={handleClick}></Button>
    </div>
  )
}

export default App

image.png

8.11 状态的重置问题

  • 当组件被销毁时,所对应的状态也会被重置
  • 当组件位置没有发生改变时,状态会被保留
  • 重置状态:1.不同的结构体 2.给组件添加key属性

import { useState } from 'react'

function Count() {
  const [count, setCount] = useState(0)
  const handleClick = () => {
    setCount(count + 1)
  }
  return (
    <div>
      <button onClick={handleClick}>点击+1</button>
      <div>{count}</div>
    </div>
  )
}

function App() {
  const [isShow, setIsShow] = useState(true)
  // 切换显示与隐藏
  const changeShow = () => {
    setIsShow(!isShow)
  }
  return (
    <div>
      <button onClick={changeShow}>切换显示隐藏</button>
      {isShow && <Count></Count>}
    </div>
  )
}

export default App

QQ20231125-111706-HD.gif


import { useState } from 'react'

function Counter({ style }) {
  const [count, setCount] = useState(0)
  const handleClick = () => {
    setCount(count + 1)
  }
  return (
    <div>
      <button style={style} onClick={handleClick}>
        计数
      </button>
      <div>{count}</div>
    </div>
  )
}

function App() {
  const [isStyle, setIsStyle] = useState(false)
  const handleClick = () => {
    setIsStyle(true)
  }
  return (
    <div>
      <button onClick={handleClick}>添加样式</button>
      {isStyle ? (
        <Counter style={{ border: '1px solid red' }}></Counter>
      ) : (
        <Counter></Counter>  // 结构相同,状态保留
        <div> <Counter></Counter> </div>  // 结构不同,会重置状态
        <Counter key='con2'></Counter>  // 给组件加key,会重置状态
      )}
    </div>
  )
}

export default App

2.gif

3.gif

8.12 状态的计算变量

由于状态会重新渲染函数组件,可以利用当前状态快照生成对应的计算变量

import { useState } from 'react'

function App() {
  const [count, setCount] = useState(0)
  // 计算变量,类似于vue中的计算属性
  const count2 = count * 2 
  const handleClick = () => {
    setCount(count + 1)
  }

  return (
    <div>
      <button onClick={handleClick}>点击</button>
      <div>{count}</div>
      <div>{count2}</div>
    </div>
  )
}

export default App

8.13 受控组件与非受控组件

  • 受控组件:通过props控制的组件
  • 非受控组件:通过state控制的组件

注意:React表单内置了受控组件的行为

  1. value + onChange(输入框和下拉框)
  2. checked + onChange(单选框和多选框)
import { useState } from 'react'

function App() {
  // 输入框
  const [value, setValue] = useState('')
  const handleChange = (e) => {
    setValue(e.target.value)
  }
  // 多选框
  const [checked, setChecked] = useState(false)
  const changeChecked = (e) => {
    setChecked(e.target.checked)
  }
  return (
    <div>
      {/* 输入框 */}
      <input type='text' value={value} onChange={handleChange} />
      <div>{value}</div>
      {/* 多选框 */}
      <input type='checkbox' checked={checked} onChange={changeChecked} />
      <div>{checked + ''}</div>
    </div>
  )
}

export default App

image.png

8.14 实战案例 todolist

4.gif

import { useState } from 'react'
import { useImmer } from 'use-immer'

function TaskList({ title, task, handleCheck }) {
  return (
    <div>
      <div>{title}</div>
      <ul>
        {task.map((item) => (
          <li key={item.id}>
            <input
              type='checkbox'
              checked={item.isOver}
              onChange={(e) => handleCheck(e, item.id)}
            />
            <span
              style={item.isOver ? { textDecoration: 'line-through' } : null}>
              {item.task}
            </span>
          </li>
        ))}
      </ul>
    </div>
  )
}

function Todo() {
  // 输入框
  const [value, setValue] = useState('')
  const onChange = (e) => {
    setValue(e.target.value)
  }
  // 任务列表
  const [task, setTask] = useImmer([])
  // 添加任务
  const onClick = () => {
    setTask((draft) => {
      draft.push({ id: task.length, task: value, isOver: false })
    })
    setValue('')
  }
  // 未完成的任务数
  const noOver = task.filter((item) => item.isOver === false)
  // 已完成的任务数
  const yesOver = task.filter((item) => item.isOver === true)

  // 点击多选框
  const handleCheck = (e, id) => {
    setTask((draft) => {
      draft.find((item) => item.id === id).isOver = e.target.checked
    })
  }

  return (
    <div>
      <input type='text' value={value} onChange={onChange} />
      <button onClick={onClick}>添加任务</button>
      <TaskList
        title={<h2>未完成的任务:{noOver.length}个</h2>}
        task={task.filter((item) => item.isOver === false)}
        handleCheck={handleCheck}></TaskList>
      <TaskList
        title={<h2>已完成的任务:{yesOver.length}个</h2>}
        task={task.filter((item) => item.isOver === true)}
        handleCheck={handleCheck}></TaskList>
    </div>
  )
}

export default Todo