🎉 希望可以记录一些笔记,保持原有久笔记的前提下动态更新,方便对比复习
👉 值得学习的 Thinking In React
前言 🙃 记录时间、版本的不同,代码风格会不同,最新的版本示例会是最上面一个然后用 新---
和 旧---
标识
TBR
标出的,是没看明白,还需细品的 😅
基本使用 安装 使用 vite 🌎 Getting Start
先创建 vite
再选择 react
直接指定 react
1 2 3 4 5 6 7 npm create vite@latest my-react-app -- --template react # yarn yarn create vite my-react-app --template react # pnpm pnpm create vite my-react-app --template react
非脚手架 1 npm install react react-dom
通过<script>
导入,注意导入顺序
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <div id ="root" > </div > <script src ="./node_modules/react/umd/react.development.js" > </script > <script src ="./node_modules/react-dom/umd/react-dom.development.js" > </script > <script > const title = React .createElement ("h1" , null , "hello react!" ); ReactDOM .render (title, document .getElementById ("root" )); </script >
脚手架 React.createElement()
和createRoot()
的区别
前者:创建 React Element
后者:创建一个 root-level 的容器来渲染 React 程序 React 18
React 18 以前 下载
1 2 3 4 5 6 # 推荐 npx create-react-app myReact # npm init react-app myReact yarn create react-app myReact
启动(要进入项目的根目录)
使用,通过 ES6 的 import
关键字
1 2 3 4 5 6 7 8 import React from "react" ;import React from "react-dom" ;const title = React .createElement ("h1" , null , "hello react!" );ReactDOM .render (title, document .getElementById ("root" ));
React 18 以后 1 npm install react react-dom
1 2 3 4 5 6 7 8 import { createRoot } from "react-dom/client" ;document .body .innerHTML = '<div id="app"></div>' ;const root = createRoot (document .getElementById ("app" ));root.render (<h1 > Hello, world</h1 > );
或者是并不想清空当前 HTML 页面的内容,那就找一个元素当作容器来渲染当前的 React 组件
1 2 3 4 5 6 7 8 9 10 11 <!DOCTYPE html > <html > <head > <title > My app</title > </head > <body > <p > This paragraph is a part of HTML.</p > <nav id ="navigation" > </nav > <p > This paragraph is also a part of HTML.</p > </body > </html >
1 2 3 4 5 6 7 8 9 10 import { createRoot } from "react-dom/client" ;function NavigationBar ( ) { return <h1 > Hello from React!</h1 > ; } const domNode = document .getElementById ("navigation" );const root = createRoot (domNode);root.render (<NavigationBar /> );
React 和 JSX jsx
是 JavaScript XML
的简写,在 JavaScript 文件中写 HTML-like 标签
因为jsx
不是标准的ECMAScript
语法,而是它的语法扩展,所以在普通环境是不可以使用的;要在脚手架中使用(因为脚手架中包含了babel
,@babel/preset-react)
JSX 和 React 是两个东西,前者是语法拓展,后者是一个 JavaScript 库。React 是使用了这种语法拓展。通常是一起使用的,但也可以分开使用。更多介绍
语法规则 1. 单根节点
原因:JSX 看起来像 HTML,但是是转换成原生的 JavaScript 对象,一个方法是不可以返回两个对象的,所以才需要包裹起来
1 2 3 4 5 6 7 8 9 <div> <p > 123</p > <p > 456</p > </div> <> <p > 123</p > <p > 456</p > </>
这个空白的标签叫做 Fragment (片段 ??)
完整样子:<Fragment></Fragment>
向 Fragment 传 _key_,不能使用短标签 <></>
,需要从 react 导入 Fragment 然后 s 使用<Fragment key={yourKey}>...</Fragment>
不会重置state
,在从<><Child /></>
到[<Child>]
或反转的时候;<><><Child /></></>
就会重置。关于重置 state,需要跳转看到state
章节
1 2 3 4 5 6 7 8 9 10 11 12 import React from "react" ;import React from "react-dom" ;const title = ( <h1 className ="title" > Hello Hi <span /> </h1 > ); ReactDOM .render (title, document .getElementById ("root" ));
class => className 、for => htmlFor,等等
如果元素没有子节点,可以转为单标签:<span></span>
=> <span />
,当然不转也行
2. 必须要关闭标签
…其实一直不知道标签还可以不用关闭的 🥲
1 2 3 4 5 6 7 <ul> <li>12 <li>34 <li>56 </ul> <img>
1 2 3 4 5 6 7 <ul > <li > 12</li > <li > 34</li > <li > 56</li > </ul > <img />
3. 驼峰式属性名
因为 JSX 要转换成 JavaScript 对象,所以,例如 HTML-like 中的样式类class
改成了className
,不然得和 JavaScript 类的关键字冲突
DOM 节点的className
属性也是这个意思,避免与操作 DOM 的编程语言保留的class
关键字冲突
对于样式类,class 是属于 HTML 的,而 className 是 DOM 属性
👉 这里查看所有的属性名
由于历史原因aria-*
和 data-*
依然使用 -
而不是驼峰式。主要原因有 W3C 定的一些 HTML 规范,然后 React 也遵循这些规范以适配很多库、开发工具、不同技术等等
aria 规范: Accessible Rich Internet Applications,提供一组属性增强 web 应用程序的可访问性。
使用 js 表达式 (JSX 的花括号)
❗ 花括号外面是不需要加 双引号或者单引号 的;花括号里面如果是字符串 return 语句中 JS 表达式要写在花括号里面
1 2 3 4 5 export default function Avatar ( ) { const avatar = "https://i.imgur.com/7vQD0fPs.jpg" ; const description = "Gregorio Y. Zara" ; return <img className ="avatar" src ={avatar} alt ={description} /> ; }
1 2 3 const name = "Jerry" ; const title = <h1 > {name}</h1 > ;ReactDOM .render (title, document .getElementById ("root" ));
在{}
中可以使用任意的合法的JavaScript
表达式,不过也有例外
1 2 3 4 5 6 7 8 9 10 11 12 const hello = ( ) => "hello" ;const myDiv = <div > 我是一个div</div > ;const title = ( <div > <p > {1 + 1}</p > <p > {1 < 2 ? "对呀" : "不对"}</p > <p > {hello()}</p > {div} </div > ); ReactDOM .render (title, document .getElementById ("root" ));
jsx 自身也是表达式,所以{div}
也适用
<p>{ {a: "我是a"} }</p>
,这种对象是不行 的,但是在style
样式中又可以使用
在里面使用语句也是不行 的:if、for 这些
JSX 使用两个花括号的场景
CSS:<ul style={{backgroundColor: 'black', color: 'pink'}}>
JSX 传递对象:person={{ name: "Hedy Lamarr", inventions: 5 }}
条件渲染 1 2 3 4 5 6 7 8 9 10 11 12 function Item ({ name, isPacked } ) { return <li className ="item" > {isPacked ? name + "✔" : name}</li > ; } export default function Pane ( ) { return ( <div > <Item name ="mike" isPacked ={false} > </Item > <Item name ="amy" isPacked ={true} > </Item > </div > ); }
使用逻辑与简化条件判断
1 2 3 4 5 return ( <li className ="item" > {name} {isPacked && "✔"} </li > );
React considers false
as a “hole” in the JSX tree,像undefined
和null
一样不渲染东西
可用多个花括号
注意 &&
左边有数字,因为如果是 0 的话,会被认为是false
;可以加个前提判断当左边大于 0
如果要简化返回语句,或者有个默认返回,可以使用结合使用变量和 JSX
1 2 3 4 5 6 7 8 function Item ({ name, isPacked } ) { let content = name; if (isPacked) { content = <del > {name + "✔"}</del > ; } return <li className ="item" > {content}</li > ; }
列表渲染
箭头函数=>
后面隐式返回,不用加上return
,但是只返回一行;返回多行=>
需要加上{}
和return
如果要渲染一组数据,应该使用数组的map()
方法
1 2 3 4 5 6 7 8 9 10 11 const songs = [ { id : 1 , name : "我很快乐" }, { id : 2 , name : "你很快乐" }, { id : 3 , name : "他很快乐" }, ]; export default function SongList ( ) { const songItems = songs.map (item => <li key ={item.id} > {item.name}</li > ); return <ul > {songItems}</ul > ; }
不要使用Math.random()
生成 key,key 除了标识当前 DOM,还会有缓存作用,数据不变化的不会重新渲染以使得渲染更快,如果用了随机数,所有的 DOM 都会在数据更新时重新渲染 key 在props
是获取不到的
想要渲染多个 DOM 节点但是又不想在外面包一个多余的节点,例如
1 2 3 4 5 6 const listItems = people.map (person => ( <div key ={person.id} > <h1 > {person.name}</h1 > <p > {person.bio}</p > </div > ));
不想,要这个外面的<div>
,可用<Fragment>
代替,在 DOM 中 Fragments 不会出现
1 2 3 4 5 6 7 8 9 import { Fragment } from "react" ;const listItems = people.map (person => ( <Fragment key ={person.id} > <h1 > {person.name}</h1 > <p > {person.bio}</p > </Fragment > ));
记得要事先导入:
不能使用 <></>
,因为它不能传递 key
样式处理 行内样式:style(不推荐),使用样式时可以在{}
中使用对象
1 2 3 const list = ( <h1 style ={{ color: "red ", backgroundColor: "yellow " }}> JSX行内样式</h1 > );
类名:className
1 2 3 4 5 .title { color : "red" ; background-color : "yellow" ; }
1 import "index.css" const list = ( <h1 className ="title" > JSX类样式</h1 > )
小结:React 是利用 JavaScript 语言自身来编写界面,而不是像 Vue 一样通过增强 HTML 的功能。
组件基础
一个页面可以全部都是 React 组件。
a React component is a JavaScript function that you can sprinkle with markup
两种创建方式
React 16.8
以后 推荐创建组件的方法不再是 类组件 ,而是 函数组件
函数的方式和类的方式
函数组件
使用function
关键字
函数名称大写 开头
函数组件必须有返回值(返回null
表示不渲染内容)
不要在组件里面再定义其他组件
1 2 3 4 5 6 function Hello ( ) { return <div > 这是一个函数组件</div > ; } ReactDOM .render (<Hello /> , document .getElementById ("root" ));
1 2 3 4 5 6 7 export default function Profile ( ) { return ( <div > <img src ="https://i.imgur.com/MK3eW3Am.jpg" alt ="Katherine Johnson" /> </div > ); }
虽然里面有src 、_alt_,但是实则为 JavaScript,这种写法叫
React 会以大小写来区分 HTML 标签和 React 组件
类组件
使用class
关键字
类名要大写开头,并且继承于React.Component
必须有render()
方法,并且这个方法要有返回值
1 2 3 4 5 6 7 class Hello extends React.Component { render ( return ( <div > 这是一个类组件</div > ) ) } ReactDOM .render (<Hello /> , document .getElementById ("root" ))
组件的导入导出
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import NavMenu from "./navMenu.js" ;function HeaderTitle ( ) { return <h1 > </h1 > ; } export default function Header ( ) { return ( <div > <NavMenu /> <HeaderTitle /> </div > ); }
1 2 3 4 5 6 7 8 9 10 11 import React from "react" class Hello extends React.Component { render ( return ( <div > 这是一个独立文件组件</div > ) ) } export default Hello
1 2 3 4 5 6 7 8 import React from "react" ;import React from "react-dom" ;import Hello from "./Hello.js" ;ReactDOM .render (<Hello /> , document .getElementById ("root" ));
默认与命名导出导入
语法
导出
导入
默认
export default function Button() {}
import Button from './Button.js';
命名
export function Button() {}
import { Button } from './Button.js';
默认导出,导入的时候,导入名字随便写
命名的时候,名字需要对应
虽然默认导出export default () => {}
没问题,但是不推荐没有名字
事件处理 基本使用
on+事件名称={事件处理程序}
,onClick={()=>{}}
驼峰式命名
事件处理函数必须是传递 而不是调用 ,就是说不用在函数后面加上括号
给组件添加事件处理:定义一个函数,然后作为props 传递给<button>
这个函数一般在当前这个组件里面
以handle
开头:onClick={handleClick}
, onMouseEnter={handleMouseEnter}
1 2 3 4 5 6 7 export default function Button ( ) { function handleClick ( ) { alert ("You clicked me!" ); } return <button onClick ={handleClick} > 点击</button > ; }
1 2 3 4 5 6 7 8 class Button extends React.Component { handleClick ( ) { console .log ("触发单击事件" ); } render ( ) { return <button onClick ={this.handleClick} > 点击</button > ; } }
事件对象
事件处理器会捕获到子组件可能会有的事件:称为冒泡或者传播;在事件发生的地方开始,然后顺着组件树往上传递。比如子组件和父组件都有点击事件。
所有事件都会冒泡,除了onScroll
,只在使用的地方促发
事件处理函数仅有的一个参数就是事件对象 ,一般用e
来表示
e.stopPropagation()
阻止冒泡
父组件添加onClickCapture={()=>{ /* ... */}}
捕获子组件事件
1 2 3 4 5 <div onClickCapture={() => {}}> <button onClick ={e => e.stopPropagation()} /> <button onClick ={e => e.stopPropagation()} /> </div>
1 2 3 4 5 6 7 8 9 10 11 12 13 class Button extends React.Component { handleClick (e ) { e.preventDefault (); } render ( ) { return ( <a href ="www.baidu.com" onClick ={this.handleClick} > 去百度 </a > ); } }
事件处理函数读取 props 和作为 props 传递 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 function Button ({ onClick, children } ) { return <button onClick ={onClick} > {children}</button > ; } function PlayButton ({ movieName } ) { function handlePlayClick ( ) { alert (`Playing ${movieName} !` ); } return <Button onClick ={handlePlayClick} > Play "{movieName}"</Button > ; } export default function Toolbar ( ) { return ( <div > <PlayButton movieName ="Kiki's Delivery Service" /> </div > ); }
自定义事件处理函数 props 名字
作为 props 传递时使用的名字
上面的handlePlayClick
作为 props 传递的时候使用的是onClick
,算是使用了默认名字
一般以on
开头,然后驼峰式命名
1 2 3 4 5 6 7 8 9 10 11 12 function Button ({ onSmash, children } ) { return <button onClick ={onSmash} > {children}</button > ; } export default function App ( ) { return ( <div > <Button onSmash ={() => alert("Playing!")}>Play Movie</Button > <Button onSmash ={() => alert("Uploading!")}>Upload Image</Button > </div > ); }
对于原生的 HTML 元素,要尽量使用对应功能的元素其对应的事件。比如点击事件会用到<button>
而不是div
state 随着时间改变的数据叫 _state_,对于对象和数组,react 推荐它们的使用是不可变的(immutable),要想更新,就创建一个新的
有状态和无状态组件
无状态组件:函数组件;有状态组件:类组件 React 16.8
状态(state)负责数据
函数组件没状态,可以用于展示数据(静态) React 16.8
类组件有自己状态,可以用于更新界面(动态)React 16.8 后不再是主推的
state 基本使用(新) 1 2 3 4 5 6 7 8 import { useState } from "react" ;const [index, setIndex] = useState (0 );function handleClick ( ) { setIndex (index + 1 ); }
useState
返回两个东西通过解构获得,一个是这个值,另一个是更新这个值二点方法
名字随便起,但习惯使用 名字
和 set名字
每个组件里的 state 都是独立的
use
开头的Hooks 方法,只可在组件的top level 执行,不可再条件判断、列表循环中使用
更改 对象 类型的 state 直接更新对象里的属性是不会触发页面更新的
1 2 3 4 5 6 const [position, setPosition] = useState ({ x : 0 , y : 0 });position.x = e.clientX ; position.y = e.clientY ;
虽然在某些情况这样做会有效,但是并不推荐。所以要使用setPosition
传递一个新的对象过去,然后组件重新渲染
重点是使用setXXX
这个函数,不管要更新的值以什么形式变化,比如
1 2 3 4 5 6 7 8 const nextPosition = {};nextPosition.x = e.clientX ; nextPosition.y = e.clientY ; setPosition (nextPosition); setPosition ({ x : e.clientX , y : e.clientY });
不过这样会有个新的问题,就是只是想要改变某一个属性的值,不想要改变其他值。
使用展开运算符将不需要改变的对象属性复制到新的对象
1 2 3 4 5 6 7 8 9 10 11 12 setPerson ({ firstName : e.target .value , lastName : person.lastName , email : person.email , }); setPerson ({ ...person, firstName : e.target .value , });
展开运算符仅在对象的第一层起作用,如果要复制更深层的,得多次使用展开运算符
1 2 3 4 5 6 7 8 const [person, setPerson] = useState ({ name : "Niki de Saint Phalle" , artwork : { title : "Blue Nana" , city : "Hamburg" , image : "https://i.imgur.com/Sd1AgUOm.jpg" , }, });
1 2 3 4 5 6 7 8 9 10 11 12 13 const nextArtwork = { ...person.artwork , city : "New Delhi" };const nextPerson = { ...person, artwork : nextArtwork };setPerson (nextPerson);setPerson ({ ...person, artwork : { ...person.artwork , city : "New Delhi" , }, });
❔ 其他情况:更改 obj3.artwork
,obj1
和 obj2.artwork
也会改变,因为它们是相同的对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 let obj1 = { title : "Blue Nana" , city : "Hamburg" , image : "https://i.imgur.com/Sd1AgUOm.jpg" , }; let obj2 = { name : "Niki de Saint Phalle" , artwork : obj1, }; let obj3 = { name : "Copycat" , artwork : obj1, };
更新 数组 类型的 state
同样推荐不可变(immutable),所以不要使用arr[0] = "qaq"
来修改其中的值
同样也不推荐使用pop()
、push()
等方法来改变它
推荐从旧数组的基础上创建新的数组,并且使用不会改变旧数组的数组方法,例如filter()
、map()
React 中 state 中数组的操作方法推荐(避免使用该百年数组的,推荐使用返回新数组的):
避免
推荐
添加
push, unshift
concat, […arr]
删除
pop, shift, splice
filter, slice
替换
splice, arr[i]=xx
map
排序
reverse, sort
先复制这个数组
添加内容 :和对象一样,使用扩展运算符来复制以达到改变某一个值的目的
1 2 3 4 5 6 7 8 9 10 const [artists, setArtists] = useState ([]);setArtists ( [ ...artists, { id : nextId++, name : name }, ] );
要改变新插入值的位置,将新插入的值这行放到扩展运算的上面就行
删除内容 :最简单的就是过滤掉这个不需要的,或者直接创建个新数组的不包含这个要删除的内容
替换内容 :在原有的基础上创建一个新的数组,使用map
,如果符合,改变这个值然后返回,不符合的返回原来的样子
1 2 3 4 5 6 7 8 9 10 11 12 const [counters, setCounters] = useState ([xx, xxx]);const nextCounters = counters.map ((c, i ) => { if (i === index) { return c + 1 ; } else { return c; } }); setCounters (nextCounters);
插入内容 :确定要添加的位置,然后使用slice
分割数组,将要添加的放到两个切片中间
1 2 3 4 5 6 7 8 9 10 11 12 13 const [artists, setArtists] = useState ([xxx, xx, x]);const insertAt = 1 ; const nextArtists = [ ...artists.slice (0 , insertAt), { id : nextId++, name : name }, ...artists.slice (insertAt), ]; setArtists (nextArtists);
其他操作 :比如 _反转_,_排序_,js 方法会改变旧的数组,所以要先复制出一个数组然后再做出改变,如nextList.sort()
,nextList[0] = {name: "zs", age: 18}
对于数组 list 和 _nextList_,虽然不是相同的数组,但是list[0]
和nextList[0]
指向的是相同的对象,所以直接nextList[0].age=19
这样的还是不推荐的,因为这是浅拷贝,是直接改掉了对象里面的东西
👉 更新数组里面的对象
对象其实并不是再数组里面的,只是在代码这里看起来是在里面;但事实上当使用数组时(虽然已经使用拓展运算符复制出不同的数组),尝试改变其中数组内元素的值,另外的引用也会跟着改变,因为数组它内容本身还是和旧数组一样,只是在新的数组里面呆着罢了
所以改变数组里面你的对象,可以通过使用map
找出要改变的对象,然后使用更新对象的方法更新目标对象
state 基本使用
一个对象,是组件内部私有 的数据,只能在组件内部使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 class Hello extends React.component { construtor ( ) { super (); this .state = { count : 0 , }; } render ( ) { return ( <div > <h1 > 计数器:{this.state.count}</h1 > <button onClick ={() => { this.setState({ count: this.state.count + 1 }); }} > +1 </button > </div > ); } }
❗ 注:不能直接修改 state 中的值:this.state.count++
,这样是错的
上面的语法有个简化版的,去掉了构造器和 super
1 2 3 4 5 6 7 8 9 10 11 12 class Hello extends React.component { state = { count : 0 , }; render ( ) { return ( <div > <h1 > 计数器:{this.state.count}</h1 > </div > ); } }
this 指向问题解决 1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class Hello extends React.component { construtor ( ) { super (); this .state = { count : 0 }; } add ( ) { this .setState ({ count : this .state .count + 1 , }); } render ( ) { return ( <div > <h1 > 计数器:{this.state.count}</h1 > <button onClick ={() => this.add()}>+1</button > </div > ); } }
❗ 注:在<button>
中调用时,函数后面要 加上()
this 指向问题解决 2
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 class Hello extends React.component { construtor ( ) { super (); this .add = this .add .bind (this ); this .state = { count : 0 , }; } add ( ) { this .setState ({ count : this .state .count + 1 , }); } render ( ) { return ( <div > <h1 > 计数器:{this.state.count}</h1 > <button onClick ={this.add} > +1</button > </div > ); } }
❗ 注:在<button>
中调用时,这里已经不是函数调用,所以函数后面不用 加上()
this 指向问题解决 3 基于上面的内容,只需要修改add()
1 2 3 4 5 add = () => { this .setState ({ count : this .state .count + 1 , }); };
表单处理 受控组件
HTML 中的状态(数据)是元素自己控制的,但是在 React 中要在 state 中,并且只能通过 setState 来修改
解决这个冲突,React 将state
和元素的value
绑定在一起
受控组件,就是其值是受到 React 控制的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 class App extends React.Component { state = { txt : "" , city : "gz" , isCheck : false , }; handleChange = e => { this .setState ({ txt : e.target .value , }); }; handleCity = e => { this .setState ({ city : e.target .value , }); }; handleChecked = e => { this .setState ({ isCHecked : e.target .checked , }); }; render ( ) { return ( <div > <input type ="text" value ={this.state.txt} onChange ={this.handleChange} /> <select value ={this.state.city} onChange ={this.handleCity} > <option value ="sh" > 上海</option > <option value ="bj" > 北京</option > <option value ="gz" > 广州</option > </select > <input type ="checkbox" checked ={this.state.isChecked} onChange ={this.handleChecked} /> </div > ); } }
🌝 可以对上面的代码进行优化
给表单元素添加name
属性,用来区分不同的表单元素,名称与对应的state
相同。
根据表单元素类型获取对应的值。(value、checked)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 class App extends React.Component { state = { txt : "" , city : "gz" , isCheck : false , }; handleForm = e => { const target = e.target ; const value = target.type === "checkbox" ? target.checked : target.value ; const name = target.name ; this .setState ({ [name]: value, }); }; render ( ) { return ( <div > <input type ="text" name ="txt" value ={this.state.txt} onChange ={this.handleForm} /> <select name ="city" value ={this.state.city} onChange ={this.handleForm} > <option value ="sh" > 上海</option > <option value ="bj" > 北京</option > <option value ="gz" > 广州</option > </select > <input name ="isChecked" type ="checkbox" checked ={this.state.isChecked} onChange ={this.handleForm} /> </div > ); } }
非受控组件
通过ref
,使用原生 DOM 来获取表单元素的值
创建一个 ref 对象
将创建好的 ref 对象放到目标元素中
1 <input type="text" ref={this .txtRef } />
通过 ref 获取到目标元素的值
1 console .log (this .txtRef .current .value );
react 中不推荐直接操作 DOM
保持组件整洁 1:
组件就像是公式,不会有意料之外的结果,比如
1 2 3 function double (number ) { return 2 * number; }
2:
保持为一个纯函数,只管自己的事,在调用这个组件之前,不会改变存在的变量或对象
❌ 不好的示例:每使用一次组件guest
的值增加了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 let guest = 0 ;function Cup ( ) { guest = guest + 1 ; return <h2 > Tea cup for guest #{guest}</h2 > ; } export default function TeaSet ( ) { return ( <> <Cup /> <Cup /> <Cup /> </> ); }
正确做法是通过props将值传进去
或者将数据在TeaSet
中操作
3:
“副作用” 不需要保持整洁?
就是一些 事件处理器 不需要 这样,因为渲染的时候它们并没有执行,而是在等时间触发。所以在这可以改变一些用户的输入、响应等
TBR:
Keeping Components Pure
组件进阶 props
接收传递给组件的数据
传递数据:给组件标签添加属性
接收数据:函数组件通过参数props
接收数据,类组件通过this.props
接收数据
props
只可读
props 是动态的,并不是组件被创建之后就写死了的
但是 props 是不可变对象
当组件要改变它的 props,首先会向父组件请求所需数据来传递不同的 props
旧的 props 被丢弃,随后被 JavaScript 引擎回收这个 props 占的内存
不要直接改变 props 的值,需要使用 set state
新---
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 export default function Profile ( ) { return ( <Avatar person ={{ name: "Lin Lanying ", imageId: "1bX5QH6 " }} size ={100} /> ); } function Avatar ({ person, size } ) { return ( <img className ="avatar" src ={getImageUrl(person)} alt ={person.name} width ={size} height ={size} /> ); } function getImageUrl ( ) { }
指定默认值
和 js 一样,在参数那指定就行
1 2 3 function Avatar ({ person, size = 10 } ) { }
没传递 size 的时候会使用默认值,或者是传递 size={undefined}
size={null}
或者size={0}
,不会使用默认值
更简洁的传递 props
前提是要想好使用场景?比如父组件就是需要解构,那没办法
不简洁的
1 2 3 4 5 6 7 8 9 10 11 12 function Profile ({ person, size, isSepia, thickBorder } ) { return ( <div className ="card" > <Avatar person ={person} size ={size} isSepia ={isSepia} thickBorder ={thickBorder} /> </div > ); }
这里父组件接收到的 props 然后又原封不动再写一次传给子组件,有点麻烦
可以这样
1 2 3 4 5 6 7 function Profile (props ) { return ( <div className ="card" > <Avatar {...props } /> </div > ); }
旧---
函数组件
1 2 3 4 5 6 7 8 9 10 11 const Hello = props => { return ( <div > <h1 > {props.name}</h1 > </div > ); }; ReactDOM .render (<Hello name ="tom" age ={10} /> , document .getElementById ("root" ));
类组件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 class Hello extends React.Component { constructor (props ) { super (props); } render ( ) { return ( <div > <h1 > {this.props.age}</h1 > {tag} </div > ); } } ReactDOM .render ( <Hello name ="tom" age ={10} fn ={() => { console.log("这是一个函数"); }} tag={<p > 这是一个p标签</p > } /> , document .getElementById ("root" ) );
传递非字符串的内容要使用{}
包起来
props 深入 children 属性
通过props.children
获得
1 2 3 4 5 6 7 8 9 10 11 12 13 import Avatar from "./Avatar.js" ;function Card ({ children } ) { return <div className ="card" > {children}</div > ; } export default function Profile ( ) { return ( <Card > <Avatar size ={100} /> </Card > ); }
1 2 3 4 5 6 7 8 9 10 11 12 13 const App = props => { return ( <div > <h1 > 组件标签子节点</h1 > {props.children} </div > ); }; ReactDOM .render (<App /> , document .getElementById ("root" ));ReactDOM .render (<App > 我是子节点</App > , document .getElementById ("root" ));
子节点可以为任意的jsx
表达式、组件、函数
如果是函数,直接使用props.children()
,外面不用加 {}
props 校验
在使用之前,先安装prop-types
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import PropTypes from "prop-types" ;const App = props => { const arr = props.colors ; const list = arr.map ((item, index ) => <li > {item}</li > ); }; App .propTypes = { colors : PropTypes .array , }; ReactDOM .render ( <App colors ={[ "red ", "yellow "]} /> , document .getElementById ("root" ) );
⚠ 约束规则:
常见的约束类型:array、bool、func、number、object、string
React 元素类型:element
必填项:isRequired(在约束规则后点使用)
特定结构的对象:shape({ })
……
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import PropTypes from "prop-types" ;const App = props => { return <div > </div > ; }; App .propTypes = { a : PropTypes .number , fn : PropTypes .func .isRequired , tag : PropTypes .element , filter : PropTypes .shape ({ area : PropTypes .string , price : PropTypes .number , }), }; ReactDOM .render (<App fn ={() => {}} /> , document .getElementById ("root" ));
props 默认值 1 2 3 4 App .defaultProps = { pageSize : 10 , };
render props
用于组件复用
复用 state 和操作 state 的方法
render
这个 render 名字是随便取的
使用组件时拿到组件内部的 props,可以给组件提供的一个函数,然后通过函数的参数来获取。<Mouse render={ (mouse) => {} } />
,然后函数的返回值作为页面要渲染的结构。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 import img from "./image/cat.jpg" ;class Mouse extends React.Component { state = { x : 0 , y : 0 , }; handleMouseMove = e => { this .setState ({ x : e.clientX , y : e.clientY , }); }; componentDidMount ( ) { window .addEventListener ("mousemove" , this .handleMouseMove ); } render ( ) { return this .props .render (this .state ); } } class App extends React.Component { render ( ) { reutrn ( <div > <h1 > render props 模式</h1 > <Mouse render ={mouse => { return ( <p > 鼠标位置:{mouse.x} {mouse.y} </p > ); }} /> {/* 复用一个<Mouse /> */} <Mouse render ={mouse => { return ( <img src ={img} alt ="猫" style ={{ position: "absolute ", top: "mouse.y ", left: "mouse.x ", }} /> ); }} /> </div > ); } }
在class Mouse
中,组件是要返回内容的,但是在复用组件的情况下,class Mouse
并不知道要返回什么,所以在使用<Mouse />
时候提供的要渲染的内容,然后在this.props.render
中接收
意思就是声明<Mouse />
,和使用<Mouse />
children 取代 render 🎡 格式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 render ( ) { return this .props .children (this .state ) } <Mouse > { mouse => { return ( <p > 鼠标位置:{mouse.x} {mouse.y}</p > ) } } </Mouse >
代码优化 校验
1 2 3 Mouse .propTypes = { children : Proptypes .func .isRequired , };
移除mousemove
事件
1 2 3 componentWillUnmount ( ) { windows.removeEventListener ("mousemove" , this .handleMouseMove ) }
组件之间的通讯 父组件传给子组件
父组件提供要传递的state
数据
子组件标签添加属性,值为state
中的数据
子组件通过props
接收父组件中传递的数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 class Parent extends React.Component { constructor ( ) { super (); this .state = { lastName : "tom" , }; } render ( ) { return ( <div > 父组件: <Child name ={this.state.lastName} /> </div > ); } } const Child = props => { return ( <div > <p > 子组件,接收父组件传递的数据。{props.name}</p > </div > ); }; ReactDOM .render (<Parent /> , document .getElementById ("root" ));
子组件传给父组件
父组件提供回调函数,用来接收数据(谁要接收数据,谁就提供回调函数 )
将改函数作为属性的值,传递给子组件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 class Parent extends React.Component { constructor ( ) { super (); this .state = { parentMsg : "" , }; } getChildMsg = data => { console .log ("接收子组件传递过来的数据" , data); this .setState ({ parentMsg : data, }); }; render ( ) { return ( <div > 父组件:给子组件提供了函数 <Child getMsg ={this.getChildMsg} /> {this.state.parentMsg} </div > ); } } class Child extends React.Component { constructor ( ) { super (); this .state = { msg : "你好" , }; } handleClick = () => { this .props .getMsg (this .state .msg ); }; render ( ) { return ( <div > 子组件:<button onClick ={this.handleClick} > 给父组件传递数据</button > </div > ); } }
兄弟组件传值
将要共享的数据提升到最近的公共度组件中
公共父组件要做的事:提供共享数据、提供操作共享数据的方法
要传值的子组件通过props
接收数据或是接收操作数据的方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 class Counter extends React.Component { state = { count : 0 , }; add = () => { this .setState ({ count : this .state .count + 1 , }); }; render ( ) { return ( <div > <Child1 count ={this.state.count} /> <Child2 add ={this.add} /> </div > ); } } const Child1 = props => { return <h1 > 计数器:{props.count}</h1 > ; }; const Child2 = props => { return <button onClick ={() => props.add()}>+1</button > ; }; ReactDOM .render (<Counter /> , document .getElementById ("root" ));
Context
使用React.createContext()
创建Provider
和Consumer
两个组件
使用<Provider>
将父组件包起来
设置value
属性,表示要传递的值
使用<Consumer>
组件接收数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 const { Provider , Consumer } = React .createContext ();class App extends React.Component { render ( ) { return ( <Provider value ="pink" > <div > <Node /> </div > </Provider > ); } } const Node = props => { return ( <div > <SubNode /> </div > ); }; const SubNode = props => { return ( <div > <SubNode /> </div > ); }; const Child = props => { return ( <div > <Consumer > {data => <span > 我是子节点 {data}</span > }</Consumer > </div > ); };
组件的生命周期 📚 详细指导
组件的生命周期:组件从被创建到挂载到页面中运行,再到组件不用时卸载的过程
只有类组件才有生命周期
💤 不常用的生命周期:点我 😁
创建时
更新时
卸载时
constructor、更新 DOM 和 refs 时、componentDidMount
constructor、更新 DOM 和 refs 时、render、componentDidUpdate
componentWillUnmount
创建时 🚲 执行顺序:
1 2 3 graph LR A(constructor) -->B(render) B-->C(componentDidMount)
钩子函数
触发时机
作用
constructor
创建组件时,最先执行
1. 初始化 state、2. 为事件处理程序绑定 this
render
每次组件渲染都会触发
渲染界面(**不能调用setState()
**)
componentDidMount
组件挂载(完成 DOM 渲染)后
1. 发送网络请求、2. DOM 操作
componentDidMount
是在render()
、constructor()
外面直接函数调用的,是类的一个成员
更新时
导致组件更新的情况:new props、setState()、forceUpdate()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 class App extends React.Component { constructor (props ) { super (props) this .state = { count : 0 } } handleClick = () => { this .setState ({ count : this .state .count + 1 }) } render ( ) { console .log ("生命周期钩子函数:render" ) return ( <div > <Counter count ={this.state.count} /> <button onClick ={this.handleClick} > 打豆豆</button > </div > ) } } class Counter extends React.Component { render ( ) { console .log ("子组件---生命周期钩子函数:render" ) return ( <h1 > 统计打豆豆的次数:{this.props.count}</h1 > ) } } componentDidUpdate (prevProps ) { if (prevProps.count !== this .props ){ this .setState ({ }) } console .log ("componentDidUpdate" ) }
🚙 执行顺序:
1 2 graph LR A(render) --> B(componentDidUpdate)
钩子函数
触发时机
作用
render
每次组件渲染都会触发
渲染界面
componentDidUpdate
组件更新(完成 DOM 渲染)后
1. 发送网络请求、2. DOM 操作、如果要setState()
,必须放在一个if
条件中
如果没有在 if 里面调用,就会造成递归更新(执行太多次后停下来报错)
卸载时
钩子函数
触发时机
作用
componentWillUnmount
组件卸载(从页面消失)
执行清理工作(如:清理定时器)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <div> { this .state .count > 3 ? <span > 豆豆被打GG了</span > : <Counter count ={this.state.count} /> } <button onClick={this .handleClick }>打豆豆</button> </div> componentWillUnmount ( ) { console .log ("豆豆被GG,我被触发了" ) }
高阶组件
也是用于组件的复用,包装组件,增强组件的功能
HOC,Higher-Order Component
,是一个函数,接收要包装的组件,返回增强后的组件
1 const EnhancedComponent = withHOC (WrappedComponent );
高阶组件内部创建一个类组件,在这个类组件中提供复用的状态逻辑代码,通过 prop 将复用的状态传递给被包装组件
1 2 3 4 5 class Mouse extends React.Component { render ( ) { return <WrappedComponent {...this.state } /> ; } }
基本使用 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 function withMouse (WrappedComponent ) { class Mouse extends React.Component { state = { x : 0 , y : 0 , }; componentDidMount ( ) { window .addEventListener ("mousemove" , this .handleMouseMove ); } componentWillUnmount ( ) { window .removeEventListener ("mousemove" , this .handleMouseMove ); } render ( ) { return <WrappedComponent {...this.state } /> ; } } return Mouse ; } const Position = props => ( <p > 鼠标当前位置:( x: {props.x}, y: {props.y} ) </p > ); const Cat = props => <img src ={src} alt ="cat" /> ;const MousePosition = withMouse (Position );const CatPosition = withMouse (Cat );class App extends React.Component { render ( ) { <div > {/* 6. 渲染增强后的高阶组件 */} <MousePosition /> <CatPosition /> </div > ; } }
displayName
在浏览器的 React 开发者工具中,复用的组件名字显示都是一样的,所以要设置displayName
,设置不一样的名字,便于调试(React Developer Tools)
1 2 3 4 5 6 7 8 9 10 11 12 function withMouse (WrappedComponent ) { class Mouse extends React.component { } Mouse .displayName = ` WithMouse${getDisplayName(WrappedComponent)} ` ; return Mouse ; } function getDisplayName (WrappedComponent ) { return WrappedComponent .displayName || WrappedComponent .name || "Component" ; }
传递 props
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 const Position = props => { console .log (props); return ( <p > 鼠标当前位置:( x: {props.x}, y: {props.y} ) </p > ); }; class App extends React.Component { render ( ) { <div > <MousePosition a ="1" /> </div > ; } }
在<Mosue />
中是可以得到 props 的,但是就没再往下传了
修改,在class Mouse
,继续传下去
1 2 3 render ( ) { return <WrappedComponent {...this.state } {...this.props } /> }
React 原理
组件更新机制:父组件更新,其下面的子组件都会更新,子组件的子组件也会更新
Hooks Hooks 是仅在 React 显然的时候可用的函数,都是以use
开头的,比如useState
setState 说明 1️⃣ setState()
数据更新是异步的
1 2 3 4 5 6 7 8 9 10 11 12 handleClick = () => { this .setState ({ count : this .state .count + 1 , }); this .setState ({ count : this .state .count + 1 , }); console .log ("count: " , this .state .count ); };
setState()
是可以调用多次的,因为是异步的原因,所以后面setState()
的执行不要依赖前面执行的结果
但是render()
只会执行一次
2️⃣ 推荐语法:setState( (state, props) => {} )
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 handleClick = () => { this .setState ((state, props ) => { return { count : state.count + 1 , }; }); this .setState ((state, props ) => { console .log ("第二次调用" , state); return { count : state.count + 1 , }; }); console .log ("count: " , this .state .count ); };
3️⃣ setState()
的第二个参数,是一个回调函数
如果希望更新后执行什么操作,就可以使用这个回调函数
在 DOM 渲染后执行(和componentDidMount
可以相互使用)
1 2 3 4 5 6 7 8 9 10 11 12 handleClick = () => { this .setState ( (state, props ) => { return { count : state.count + 1 }; }, () => { console .log ("状态更新完成!" , this .state .count ); } ); console .log ("count: " , this .state .count ); };
JSX 语法转换过程
JSX 仅仅是createElement()
的语法糖(简化语法)
JSX 语法被@babel/preset-react
插件编译为createElement()
方法
1 2 graph LR A(JSX语法) --> B(createElement) --> C(React元素)
React 元素:是一个对象,用来描述希望在屏幕上看到的内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 const element = { <h1 className ="app" > Hello</h1 > } const element = React .createElement ( "h1" , { className : "app" }, "hello" ) const element = { type : "h1" , props : { className : "app" , children : "hello" } }
在 ES6 中,class 实现的类就是 ES5 中构造函数和原形的语法糖,可以使用typeof
来测一下
组件性能优化 减轻 state
state 中只存放跟组件渲染相关的数据
比如像定时器 id 这样的不用放在 state 中,直接放在 this 中(this.timeId = setTimeout()
)
避免不必要的渲染 1️⃣ 父组件更新子组件也会跟着更新,有时候子组件会跟着有些不必要的更新
使用钩子函数shouldComponentUpdate(nextProps, nextState)
,通过返回值决定该组件是否重新渲染。true 重新,false 不重新
钩子函数触发时机:组件重新渲染前执行。shouldComponentUpdate -> render
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 class App extends React.Component { state = { count : 0 }; handleClick = () => { this .setState (state => { return { count : state.count + 1 , }; }); }; shouldComponentUpdate (nextProps, nextState ) { console .log (nextState); console .log (this .state ); if (nextState.count === this .state .count ) { return false ; } return true ; } render ( ) { console .log ("组件更新了" ); return ( <div > <h1 > 计数器:{this.state.count}</h1 > <button onClick ={this.handleClick} > +1</button > </div > ); } }
纯组件
PureComponent
,其内部自动实现了shouldComponentUpdate
钩子,不需要进行手动比较
组件内部通过分别对比前后两次props
和state
的值,来决定是否重新渲染
1 2 3 4 class App extends React.PureComponent { }
纯组件内部的实现方式:shallow conpare
浅层对比。
对于值类型,直接复制,对于引用类型:
1 2 3 4 5 const obj = { number : 0 };const newObj = obj;newObj.number = 2 ; console .log (newObj === obj);
接着上面的引用类型,若果在 React 中
1 2 3 4 5 state = { obj : { number : 0 } }; state.obj .number = 2 ; setState ({ obj : state.obj });
❗ 所以:state
或props
中属性的值为引用类型时,应该创建新的数据,不要直接修改原数据
1 2 3 4 5 6 7 8 9 10 11 12 13 const newObj = { ...this .state .obj , number : Math .floor (Math .random () * 3 ) };this .setState (() => { return { obj : newObj }; }); this .setState ({ list : [...this .state .list , { 新数据 }], });
虚拟 DOM 和 Diff 算法
只要state
变化就重新渲染视图,有时候会浪费性能。解决这个问题,用到虚拟 DOM 和 Diff 算法
虚拟 DOM 本质上就是一个 JavaScript 对象,用来描述希望看到的内容。(实际上就是 jsx 对象)
执行过程
初次渲染时,React 会根据初始 State。创建一个虚拟 DOM 对象(树)
根据虚拟 DOM 生成真正的 DOM 然后渲染到页面中
当数据变化后(setState()),重新根据新的数据,创建新的虚拟 DOM 对象
使用diff
算法,找到与上一个虚拟 DOM 对比,然后渲染需要更新的内容
然后 React 只更新(patch)变化的内容,渲染到页面中
render 方法的调用并不意味着浏览器中的重新更新,仅仅说明要进行 diff
虚拟 DOM 不是真正的 DOM,只要可以运行 JavaScript 的地方就可以使用,这就使得 React 可以脱离浏览器而存在,可以在 Android 和 IOS 中使用
Hooks Context Hooks
远距离传输数据,不局限于父子组件,不使用 props
Ref Hooks
保存一些在渲染中不会用到的数据,比如 DOM 节点和计时器的 ID
更新 ref 不会重新渲染组件
一般会用到非 React 体系中
Effect Hooks
路由基础 SPA:单页应用程序,就是只有一个 HTML 页面的应用程序。用户体验好,对服务器压力小。 路由:就是组件和 URL 的对应关系,让用户到一个视图到另外的视图中。
基本使用 安装
1 2 3 npm install react-router-dom # 或者 yarn add react-router-dom
导入三个核心组件
1 import { BrowserRouter as Router , Route , Link } from "react-router-dom" ;
除了BrowserRouter
外还有HashRouter
,替换掉就行,不过推荐使用前者(使用的是 HTML5 的history API
)
使用<Router>
组件包裹整个应用,然后使用<Link to="/xxx">
指定路由入口,使用Route
组件指定路由出口
1 2 3 4 5 6 7 8 9 10 11 12 const First = ( ) => { return <div > 我是First</div > ; }; const App = ( ) => { <Router > <div > <h1 > 我是路由</h1 > <Link to ="/first" > 页面一</Link > <Route path ="/first" component ={First} > </Route > </div > </Router > ;};
<Link>
最终编译成<a>
,to 被编译成 href;可以通过location.pathname
得到 to
<Route>
的位置在哪,就在哪个位置渲染
编程式导航
就是通过 JavaScript 代码来实现页面跳转
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 class Login extends React.Component { handleLogin = () => { this .props .history .push ("/home" ); }; render ( ) { return ( <div > <p > 登录页面</p > <button onClick ={handleLogin} > 登录</button > </div > ); } } const Home = props => { const handleBack = ( ) => { props.history .go (-1 ); }; return ( <div > <h2 > 我是后台首页</h2 > <button onClick ={handleLogin} > 返回登录页面</button > </div > ); }; const App = ( ) => ( <Router > <div > <h1 > 编程式导航</h1 > <Link to ="/login" > 去登录页面</Link > <Route path ="/login" component ={Login} > </Route > <Route path ="/home" component ={Home} > </Route > </div > </Router > );
默认路由 :进入页面时就会匹配的路由,使用/
,后面不加内容
1 <Route path="/" , component={Home }></Route >
匹配模式 模糊匹配模式
默认情况下 React 使用模糊匹配模式
模糊匹配规则:只要 pathname 以/
开头就会匹配成功
1 2 3 4 5 6 7 8 9 <Route > <div > <h1 > 默认路由</h1 > <Link to ="/login" > 登录页面</Link > <Route path ="/" component ={Home} > </Route > <Route path ="/login" component ={Login} > </Route > </div > </Route >
不管<Link>
中的 to 里面的内容是什么(to=”/a”,to=”/abc”),<Route path="/">
都会被匹配到
同样,to="/login/a/b"
也能匹配到path="/first"
精确模式
1 <Route exact path="/login" component={Login }></Route >
推荐使用精确模式
其他 一些网站 组件
chakra UI
Material UI
一些库 immer
:修改state 的好帮手,比如对于嵌套好深的对象
NOTE
一般情况下,错误都可以在页面报错信息中找到