# react > 类似 vue 的全家桶: vite+react+zustand+swr > **"Hook"可以被理解为一种设计模式或机制,它允许函数或模块之间以一种更加动态和灵活的方式进行交互**。具体来说,Hook 通常指的是一个函数,该函数可以在不修改原始代码的情况下扩展或改变某些行为 >  `ref` 和 `reactive` 是 Vue 的"Hook" > React: 状态不可变, vue 的状态是可变的 > 函数式编程: 1. 相同的输入总是得到相同的输出 2. 没有副作用(不修改外部状态,如全局变量、DOM 等) ## 简介 > FaceBook 开源的一个构建用户界面的 JavaScript 库 [官网](https://zh-hans.reactjs.org/) [官方教程](https://zh-hans.reactjs.org/docs/getting-started.html) ## 核心库 * react: 核心库 * react-dom: DOM 操作库, 开发 web 应用 拆开原因: 像 react-navie 只需要核心库 babel.min.js 的作用: * 把 es6 转 es5 * jsx 转 js ## vscode 插件 `ES7+ React/Redux/React-Native snippets` : ```jsx // 快捷键rafce import React from 'react' const $1 = () => { return
$0
} export default $1 ``` [vscode-react-javascript-snippets/Snippets.md](https://github.com/ults-io/vscode-react-javascript-snippets/blob/HEAD/docs/Snippets.md) `VSCode React Refactor` vscode 编辑器 react 代码中开启 emmet 提示: `设置搜索 emmet => 找到 Emmet:Include Languages => 添加项 javascript 值 javascriptreact` ## hello React React 18: ```markdown 三个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算法, 只更新变化的部分)** ``` ```html Document
``` 通过`React.createElement`创建元素很麻烦, 引入`JSX` (React.createElement 的语法糖) ```jsx Document
``` React 17->18 的`api`发生了变化, `ReactDOM.render(vm, container)`替换成 `ReactDOM.createRoot(container[, options])` + `root.render(element)` React 17: ```markdown 1. 准备一个用来挂载容器的标签 2. 引入核心库 3. 引入DOM操作库 4. 引入babel 5. 创建虚拟DOM 6. 把虚拟DOM渲染成真实DOM并挂载到容器, ReactDOM.render(vm, container) ``` ```html Document
``` ## creat-react-app ```sh # npm npx create-react-app react-app # pnpm pnpm dlx create-react-app react-app ``` 设置默认打开浏览器:修改 'package.json' ```json "scripts": { "start": "BROWSER='Google Chrome Beta' react-scripts start", }, ``` 备注: 使用的是 webpack ## vite ```sh # pnpm pnpm create vite # 然后选择配置 # 或者直接指定 pnpm create vite my-react-app --template react ``` ## JSX > JavaScript XML, 是一个 JS 的拓展, 需要通过 babel 转化成浏览器能执行的 js 代码 * xml 是早期用来存储数据的(效率不高, 改用 Json) ```xml jack 18 ``` ```json "{"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 条件渲染 * 用三元表达式 ```js ``` * 写注释`{/* jsx里面的注释 */}` ## 组件 react 中组件有两种创建方式: 一: 函数式组件: 函数返回一个`jsx`, 函数首字母必须`大写` 二: 类组件: 类组件必须继承 `React.Component`, 类组件中, 必须添加一个`render()`方法, 且方法的返回值是`jsx` ```js // 函数组件 const App = () => { return
jsx
} export default App ``` ```js // 类组件 import React from 'React' class App extends React.Component { render() { return
jsx
} } export default App ``` ## 事件 ```js const clickHandler = event => { alert('我是提示') console.log(event) // 取消默认行为 event.preventDefault() // 阻止冒泡 event.stopPropagation() } const MyButton = () => { return } export default MyButton ``` * 事件用小驼峰 * react 中, 无法通过 return false 取消默认行为 * react 事件中可以在相应函数中定义参数来接受事件对象, 事件对象是经过 react 包装过的, 不是原生的事件对象 * 取消默认行为: `event.preventDefault()` * 阻止冒泡`event.stopPropagation()` ## props: 父传子 * 父组件通过`props`传递参数给子组件 * 子组件通过形参接收父组件传来的所有参数 * **props**是只读的,不能修改 ```js /** 父组件中:通过属性传递 */ /** 子组件中:通过props接收 */ const Child = (props)=>{ const {data1, data2} = props return
} export default Clild; ``` ## useState > * 在 React 中, 组件渲染完毕后, 再修改组件中的数据, 不会触发重新渲染 > > * React 提供特殊变量 state, 改变后触发渲染(渲染前也会进行 diff) > > * 用 setState 改变 state, 改变的不是当前的 state,而是下次渲染的 state > > * 渲染是异步的,会进入队列,最终进行合并 > > * 当用 setState 需要用到旧 state 时, 有可能出现计算错误的情况(举例: 点击按钮,每次都是旧 state 加 1. 快速点两下,state 只加 1) > > 为了解决这种问题,给 setState 传入回调函数,回调函数接收旧的 state, 返回值将成为新的 state ```js 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 (
{value}
{userInfo.name}-{userInfo.age}
) } export default App ``` ## useRef 获取原生 dom * React 中的钩子函数只能用于函数组件或者自定义钩子 * 钩子函数只能在函数组件中调用 1. 创建 ref 容器 2. 绑定到标签的 ref 属性 ```js 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 (
ref
) } export default App ``` ## 插槽 通过`props.children`接收插槽的内容 通过`props.chlaName`和模板字符串 合并类名 ```js import React from 'react' import './card.css' const Card = props => { return (
>{props.children}
) } export default Card ``` ## 双向绑定 > 页面 <<==> > 数据 * 用`useState` 初始化值, 把`state`绑定到 input 的 value 上 * 当 input 发生变化的时候, 通过`setState`更新 state * 当 state 发生变化的时候, 触发重新渲染. 例如格式化 state 为初始值, input 的内容重新渲染为空 ```js 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 (
) } export default NoteForm ``` ## 子传父: props 中传函数, 子组件中调用 * 把更新数据的函数绑定到标签的`onXxx`属性上 * 子组件同样通过`props`形参接受所有父组件传递的数据 * 子组件通过`props.onXxx(newValue)`向父组件的更新函数传递新数据 * 父组件的更新函数被调用, 拿到新的数据, 通过 setState 更新数据(作用域在父组件中) ## portal 传送门 > 可以将子节点渲染到存在于父组件以外的 DOM 节点 > > 例如: 子组件弹全局提示窗 ```js // 1.在index.html需要的位置添加标签 //
// 2. 修改组件的渲染方式 // 2.1 import ReactDOM from 'react-dom' // 2.2 通过ReactDOM.createPortal()作为返回值创建元素 // 参数: 1.jsx // 2.DOM元素, 可以通过document.getElementById('backdrop-root')获取 ``` ## Fragment jsx 中要求虚拟 dom 有根标签, 但实际中不想增加一个没用的 div ```jsx import {Fragment} from 'React' // ...
1
2
3
``` ```js // 甚至提供了语法糖, 不用引入, 直接用空标签 <>
1
2
3
``` 相当于创建一个空白的组件 ```js const Fragment = props => { return props.children } export default Fragment ``` 然后用来当容器 ```js import Fragment from './Fragment.js' const Test = () => { return (
1
2
) } 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 ## 移动端适配 ```js // 移动端适配 // 常见设计稿750px * 1340px // 1px/750px * 100vw, 750对应设计稿宽度,分成750份,把1份的大小当成根字体大小 document.documentElement.style.fontSize = 100/750 + 'vw' ``` ```js // 移动端适配 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 一层层传递数据, 在外层统一设置, 在内层所有组件都可以访问到 ```js /** 定义:React.createContext(), 一般不使用初始值 */ import React from 'react' const TestContext = React.createContext({ name: 'Tom', age: 18 }) export default TestContext ``` ```js /** 数据生产:xx.provider标签通过value定义数据 context有就近原则,的数据生产者是最近的一层context */ import TestContext from 'xxx' const Xx = ()=>{ } ``` ```js /** 数据消费: 使用钩子 useContext */ import TestContext from 'xxx' const A = ()=>{ const ctx = useContext(TestContext) return
name: {ctx.name} - age: {ctx.age}
} export default A ``` ## 副作用Effect 有部分逻辑直接写在函数体中,会影响组件的渲染,这部分代码会产生"副作用"。(例如修改 state 写到组件中,可能导致死循环渲染) ### React.StrictMode React 的严格模式,在开发模式下,会主动重复调用一些函数,以使副作用凸显。所以在开发模式且开启严格模式,这些函数会被调用两次: * 函数组件的函数体 * 参数为函数的`setState` * 参数为函数的`useState`、`useMemo`、`useReducer` ### 重渲染案例 ```jsx 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 (
) } export default App ``` ```jsx import React from 'react'; function B(props) { console.log('B组件重新渲染'); return (
我是子组件
); } export default B; ``` ### useEffect > 将会产生副作用的函数写在useEffect的回调函数里 ```js useEffect(() => setCount(0)) /** useEffect(fn, []) * fn作为第一个参数的函数将在组件渲染完成后执行 * 第二个可选参数是依赖项,只有依赖项发生变化,才执行. 数组中包含了**所有**外部作用域中会发生变化且在 effect 中使用的变量 * useState创建的setState在每次渲染获取都是相同的,写不写没关系 * 如果传空数组[], effect只会在初始化时触发一次 */ ``` ### 清除 effect ```js useEffect(() => { // 防抖 const timeId = setTimeout(() => { props.onSearch(keyword) }, 1000) return () => { // 下次effect执行, 可以用来清理上次effect的影响 clearTimeout(timeId) } }, [keyword]) // return 返回一个清除函数, 在下次effect先执行 ``` ## useReducer > useState 的替代方案 > > 当 state 过于复杂时, 使用 useReducer 进行整合 ```js /** useReducer(reducer, initialArg, init) * reducer: 整合函数 * 对state的所有操作都应该在该函数中定义 * 该函数的返回值, 会成为state的新值 * reducer执行时会收到两个参数: * 第一个参数: 当前state最新的值 * 第二个参数: action: 对象, 从countDispatch 传进来的值 * 为了避免每次渲染都重新创建reducer,一般写在函数组件外面 * initialArg: state的初始值, 作用和uueState()中的值一样 * init: 函数, 可选参数, 惰性初始化, 初始state就由它决定,而不再是第二个参数 * @return * 数组: * 第一个参数: state * 第二个参数: state 修改的派发器 * 具体通过reducer执行 */ ``` ```jsx 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 (
count is {count}
) } export default App ``` ## React.memo React 组件会在两种情况下重新渲染 1. 组件自身的 state 发生变化 2. 组件的父组件重新渲染 如果子组件的 state 并没有发生变化, 但父组件重新渲染导致子组件也重新渲染, 为了减少性能损失, 可以用`React.memo()` ```js /** * React.memo()是高阶组件 * 接收另一个组件作为参数,返回一个包装后的新组件 * 包装后的新组件具有缓存功能 * 只有组件的props发生变化,才会触发组件的重新渲染,否则总是返回缓存中的结果 */ ``` ```jsx 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 (
) } export default App ``` ```jsx import React from 'react' const B = () => { console.log('B组件重渲染') s return
B组件
} export default React.memo(B) ``` ```jsx import React from 'react' const C = props => { console.log('C组件重新渲染') return
props传过来的count: {props.count}
} export default React.memo(C) ``` ## useCalkback ```js /** * useCallback() * 钩子函数,用来创建React中的回调函数 * 创建的回调函数可以在组件重新渲染时不重新创建 * 参数: * 1.回调函数 * 2.依赖数组 * - 当依赖数组中的变量发生变化时,回调函数才会重新创建 * - 如果不指定依赖数组(不传),回调函数每次都会重新创建 * - 一定要把回调函数用到的变量都放到数组里, 除了setState * 不放的话,变量的作用域只会停留在第一次创建的时候 */ ``` ```jsx 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 (
count is {count}
num is {num}
) } export default App ``` ```jsx import React from 'react' const B = props => { console.log('B组件重渲染') return (
B组件
) } export default React.memo(B) /** * B组件也重新渲染: props里的addCount在A组件重新渲染后重新创建 */ ``` ```jsx import React from 'react' const C = props => { console.log('C组件重渲染') return (

C组件
回调使用useCallback
) } export default React.memo(C) ``` ```jsx import React from 'react' const D = props => { console.log('D组件重渲染') return (

D组件
变量没有放到依赖数组
) } export default React.memo(D) ``` ```jsx import React from 'react' const E = props => { console.log('E组件重渲染') return (

E组件
变量放到依赖数组
) } export default React.memo(E) ``` ## hooks > React 中的钩子只能在函数组件或者自定义钩子中调用 > > 自定义钩子: 本质就是普通函数, 命名上以 use 开头 例子: useFetch ```js 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 ``` 使用 ```js 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 ```js const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]); ``` 把"创建"函数和依赖项数组作为参数传入 `useMemo`,它仅会在某个依赖项改变时才重新计算 memoized 值。这种优化有助于避免在每次渲染时都进行高开销的计算。 传入 `useMemo` 的函数会在渲染期间执行。不要在这个函数内部执行不应该在渲染期间内执行的操作,诸如副作用这类的操作属于 `useEffect` 的适用范畴,而不是 `useMemo`。 `useMemo` 缓存的是函数的执行结果, `useCallbakc` 缓存的是函数, `React.memo` 是在子组件中使用 `useMemo` 也可以用来缓存 jsx/组件 如果没有提供依赖项数组,`useMemo` 在每次渲染时都会计算新的值。 ```jsx 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 (

sum:{num}

count:{count}

) } export default App ``` ## useImperativeHandle ```js // 无法直接获取react组件的dom对象 // 例如 cont myref = useRef() // // 这样是获取不到的 // 用React.forwardRef()把组件包起来 // useImperativeHandle()自定义要返回的ref // 把子组件的东西暴露给父组件,但不是直接暴露dom给父组件操作, 而是可以控制要暴露哪些值或方法 ``` ```js useImperativeHandle(ref, createHandle, [deps]) ``` ```jsx // 子组件 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 ( ) } export default React.forwardRef(Child) ``` ```jsx import { useRef } from 'react' import Child from './components/Child' const App = () => { const childRef = useRef() return (
) } export default App ``` ## useLayoutEffect ```mermaid 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 操作 ```js 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 (
123
) } export default App ``` ## useDebugValue 可以用来给钩子打标签, 开发者插件中可以看到 ```js // 自定义勾子 import { useEffect,useDebugValue } from 'react' const useMyhook = () => { useDebugValue('标签') useEffect(()=>{ console.log('自定义钩子') }) } export default useMyhook ``` ![](https://img.081024.xyz/20230308235037.png) ## useDeferredValue 设置一个延迟的 state ```js /** * 当多个组件使用同一个state时, 组件可能相互影响 * 一个组件卡顿,导致所有组件卡顿 * 此时设置延迟值,传给卡顿的组件,快的组件就不用等卡顿的组件了 * 实际体验: 快速改动value,触发deffredValue快速变化, 重渲染还是卡 */ ``` ```jsx // 模拟卡顿的组件 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 (
    {filterList.map(item => { return
  • {item}
  • })}
) } export default React.memo(List) ``` ```jsx 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快速变化, 重渲染还是卡 */ return (
{ setValue(e.target.value) }} /> {/* */}
) } export default App ``` ## useTransition 让某个 setState 比其他的 setState 更慢执行 ```js // 模拟性能很差的组件 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 (
    {filterList.map(item => { return
  • {item}
  • })}
) } export default React.memo(List) ``` ```jsx 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 (
{!isPending && }
) } export default App ``` useDeferredValue 和 useTransition 都代替不了节流防抖