2025-03-29 14:35:49 +08:00

38 KiB
Executable File
Raw Permalink Blame History

react

类似 vue 的全家桶: vite+react+zustand+swr

"Hook"可以被理解为一种设计模式或机制,它允许函数或模块之间以一种更加动态和灵活的方式进行交互。具体来说Hook 通常指的是一个函数,该函数可以在不修改原始代码的情况下扩展或改变某些行为  ref 和 reactive 是 Vue 的"Hook"

React: 状态不可变, vue 的状态是可变的

函数式编程: 1. 相同的输入总是得到相同的输出 2. 没有副作用不修改外部状态如全局变量、DOM 等)

简介

FaceBook 开源的一个构建用户界面的 JavaScript 库

官网

官方教程

核心库

  • react: 核心库
  • react-dom: DOM 操作库, 开发 web 应用

拆开原因: 像 react-navie 只需要核心库

babel.min.js 的作用:

  • 把 es6 转 es5
  • jsx 转 js

vscode 插件

ES7+ React/Redux/React-Native snippets :

// 快捷键rafce
import React from 'react'

const $1 = () => {
  return <div>$0</div>
}

export default $1

vscode-react-javascript-snippets/Snippets.md

VSCode React Refactor

vscode 编辑器 react 代码中开启 emmet 提示:

设置搜索 emmet => 找到 Emmet:Include Languages => 添加项 javascript 值 javascriptreact

hello React

React 18:

  三个API
  1. React.createElement(type, [props], [...children])
    用来创建React元素
    参数:
      1.1 元素名, html标签必须小写
      1.2 元素中的属性
        1.2.1 设置事件时, 属性名需要小驼峰
        1.2.2 class要改为className
      1.3. 元素中的子元素(内容)
  2. React.createRoot(container[, options])
    用来创建React的根容器, 容器用来放置React元素
  3. root.render(element)
    3.1 首次调用的时候, 容器节点里的所有DOM元素都会被替代, 后续调用会使用diff算法进行更新
    3.2 **React元素创建后无法修改, 只能通过创建新的元素进行替换(实际会调用diff算法, 只更新变化的部分)**
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <script src="https://unpkg.com/react@18/umd/react.development.js" crossorigin></script>
    <script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js" crossorigin></script>
  </head>
  <body>
    <div id="root"></div>
  </body>
  <script>
    // 1. React.createElement 创建React元素
    const button = React.createElement(
      'button',
      {
        id: 'btn',
        type: 'button',
        className: 'hello',
        onClick: () => alert(123),
      },
      'react创建的按钮'
    )

    // 2. ReactDOM.createRoot 创建React根元素, 需要提供一个DOM元素作为参数
    const root = ReactDOM.createRoot(document.getElementById('root'))

    // 3. root.render() 将React元素渲染到根元素中
    root.render(button)
  </script>
</html>

通过React.createElement创建元素很麻烦, 引入JSX (React.createElement 的语法糖)

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <script src="./js/react.development.js"></script>
    <script src="./js/react-dom.development.js"></script>
    <script src="./js/babel.min.js"></script>
  </head>
  <body>
    <div id="root"></div>
  </body>
  <script type="text/babel">
    // 1. JSX 会被babel翻译成React语法
    const button = (
      <div>
        <button id="btn" type="button" className="hello" onClick={() => alert(123)}>
          按钮
        </button>
      </div>
    )

    // 2. ReactDOM.createRoot 创建React根元素, 需要提供一个DOM元素作为参数
    const root = ReactDOM.createRoot(document.getElementById('root'))

    // 3. root.render() 将React元素渲染到根元素中
    root.render(button)
  </script>
</html>

React 17->18 的api发生了变化, ReactDOM.render(vm, container)替换成 ReactDOM.createRoot(container[, options]) + root.render(element)

React 17:

1. 准备一个用来挂载容器的标签
2. 引入核心库
3. 引入DOM操作库
4. 引入babel
5. 创建虚拟DOM
6. 把虚拟DOM渲染成真实DOM并挂载到容器, ReactDOM.render(vm, container)
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <!-- 准备一个用来挂载容器的标签 -->
    <div id="test"></div>

    <!-- 引入react库有顺序要求 -->
    <!-- 1. 引入核心库 -->
    <script type="text/javascript" src="./js/react.development.js"></script>

    <!-- 2. 引入DOM操作库 -->
    <script type="text/javascript" src="./js/react-DOM.development.js"></script>

    <!-- 3. 引入babel -->
    <script type="text/javascript" src="./js/babel.min.js"></script>

    <!-- 4. 编写jsx -->
    <!-- type类型必须是text/babel, 不是javascript -->
    <script type="text/babel">
      // 4.1 创建虚拟DOM
      const vm = <h1>hello react</h1>
      console.log('vm', vm)
      /**
      关于虚拟DOM
        1. 本质是Object类型的对象
        2. 虚拟Dom的属性远比真实Dom少
        3. 虚拟Dom最终被React转化为真实Dom挂载到页面上
    */

      // 4.2 挂载DOM到页面
      ReactDOM.render(vm, document.getElementById('test'))
    </script>
  </body>
