38 KiB
Executable File
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 中的钩子函数只能用于函数组件或者自定义钩子
- 钩子函数只能在函数组件中调用
- 创建 ref 容器
- 绑定到标签的 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 模块:
- 创建
xxx.module.css
import xx form './xxx.module.css'
- 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
- 参数为函数的
useState
、useMemo
、useReducer
重渲染案例
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 组件会在两种情况下重新渲染
- 组件自身的 state 发生变化
- 组件的父组件重新渲染
如果子组件的 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 都代替不了节流防抖