</html>

creat-react-app

# npm
npx create-react-app react-app
# pnpm
pnpm dlx create-react-app react-app

设置默认打开浏览器:修改 'package.json'

  "scripts": {
    "start": "BROWSER='Google Chrome Beta' react-scripts start",
  },

备注: 使用的是 webpack

vite

# pnpm
pnpm create vite
# 然后选择配置
# 或者直接指定
pnpm create vite my-react-app --template react

JSX

JavaScript XML, 是一个 JS 的拓展, 需要通过 babel 转化成浏览器能执行的 js 代码

  • xml 是早期用来存储数据的(效率不高, 改用 Json)
<student>
	<name>jack</name>
  <age>18</age>
</student>
"{"student":{"name":"jack", "age":18}}"

语法:

  • 定义虚拟 dom 时不要用引号
  • 标签中混入 js 的表达式时, 用{}
  • 样式的类名不能用 class , 要用className
  • 内联样式要用 style={\{ key: value \}} 的方式, 第一个花括号表示里面是 js 表达式, 第二个花括号表示是对象
  • 虚拟 dom 只能有根标签
  • 标签必须闭合
  • 标签的首字母大小写
    • 小写, 先识别成 html 中的同名标签
    • 大写, 识别成 React 的组件
  • jsx 循环渲染
    • { 数组变量.map( item=>( <标签>{item}</标签> ) ) }
    • map()的箭头函数的方法用小括号包括 jsx, 使用{}会报错
    • map()循环生成的元素一定要加上 key
  • jsx 条件渲染
    • 用三元表达式
<script type="text/babel">
  // 4.1 创建虚拟DOM const title = '前端js框架' const list = ['Angular', 'React', 'Vue'] const vm ={' '}
  <div>
    <div style={{ fontSize: 30 + 'px' }}>{title}</div>
    {list.map((item, index) => {
      return <li key={index}>{item}</li>
    })}
  </div>
  // 4.2 挂载DOM到页面 ReactDOM.render(vm, document.getElementById('test'))
</script>
  • 写注释{/* jsx里面的注释 */}

组件

react 中组件有两种创建方式:

一: 函数式组件: 函数返回一个jsx, 函数首字母必须大写

二: 类组件: 类组件必须继承 React.Component, 类组件中, 必须添加一个render()方法, 且方法的返回值是jsx

// 函数组件
const App = () => {
  return <div>jsx</div>
}

export default App
// 类组件
import React from 'React'

class App extends React.Component {
  render() {
    return <div>jsx</div>
  }
}

export default App

事件

const clickHandler = event => {
  alert('我是提示')
  console.log(event)
  // 取消默认行为
  event.preventDefault()
  // 阻止冒泡
  event.stopPropagation()
}

const MyButton = () => {
  return <button onClick={clickHandler}>点我弹窗</button>
}

export default MyButton
  • 事件用小驼峰
  • react 中, 无法通过 return false 取消默认行为
  • react 事件中可以在相应函数中定义参数来接受事件对象, 事件对象是经过 react 包装过的, 不是原生的事件对象
  • 取消默认行为: event.preventDefault()
  • 阻止冒泡event.stopPropagation()

props: 父传子

  • 父组件通过props传递参数给子组件
  • 子组件通过形参接收父组件传来的所有参数
  • props是只读的,不能修改
/**
父组件中:通过属性传递
*/
<Child data1={Data1} data2={Data2}><Child/>

/**
子组件中通过props接收
*/
const Child = (props)=>{
  const {data1, data2} = props
  return <div><div/>
}

export default Clild;

useState

  • 在 React 中, 组件渲染完毕后, 再修改组件中的数据, 不会触发重新渲染

  • React 提供特殊变量 state, 改变后触发渲染(渲染前也会进行 diff)

  • 用 setState 改变 state, 改变的不是当前的 state而是下次渲染的 state

  • 渲染是异步的,会进入队列,最终进行合并

  • 当用 setState 需要用到旧 state 时, 有可能出现计算错误的情况(举例: 点击按钮,每次都是旧 state 加 1. 快速点两下state 只加 1

为了解决这种问题,给 setState 传入回调函数,回调函数接收旧的 state, 返回值将成为新的 state

import { useState } from 'react'
import './App.css'

const App = () => {
  const data = useState(1) // 传一个初始值,可以是常数或者数组等
  console.log('data', data)
  // 返回一个数组,第一个是初始值, 第二个是函数
  // 初始值只是用来显示的, 改变不会触发渲染
  // 函数通常命名为setXxx,参数为新的值
  const [value, setData] = data
  const dataChange = () => {
    setData(value + 1)
  }

  const [userInfo, updateUserInfo] = useState({ name: '小明', age: 10 })
  console.log('userInfo', userInfo)
  const updateUserInfoHandler = () => {
    updateUserInfo({ name: '小红' })
  }
  // 发现年龄没了
  // 当state是对象时, setState是用新的对象替换旧的对象

  const updateUserInfoHandler2 = () => {
    updateUserInfo({ ...userInfo, name: '小红' })
  }

  const updateUserInfoHandler3 = () => {
    userInfo.name = '小李'
    updateUserInfo(userInfo)
  }
  // 发现没有改变名字: 因为对象还是那个内存地址, 没有变化

  const updateUserInfoHandler4 = () => {
    setTimeout(() => {
      updateUserInfo({ ...userInfo, age: userInfo.age + 1 })
    }, 1000)
  }
  // 手速快点, 发现数值改变发生异常. setState改变的不是当前的state

  const updateUserInfoHandler5 = () => {
    setTimeout(() => {
      // 给setState传回调函数, 返回值将作为新的state
      updateUserInfo(prev => {
        return { ...prev, age: prev.age + 1 }
      })
    }, 1000)
  }

  return (
    <div>
      <div>{value}</div>
      <button onClick={dataChange}>更改</button>
      <div>
        {userInfo.name}-{userInfo.age}
      </div>
      <button onClick={updateUserInfoHandler}>改名1</button>
      <button onClick={updateUserInfoHandler2}>改名2</button>
      <button onClick={updateUserInfoHandler3}>改名3</button>
      <button onClick={updateUserInfoHandler4}>年龄加1</button>
      <button onClick={updateUserInfoHandler5}>年龄加1</button>
    </div>
  )
}

export default App

useRef 获取原生 dom

  • React 中的钩子函数只能用于函数组件或者自定义钩子
  • 钩子函数只能在函数组件中调用
  1. 创建 ref 容器
  2. 绑定到标签的 ref 属性
import { useRef } from 'react'
import './App.css'

const App = () => {
  const myRef = useRef() // 用来存放dom的容器
  console.log('myRef', myRef) // 和手动创建的{current:undefined}的区别
  // 手动创建的在重新渲染后都发生变化
  // ureRef创建的可以确保每次渲染后都是同一个对象(生命周期内)

  const clickHandler = () => {
    console.log('myRef', myRef) // 存到current属性里
    console.log('myRef', myRef.current)
  }

  return (
    <div>
      <div ref={myRef}>ref</div>
      <button onClick={clickHandler}>按钮</button>
    </div>
  )
}

export default App

插槽

通过props.children接收插槽的内容

通过props.chlaName和模板字符串 合并类名

import React from 'react'
import './card.css'

const Card = props => {
  return (
    <div className={`card${props.className ? ' ' + props.className : ''}`}>>{props.children}</div>
  )
}

export default Card

双向绑定

页面 <<==> > 数据

  • useState 初始化值, 把state绑定到 input 的 value 上
  • 当 input 发生变化的时候, 通过setState更新 state
  • 当 state 发生变化的时候, 触发重新渲染. 例如格式化 state 为初始值, input 的内容重新渲染为空
import { useState } from 'react'

const NoteForm = () => {
  // 表单初始数据
  const [formData, setFormData] = useState({
    date: '',
    desc: '',
    time: '',
  })

  // 更新表单数据
  const inputChangeHandler = e => {
    setFormData(prev => {
      return { ...prev, [e.target.id]: e.target.value }
    })
  }

  const formSubmitHandler = e => {
    e.preventDefault() // 阻止表单默认行为
    // todo 校验表单
    // todo 提交表单
    const formDataFormat = {
      data: Date.parse(new Date(formData.date)),
      desc: formData.desc,
      time: +formData.time,
    }
    console.log('提交表单', formDataFormat)
    setFormData(() => ({
      date: '',
      desc: '',
      time: '',
    }))
  }

  return (
    <div className="note-form">
      <form onSubmit={formSubmitHandler}>
        <div className="form-item">
          <label htmlFor="date">日期:</label>
          <input
            type="date"
            name="date"
            id="date"
            onChange={inputChangeHandler}
            value={formData.date}
          />
        </div>
        <div className="form-item">
          <label htmlFor="desc">内容:</label>
          <input
            type="text"
            name="desc"
            id="desc"
            autoComplete="off"
            onChange={inputChangeHandler}
            value={formData.desc}
          />
        </div>
        <div className="form-item">
          <label htmlFor="time">时长:</label>
          <input
            type="number"
            name="time"
            id="time"
            onChange={inputChangeHandler}
            value={formData.time}
          />
        </div>
        <button className="form-btn">添加</button>
      </form>
    </div>
  )
}

export default NoteForm

子传父: props 中传函数, 子组件中调用

  • 把更新数据的函数绑定到标签的onXxx属性上
  • 子组件同样通过props形参接受所有父组件传递的数据
  • 子组件通过props.onXxx(newValue)向父组件的更新函数传递新数据
  • 父组件的更新函数被调用, 拿到新的数据, 通过 setState 更新数据(作用域在父组件中)

portal 传送门

可以将子节点渲染到存在于父组件以外的 DOM 节点

例如: 子组件弹全局提示窗

// 1.在index.html需要的位置添加标签
// <div id="backdrop-root"></div>

// 2. 修改组件的渲染方式
// 2.1 import ReactDOM from 'react-dom'
// 2.2 通过ReactDOM.createPortal()作为返回值创建元素
//    参数: 1.jsx
//         2.DOM元素, 可以通过document.getElementById('backdrop-root')获取

Fragment

jsx 中要求虚拟 dom 有根标签, 但实际中不想增加一个没用的 div

import {Fragment} from 'React'
// ...
<Fragment>
	<div>1</div>
  <div>2</div>
  <div>3</div>
<Fragment>
// 甚至提供了语法糖, 不用引入, 直接用空标签
<>
  <div>1</div>
  <div>2</div>
  <div>3</div>
</>

相当于创建一个空白的组件

const Fragment = props => {
  return props.children
}

export default Fragment

然后用来当容器

import Fragment from './Fragment.js'

const Test = () => {
  return (
    <Fragment>
      <div>1</div>
      <div>2</div>
    </Fragment>
  )
}

export default Test

React 中的 css

  • 在 jsx 中使用内联样式: style={\{key:value\}}

  • 外联样式表: import 导入.css, 通过 className控制, 没有作用域, 全局作用

  • css 模块:

    1. 创建xxx.module.css
    2. import xx form './xxx.module.css'
    3. jsx 中, className={xx.p1}, xx 是模块名, p1 是 css 模块文件中的类名

    好处:

    p1 这些 class 并不是最终实际的 class, react 会自动生成唯一的 class

移动端适配

// 移动端适配
// 常见设计稿750px * 1340px
// 1px/750px * 100vw, 750对应设计稿宽度,分成750份,把1份的大小当成根字体大小
document.documentElement.style.fontSize = 100/750 + 'vw'
// 移动端适配
const autoResize = ()=>{
  const devicewidth = document.documentElement.clientWidth
  // 常见设计稿750px * 1340px
  if(devicewidth>750){
    document.documentElement.style.fontSize = '1px'
  }else{
    // 1px/750px * 100vw, 750对应设计稿宽度,分成750份,把1份的大小当成根字体大小
    document.documentElement.style.fontSize = 100/750 + 'vw'
  }
}
autoResize()
let timeId = ''
window.onresize = ()=>{
  timeId = setTimeout(() => {
    clearTimeout(timeId)
    autoResize()
  }, 500);
}

Context useContext

不再限制于 props 一层层传递数据, 在外层统一设置, 在内层所有组件都可以访问到

/**
定义React.createContext(), 一般不使用初始值
*/
import React from 'react'
const TestContext = React.createContext({ name: 'Tom', age: 18 })
export default TestContext
/**
数据生产xx.provider标签通过value定义数据
context有就近原则<B/>的数据生产者是最近的一层context
*/
import TestContext from 'xxx'

const Xx = ()=>{
  <TestContext.provider value={{name: 'Jane', age: 19}}>
    <A/>
    <TestContext.provider value={{name: 'July', age: 17}}
    	<B/>
    <TestContext.provider/>
  <TestContext.provider/>
}
/**
数据消费: 使用钩子 useContext
*/
import TestContext from 'xxx'

const A = ()=>{
  const ctx = useContext(TestContext)

  return <div>name: {ctx.name} - age: {ctx.age}<div>
}
export default A

副作用Effect

有部分逻辑直接写在函数体中,会影响组件的渲染,这部分代码会产生"副作用"。(例如修改 state 写到组件中,可能导致死循环渲染)

React.StrictMode

React 的严格模式,在开发模式下,会主动重复调用一些函数,以使副作用凸显。所以在开发模式且开启严格模式,这些函数会被调用两次:

  • 函数组件的函数体
  • 参数为函数的setState
  • 参数为函数的useStateuseMemouseReducer

重渲染案例

import { useState } from 'react'
import B from './components/B.jsx'

function App() {
  console.log('重渲染')
  const [count, setCount] = useState(0)
  // setCount(0)
  // 直接在函数体中调用setState, Too many re-renders
  /** 为什么初始值和新值一样,还会触发重渲染?
   * setState()的执行流程(函数组件)
   *    setCount() --> dispatchSetData()
   *                   会先判断当前组件处于什么阶段
   * 1. 渲染阶段: 不会检查state值是否相同
   * 2. 非渲染阶段: 会检查state是否相同
   *     2.1 不相同, 进行渲染
   *     2.3 相同,不渲染
   *
   * */

  /** 为什么初始值0, 第二次点按钮会重渲染, 第三次才不会重渲染
   * 0 ----> 1 ----> 1 --> 1
   *   打印    打印
   *  非渲染阶段值相同: 在一些情况下会继续执行当前组件的重渲染, 但不会触发子组件的渲染, 且这次渲染不会产生实际的效果(通常发生在第一次相同)
   *
   */

  return (
    <div className="App">
      <button onClick={() => setCount(1)}>count is {count}</button>

      <B />
    </div>
  )
}

export default App
<!--B.jsx-->
import React from 'react';

function B(props) {
  console.log('B组件重新渲染');
  return (
    <div>
      我是子组件
    </div>
  );
}

export default B;

useEffect

将会产生副作用的函数写在useEffect的回调函数里

useEffect(() => setCount(0))
/** useEffect(fn, [])
 * fn作为第一个参数的函数将在组件渲染完成后执行
 * 第二个可选参数是依赖项,只有依赖项发生变化,才执行. 数组中包含了**所有**外部作用域中会发生变化且在 effect 中使用的变量
 *   useState创建的setState在每次渲染获取都是相同的,写不写没关系
 *   如果传空数组[], effect只会在初始化时触发一次
 */

清除 effect

useEffect(() => {
  // 防抖
  const timeId = setTimeout(() => {
    props.onSearch(keyword)
  }, 1000)
  return () => {
    // 下次effect执行, 可以用来清理上次effect的影响
    clearTimeout(timeId)
  }
}, [keyword])
// return 返回一个清除函数, 在下次effect先执行

useReducer

useState 的替代方案

当 state 过于复杂时, 使用 useReducer 进行整合

/** useReducer(reducer, initialArg, init)
 * reducer: 整合函数
 *          对state的所有操作都应该在该函数中定义
 *          该函数的返回值, 会成为state的新值
 *          reducer执行时会收到两个参数:
 *              第一个参数: 当前state最新的值
 *              第二个参数: action: 对象, 从countDispatch 传进来的值
 *         为了避免每次渲染都重新创建reducer,一般写在函数组件外面
 * initialArg: state的初始值, 作用和uueState()中的值一样
 * init: 函数, 可选参数, 惰性初始化, 初始state就由它决定,而不再是第二个参数
 * @return
 *    数组:
 *        第一个参数: state
 *        第二个参数: state 修改的派发器
 *              具体通过reducer执行
 */
import { useState, useReducer } from 'react'
import './App.css'

const initCounter = () => {
  return 10
}

const countReducer = (state, action) => {
  switch (action.type) {
    case 'add':
      return state + 1
    case 'sub':
      return state - 1
    default:
      return state
  }
}

function App() {
  // useState的写法
  // const [count, setCount] = useState(0)
  // const addHandler = ()=>{
  //   setCount(prev=>prev+1)
  // }
  // const subHandler = ()=>{
  //   setCount(prev=>prev-1)
  // }

  // 改用useReducer
  // const [count,countDispatch] = useReducer(countReducer, 1)
  const [count, countDispatch] = useReducer(countReducer, 1, initCounter)

  const addHandler = () => {
    countDispatch({ type: 'add' })
  }
  const subHandler = () => {
    countDispatch({ type: 'sub' })
  }

  return (
    <div className="App">
      <div className="card">
        count is {count}
        <div>
          <button onClick={addHandler}>增加</button>
          <button onClick={subHandler}>减少</button>
        </div>
      </div>
    </div>
  )
}

export default App

React.memo

React 组件会在两种情况下重新渲染

  1. 组件自身的 state 发生变化
  2. 组件的父组件重新渲染

如果子组件的 state 并没有发生变化, 但父组件重新渲染导致子组件也重新渲染, 为了减少性能损失, 可以用React.memo()

/**
 * React.memo()是高阶组件
 *    接收另一个组件作为参数,返回一个包装后的新组件
 *    包装后的新组件具有缓存功能
 *         只有组件的props发生变化,才会触发组件的重新渲染,否则总是返回缓存中的结果
 */
import { useState } from 'react'
import './App.css'
import B from './components/B'
import C from './components/C'

function App() {
  console.log('A组件重渲染')
  const [count, setCount] = useState(0)

  return (
    <div className="App">
      <button onClick={() => setCount(count => count + 1)}>count is {count}</button>
      <B />
      <C count={count} />
    </div>
  )
}

export default App
import React from 'react'

const B = () => {
  console.log('B组件重渲染')
  s
  return <div>B组件</div>
}

export default React.memo(B)
import React from 'react'

const C = props => {
  console.log('C组件重新渲染')
  return <div>props传过来的count: {props.count}</div>
}

export default React.memo(C)

useCalkback

/**
 * useCallback()
 * 钩子函数,用来创建React中的回调函数
 * 创建的回调函数可以在组件重新渲染时不重新创建
 * 参数:
 *    1.回调函数
 *    2.依赖数组
 *        - 当依赖数组中的变量发生变化时,回调函数才会重新创建
 *        - 如果不指定依赖数组(不传),回调函数每次都会重新创建
 *        - 一定要把回调函数用到的变量都放到数组里, 除了setState
 *              不放的话,变量的作用域只会停留在第一次创建的时候
 */
import { useCallback } from 'react'
import { useState } from 'react'
import './App.css'
import B from './components/B'
import C from './components/C'
import D from './components/D'
import E from './components/E'

function App() {
  console.log('A组件重渲染')

  const [count, setCount] = useState(0)
  const [num, setNum] = useState(1)

  const addCount = () => {
    setCount(prev => prev + 1)
  }

  /**
   * useCallback()
   */
  const addCount2 = useCallback(() => {
    setCount(prev => prev + 1)
  }, [])

  const cAddN = useCallback(() => {
    setCount(prev => prev + num)
    setNum(prev => prev + 1)
  }, [])
  /**
   * 没有收集num,num固定是第一次的1
   */

  const cAddN2 = useCallback(() => {
    setCount(prev => prev + num)
    setNum(prev => prev + 1)
  }, [num])
  /**
   * num放到依赖数组,num的作用域会跟着变化
   */

  return (
    <div className="App">
      <div>
        <div>count is {count}</div>
        <div>num is {num}</div>
      </div>
      <B addCount={addCount} />
      <C addCount={addCount2} />
      <D cAddN={cAddN} />
      <E cAddN2={cAddN2} />
    </div>
  )
}

export default App
import React from 'react'

const B = props => {
  console.log('B组件重渲染')
  return (
    <div>
      B组件
      <button onClick={props.addCount}>+</button>
    </div>
  )
}

export default React.memo(B)
/**
 * B组件也重新渲染: props里的addCount在A组件重新渲染后重新创建
 */
import React from 'react'

const C = props => {
  console.log('C组件重渲染')
  return (
    <div>
      <hr />
      C组件
      <div>
        回调使用useCallback
        <button onClick={props.addCount}>count+1</button>
      </div>
    </div>
  )
}

export default React.memo(C)
import React from 'react'

const D = props => {
  console.log('D组件重渲染')
  return (
    <div>
      <hr />
      D组件
      <div>
        变量没有放到依赖数组
        <button onClick={props.cAddN}>count=count+num</button>
      </div>
    </div>
  )
}

export default React.memo(D)
import React from 'react'

const E = props => {
  console.log('E组件重渲染')
  return (
    <div>
      <hr />
      E组件
      <div>
        变量放到依赖数组
        <button onClick={props.cAddN2}>count=count+num</button>
      </div>
    </div>
  )
}

export default React.memo(E)

hooks

React 中的钩子只能在函数组件或者自定义钩子中调用

自定义钩子: 本质就是普通函数, 命名上以 use 开头

例子: useFetch

import {useState} from "react"
const baseURL = import.meta.env.VITE_HOST

const useFetch = ({url,type='GET',headers,successCallback,errorCallback}) => {
  const [loading, setLoading] = useState(false)
  const fetchInit = {
    method: type,
    headers: headers || {
      'Content-Type': 'application/json',
    }
  }
  const fetchData = async (body)=>{
    if (loading) {
      console.log('等待服务器响应,请勿重复提交!')
      return
    } else {
      setLoading(true)
    }
    try {
      if(!!body) {
        fetchInit.body = JSON.stringify(body)
      }
      const res = await fetch(baseURL+'/api/'+url, fetchInit)
      if (res.statusText === 'OK') {
        const resData = await res.json()
        successCallback && successCallback(resData)
      } else {
        errorCallback && errorCallback()
        throw new Error(res.statusText)
      }
    } catch (error) {
      console.log(error.message)
    } finally {
      setLoading(false)
    }
  }

  return {
    loading,
    setLoading,
    fetchData
  }
}

export default useFetch

使用

import useFetch from '@/hooks/useFetch'

// 在函数组件中
//...
// 修改学生/添加学生
const { loading: updateLoading, fetchData: updateStu } = useFetch({
  url: props.stu ? 'students/' + props.stu?.id : '/students',
  type: props.stu ? 'PUT' : 'POST',
  successCallback: ctx.fetchStuList,
})
const submitHandler = () => {
  if (checkStu(stu) == false) {
    console.log('检查输入')s
    return
  }
  updateStu({ data: stu })
}
//...

useMemo

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

把"创建"函数和依赖项数组作为参数传入 useMemo,它仅会在某个依赖项改变时才重新计算 memoized 值。这种优化有助于避免在每次渲染时都进行高开销的计算。

传入 useMemo 的函数会在渲染期间执行。不要在这个函数内部执行不应该在渲染期间内执行的操作,诸如副作用这类的操作属于 useEffect 的适用范畴,而不是 useMemo

useMemo 缓存的是函数的执行结果, useCallbakc 缓存的是函数, React.memo 是在子组件中使用

useMemo 也可以用来缓存 jsx/组件

如果没有提供依赖项数组,useMemo 在每次渲染时都会计算新的值。

import { useState, useMemo } from 'react'

const sum = (a, b) => {
  console.log('执行求和代码了')
  const time = Date.now()
  while (true) {
    // 模拟逻辑特别复杂的代码
    if (Date.now() - time > 2000) {
      break
    }
  }
  return a + b
}

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

  // const num = sum(123,456)
  // 缓存的是函数的执行结果
  const num = useMemo(() => {
    return sum(123, 456)
  }, [])

  return (
    <div className="App">
      <p>sum:{num}</p>
      <p>count:{count}</p>
      <button
        onClick={() => {
          setCount(prev => prev + 1)
        }}>
        +1
      </button>
    </div>
  )
}

export default App

useImperativeHandle

// 无法直接获取react组件的dom对象
// 例如 cont myref = useRef()
// <Child ref={myref} />
// 这样是获取不到的

// 用React.forwardRef()把组件包起来
// useImperativeHandle()自定义要返回的ref

// 把子组件的东西暴露给父组件,但不是直接暴露dom给父组件操作, 而是可以控制要暴露哪些值或方法
useImperativeHandle(ref, createHandle, [deps])
// 子组件
import { useState,useImperativeHandle } from 'react'
import React from 'react'

// 多了第二个参数ref
const Child = (props,ref) => {
  const [inpuValue, setInputValue] = useState('')
  const inputChangeHandler = (e)=>{
    setInputValue(+e.target.value)
  }
  useImperativeHandle(ref,()=>{
    // 决定要暴露哪些给父组件, 这里暴露了值和修改值的方法, 而不是直接暴露dom给父组件修改 
    return {
      setInputValue,
      value: inpuValue
    }
  })
  return (
    <input type='value' value={inpuValue} onChange={inputChangeHandler}/>
  )
}

export default React.forwardRef(Child)
import { useRef } from 'react'
import Child from './components/Child'

const App = () => {
  const childRef = useRef()

  return (
    <div className="App">
      <button
        onClick={() => {
          console.log('childRef',childRef)
          // 只能通过暴露出来的方法修改子组件
          childRef.current.setInputValue(childRef.current.value+1)
        }}>
        Child_input+1
      </button>
      <Child ref={childRef}/>
    </div>
  )
}

export default App

useLayoutEffect

  flowchart TB
  %%classDef
  classDef Purpule fill:#682b93,color:white;
  classDef Blue fill:#3b67b9,color:white;
  
  subgraph useInsertionEffect
    direction TB
    1[组件挂载]-->2[state改变]-->3[useInsertionEffect]-->4[DOM改变]-->5[绘制屏幕]
  end

  subgraph useLayoutEffect
    direction TB
    A[组件挂载]-->B[state改变]-->C[DOM改变]-->D[useLayoutEffect]-->E[绘制屏幕]
  end

  subgraph useEffect
    direction TB
    a[组件挂载]-->b[state改变]-->c[DOM改变]-->d[绘制屏幕]-->e[useEffect]
  end
  
  %%Style
  class d,5,E Purpule;
  class e,3,D Blue;

useLayoutEffect 的方法签名和 useEffect 一样功能也类似。不同点在于useLayoutEffect 的执行时机要早于 useEffect它会在 DOM 改变后调用。在老版本的 React 中它和 useEffect 的区别比较好演示React 18中useEffect 的运行方式有所变化,所以二者区别不好演示。

uselayoutEffect 使用场景不多,实际开发中,在 effect 中需要修改元素样式,且使用 useEffect 会出现闪灯现象时可以使用 uselayoutEffect 进行昔换。

useInsertionEffect 场景: 用来设置动态样式, 在性能上减少重渲染和 dom 操作

import { useRef,useLayoutEffect,useEffect,useInsertionEffect } from 'react'

const App = () => {
  const myRef = useRef()
  useEffect(() => {
    console.log('useEffect', myRef) // 3.屏幕渲染之后,可能有白屏
  })
  useLayoutEffect(() => {
    console.log('useLayoutEffect', myRef) // 2.屏幕渲染之前
  })
  useInsertionEffect(() => {
    console.log('useInsertionEffect', myRef) // 1. undefined
  })

  return (
    <div className="App">
      <div ref={myRef}>123</div>
    </div>
  )
}

export default App

useDebugValue

可以用来给钩子打标签, 开发者插件中可以看到

// 自定义勾子
import { useEffect,useDebugValue } from 'react'

const useMyhook = () => {
  useDebugValue('标签')
  useEffect(()=>{
    console.log('自定义钩子')
  })
}

export default useMyhook

useDeferredValue

设置一个延迟的 state

/**
 * 当多个组件使用同一个state时, 组件可能相互影响
 * 一个组件卡顿,导致所有组件卡顿
 * 此时设置延迟值,传给卡顿的组件,快的组件就不用等卡顿的组件了
 * 实际体验: 快速改动value,触发deffredValue快速变化, <List />重渲染还是卡 
*/
// 模拟卡顿的组件
import React from 'react'

const items = []
for (let index = 0; index < 30; index++) {
  items.push(`商品序号${index + 1}`)
}

const List = props => {
  const filterList = (() => {
    if (props.keyWord) {
      return items.filter(item => {
        return item.indexOf(props.keyWord) !== -1
      })
    } else {
      return items
    }
  })()

  // 模拟能卡的组件
  const begin = Date.now()
  while(true){
    if(Date.now() - begin > 3000){
      break
    }
  }

  return (
    <ul>
      {filterList.map(item => {
        return <li key={item.substr(3)}>{item}</li>
      })}
    </ul>
  )
}

export default React.memo(List)
import { useDeferredValue } from 'react'
import { useState } from 'react'
import List from './components/List'

const App = () => {
  console.log('组件重新渲染了')
  const [value, setValue] = useState(0)
  // useDeferredValue需要一个state作为参数
  // 设置了延迟后, 每次修改state都会触发两次渲染
  // 第一次'延迟值'是旧值, 第二次延迟值'是新值
  // 延迟值总比原state慢

  const deffredValue = useDeferredValue(value)
  console.log('value', value)
  console.log('deffredValue', deffredValue)

  // 组件重新渲染了
  // value 1
  // deffredValue 0
  // 组件重新渲染了
  // value 1
  // deffredValue 1
  /**
   * 当多个组件使用同一个state时, 组件可能相互影响
   * 一个组件卡顿,导致所有组件卡顿
   * 此时设置延迟值,传给卡顿的组件,快的组件就不用等卡顿的组件了
   * 实际体验: 快速改动value,触发deffredValue快速变化, <List />重渲染还是卡
   */

  return (
    <div className="App">
      <input
        type="text"
        value={value}
        onChange={e => {
          setValue(e.target.value)
        }}
      />
      {/* <List keyWord={value}/> */}
      <List keyWord={deffredValue} />
    </div>
  )
}

export default App

useTransition

让某个 setState 比其他的 setState 更慢执行

// 模拟性能很差的组件
import React from 'react'

const items = []
for (let index = 0; index < 30; index++) {
  items.push(`商品序号${index + 1}`)
}

const List = props => {
  const filterList = (() => {
    if (props.keyWord) {
      return items.filter(item => {
        return item.indexOf(props.keyWord) !== -1
      })
    } else {
      return items
    }
  })()

  // 模拟能卡的组件
  const begin = Date.now()
  while(true){
    if(Date.now() - begin > 3000){
      break
    }
  }

  return (
    <ul>
      {filterList.map(item => {
        return <li key={item.substr(3)}>{item}</li>
      })}
    </ul>
  )
}

export default React.memo(List)

import { useTransition } from 'react'
import { useState } from 'react'
import List from './components/List'

const App = () => {
  console.log('重新渲染')
  const [value, setValue] = useState('')
  const [keyWord, setKeyWord] = useState('')

  // isPending可以获取startTransition的状态
  const [isPending, startTransition] = useTransition()

  const changeHandler = e => {
    setValue(e.target.value)
    // startTransition的回调函数中的setState会在其他的setState都生效后才执行
    startTransition(() => {
      setKeyWord(e.target.value)
    })
  }
  return (
    <div className="App">
      <input type="text" value={value} onChange={changeHandler} />
      {!isPending && <List keyWord={keyWord} />}
    </div>
  )
}

export default App

useDeferredValue 和 useTransition 都代替不了节流防抖