一、初次接触
- 如何在html中使用react
- 引入 react 核心js (react.development.js)
- react-dom js (react-dom.development.js)
- 使用react创建一个div
let props = {
style: {
width: '100px',
height: '100px',
backgroundColor: 'tomato'
}
}
const div = React.createElement('div', props)
// 这里的div并非是真实dom,不可以使用dom的原生api
createElement 存在多个参数,前两项分别为元素名称、元素配置(例如style,class),后面的参数均为子节点(也是由React.CreateElement创建的节点,也可以是文本)信息 3、将创建的div挂载到指定节点中
-
1)选取指定节点
// 这里采用ReactDOM去创建一个根节点, let dom = document.getElementById('app') let Root = ReactDOM.createRoot(dom) -
2) 将div挂载到Root中
Root.render(div)
二、react的三个api
1、createElement
- 作用:创建一个 React 元素,React元素不同于虚拟dom与真实dom
- 参数:
-
元素名称
- 若创建的是html标签,则名称应该小写
- 若名称存在大写元素,则被解析为组件
-
属性
- 所有属性写在一个对象里
- 属性中的class类名应放在 className 属性下,避免class与js类冲突
- 事件采用驼峰的形式
// 事件写法 function click(num){ alert('执行了'+ num + '次') } let prop = { className: 'class1 class2' // className 代替 class id: `btn`, onClick: click, // 驼峰 } const button = React.createElement('button', prop, '点击') // 其他写法可能导致的问题(仅针对事件,以onClick 为例) prop = { onClick: click(1) // 解析时会立即执行, 点击时不触发alert } prop = { onClick: () => click(123) // 点击时才执行 alert('执行了123次') } -
元素的内容(子元素)
// 可以有多个子元素,可以传文本,也可以传 React 元素 const div = React.createElement('div', {}, '这是div里的button:',button)
注意点:
- React元素一旦创建不可更改(只能以重新创建并替换的方式进行修改)
- React元素最终会通过虚拟dom的形式转变为真实dom
2、createRoot(ReactDOM)
- 作用:创建根节点
- 参数:根节点对应dom
// 示例
const dom = document.getElementById('root')
const root = ReactDOM.createRoot(dom)
3、render
- 作用:渲染、挂载React元素到root中
- 参数:需要挂载的React元素
注意点:
- 调用render时,根元素中所有内容都会被删除,被替换为react元素
- render不对根节点本身有操作
- 多次调用render时,会做diff算法,只对差异的部分进行更新
三、react中使用jsx
注意点:
- jsx属于js的扩展,直接使用jsx是不被支持的,需要引入babel进行“翻译”
- jsx不是字符串,不需要加引号
- jsx中html标签必须小写,大写会被判定为React组件
- jsx必须有且仅有一个根标签,类似于vue2中template模板
- jsx中标签必须正确结束,单标签也得带上"/"
- 在jsx中,表达式可以采用 {} 嵌入表达式
- 对于 undefined、null、''、布尔值,不显示
- jsx中属性可以直接在标签中设置
// 示例
const div = <div>jsx 测试:<button>按钮</button></div>
// 等同于如下示例 babel解析后的代码与下面类似
const button = React.createElement('button', null, '按钮')
const div = React.createElement('div', null, 'jsx 测试:', button)
jsx中设置属性和事件
/**
* class 需要用className替代
* style 需要用对象表示,对象为表达式,外层需要包含 {}
* 事件采用驼峰的形式, 事件需要传参时可采用高阶函数的形式
*/
const styles = {
boder: '1px solid #e5e5e5',
backgroundColor: 'tomato'
}
let div = <div className='box' style={styles}>一个盒子</div>
function click (num){
alert(num + '次')
}
let button = <button onClick={()=> click(123)}></button>
使用jsx创建一个列表
// 示例
var arr = [<li key='a'>a</li>,<li key='b'>b</li>,<li key='c'>c</li>] // 需要给标签带上key
const ul = <ul>{arr}</ul>
四、手动创建react项目
- 基础项目结构
-
初始化项目 1、执行 npm init -y 此时项目目录下会生成 package.json 文件,使用如下命令安装react依赖
npm i react react-dom react-scripts -S2、依赖安装完成后在 index.html 和 index.js 中加入如下代码
<!-- index.html文件代码示例--> <!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>react项目</title> </head> <body> <div id="root"></div> </body> </html>// index.js 文件代码示例 import { createRoot } from 'react-dom/client'; let App = ( <div> 测试:<button>按钮</button> </div> ); const dom = document.getElementById('root'); const root = createRoot(dom); root.render(App); -
打开终端运行项目
npx react-scripts start也可以在package.json中加入, 然后执行 npm run dev
"scripts": { "dev": "react-scripts start" },
五、react中事件
-
绑定事件 只能将事件写在jsx里进行绑定
-
阻止默认行为 同原生js的阻止默认行为
const a = <a href="https://www.baidu.com/" onClick={click}>超链接</a> function click(e) { e.preventDefault(); // 阻止默认行为 /** * 与原生js的不同 * 无法通过返回 false 阻止默认行为 */ e.stopPropagation(); // 阻止冒泡 }
六、react中state
- 作用:用于数据更新时更新对应视图
- 使用方式:
// 示例
function TestComponent (){
// const [state, setState] = useState(initialState);
const [num, setNum] = useState(1);
/**
* 1、initialState是state变量初始值
* 2、setState是改变state、更新视图的函数,传state的新值,也可以是传函数
*/
function add(){
setNum(++num)
}
return <h1>{num}<button onClick={add}>更改</button></h1>
}
注意点:
- state不能置于顶级组件
- 当我们需要修改state数据时,使用setState进行修改,直接修改不能触发视图更新
- 只有state值发生变化,才会触发组件的更新
- 对于复杂数据类型,需要修改数据的指针才能触发更新,一般是用新的替换旧的
- setState的更新是异步的
异步更新存在的问题及解决方案
function Demo(){
const [num, setNum] = useState(1)
function add(num) {
setTimeout(() => {
// 这里的num是上一次更新后的值
setNum(num + 1)
}, 1000);
}
return <div className="box">
<h1>{num}</h1>
<button onClick={add}>+</button>
</div>
}
// 若短时间内连续点击两次按钮,h1里的num仍旧是2
// 解决方案
function add(){
setTimeout(() => {
setNum((pre)=>pre + 1)
}, 1000);
}
七、dom与useRef
useRef
- 作用:用于获取dom
- 使用方式:
import {useRef} from 'react'
function TestRef (){
let refDom = useRef()
// 需要获取哪个dom,就将refDom赋值给谁的ref,这里获取的是h1
return <div>
<h1 ref={refDom}></h1>
</div>
}
// refDom {current: Element}
除了通过useRef获取dom外,还有另一种获取dom的方式
function TestRef (){
let refDom = {current: null}
// 需要获取哪个dom,就将refDom赋值给谁的ref,这里获取的是h1
return <div>
<h1 ref={refDom}></h1>
</div>
}
// refDom {current: Element}
问题:这两种获取dom的区别是什么呢? 解答:采用useRef获取的dom,在每一次组件更新时,会保留获取到dom的状态;但是采用第二种方式获取的dom,每一次组件更新时,会重新获取,可以通过以下代码进行验证
let temp
function TestRef (){
let refDom = useRef()
const [count, setCount] = useState(1)
console.log(temp === refDom);
temp = refDom
function update(){
setCount(pre => pre + 1)
}
// 在每一次点击按钮时,会发现打印为true; 但是采用第二种方式获取的dom,打印为false
return <div>
<h1 ref={refDom}>{count}</h1>
<button onClick={update}>点击</button>
</div>
}
八、类组件
1、创建方式:
import { Component } from 'react';
class Index extends Component {
render() {
return <div>这是一个类组件</div>;
}
}
export default Index;
2、类组件中 props 获取
render(){
// 类组件props绑定在组件实例中,可以通过this.props 获取
let { age, name, gender } = this.props;
return <ul>
<li>年龄:{age}</li>
<li>姓名:{name}</li>
<li>性别:{gender}</li>
</ul>
}
3、类组件中事件的创建与绑定
click = () => {
// 这里采取箭头函数的写法,避免函数内部无法正确获取 this
console.log(123);
}
render(){
return <button onClick={this.click}>点击</button>
}
4、类组件中state state 和props 一样,也是放在组件实例中,通过this获取。对state的更改通过调用 this.setState 进行修改
state = {
count: 1,
obj: {
name: '张三',
age: 14
}
}
click = () => {
/**
* 1、直接改
*/
this.setState({...this.state, count: 2})
/**
* 2、只写修改的属性
* 对于不在state下直接定义的属性,需要用新的替换旧的
*/
this.setState({count: 2})
this.setState({obj: {...this.state.obj, name: '李四'}})
/**
* 3、回调函数的形式
* 回调函数默认的返回参数是最新的state
*/
this.setState((pre)=> {
pre.count = 2
pre.obj.name = '李四'
return pre
})
}
render(){
return <button onClick={this.click}>点击更新state</button>
}
九、不同组件间传值
示例: 在 App 组件下存在 Form 和 LogList 两个组件,LogList 中需要 listData(Array)来渲染页面,Form 中是对 listData 的维护
处理方式:
-
将 listData 进行提升,在 App 组件下进行维护
const App = () => { const [listData, setListData] = useState([]); return ( <div> {' '} <Form></Form> <LogList listData={listData}></LogList>{' '} </div> ); }; -
在 App 中创建一个函数,传递给 Form 组件,Form 组件将维护的数据通过函数参数的形式传回 App
// App组件 const App = () => { const [listData, setListData] = useState([]); function updateData(params) { setListData(pre=> pre.concat(params)) } return ( <div> <Form update={updateData}></Form> <LogList listData={listData}></LogList> </div> ); }; // Form组件 const Form = (props) => { const { update } = props; const [name, setName] = useState('444'); const [time, setTime] = useState('2022-01-01'); const onSubmit = function (e) { e.preventDefault(); let form = { name, birth: time, }; console.log(form); update(form); setName(''); setTime(''); }; return ( <form className="form" onSubmit={onSubmit}> <div className="form-item"> <label htmlFor="name">姓名:</label> <input id="name" onChange={(e) => onChange(e, setName)} value={name} ></input> </div> <div className="form-item"> <label htmlFor="date">日期:</label> <input type="date" id="date" value={time} onChange={(e) => onChange(e, setTime)} ></input> </div> <div className="form-btn"> <button>添加</button> </div> </form> ); }; // 监听函数 function onChange(e, f) { let value = e.target.value; f((pre) => value); }
十、reactDom的portal
Api:createPortal
作用:用于将指定 react 元素放置于指定 dom 下
在 react 编译过程中,子组件会默认放置在父组件中,示例如下
const TestComponet = () => {
// 在最终编译时,Children组件会被放置到Parent中
return (
<Parent>
<Children></Children>
</Parent>
);
};
可能存在的问题:若是Children中存在弹窗和遮罩,且Children本身有定位和层级且有多个Children,那么每一个Children弹窗触发时,会因为Children层级相同,导致前面的遮罩无法遮住后面的Children
解决思路:将遮罩层放置到与root同级的元素中
- 在index.html中创建一个元素,给一个唯一的id
- 在遮罩组件中,采用CreatePortal将遮罩组件放置到index.html新创建的元素里
// 示例如下
import { createPortal } from 'react-dom';
import './index.css'
const dom = document.getElementById('backdrop-portal'); // 获取index.html中新创建的元素
const PortalDom = (props) => {
const { children } = props;
console.log(children);
return createPortal(<div className="_modal">{children}</div>, dom);
};
export default PortalDom;
十一、react中css模块化
-
为什么要模块化?
- 在react项目中,若是直接采用 import './index.css' 这种写法,那么引入的css都属于全局的css,在项目比较大时,容易出现样式冲突的问题
-
怎么使用模块化样式?
- 1、原先css文件的命名方式改为 .module.css, 例如 index.css 改为 index.module.css
- 2、模块中样式文件导入时,对其命名, 例如
import Classes from './index.module.css'- 3、样式中类名在组件中使用时,采用 命名.类名的形式, 例如:
<div className={Classes.box}><div> -
注意事项
1、module.css 所在文件夹名不能存在空格及中文。
在react的解析中,Classes.box 会根据文件夹名进行计算,将文件夹名、样式名以及新生成的唯一字符拼接成新的样式名。文件夹名中的空格拼接成类名后,会被浏览器识别为多个类名;类名中不能包含中文。 解析示例:
import AppClass from './App.module.css' // 示例 const App = props => { return <div className={AppClass.box}></div> } /** * 解析后 * <div class="App_box__sg5bn"></div> */2、同一个module.css文件解析出来的唯一字符是固定的。
例如示例中的App.module.css, 在不同的组件中进行引入时, 最终解析出来的结果是一致的,都是 'App_' + 类名 + '__sg5bn'
3、不同组件中引入的module.css都是全局的。
为了避免样式冲突,react对module.css中样式做了唯一化处理,但是根据第二条的规律,在某一个未引入App.module.css的组件中,直接使用 App_box__sg5bn 类名,也可以正常的注入样式
十二、Fragment
应用场景:不需要创建额外的根节点,但是又必须给一个根节点
方式:
- 内置 Fragment 组件
import { Fragment } from 'react';
export default function TestFrag() {
return (
<Fragment>
<div>1</div>
<div>2</div>
<div>3</div>
</Fragment>
);
}
- 空标签
export default function TestFrag() {
return (
<>
<div>1</div>
<div>2</div>
<div>3</div>
</>
);
}
- 自定义组件
export default function Out(props) {
return props.children;
}
// 组件使用
export default function TestFrag() {
return (
<Out>
<div>1</div>
<div>2</div>
<div>3</div>
</Out>
);
}
十三、context
作用:一个公用存储空间,便于多个不同组件使用,不依赖于 props 创建方式:
import React, { useContext } from 'react';
const TestContext = React.createContext({
// 固定值
name: '张三',
age: 15,
});
export default TestContext;
使用方式:
/**
* 方法一:
* 1、导入创建的Context (这里是TestContext)
* 2、采用TestContext.Consumer使用
*/
function A() {
return (
// TestContext 开头需要大写, TextContext.Consumer内部需要传递一个函数
<TestContext.Consumer>
{(ctx) => {
// ctx是TestContext注入的值,即{name: '张三', age: 15}
return (
<>
{ctx.name}---{ctx.age}
</>
);
}}
</TestContext.Consumer>
);
}
/**
* 方法二:
* 1、导入创建的Context (这里是TestContext)
* 2、导入useContext钩子
* 3、采用useContext使用(仅可用于函数式组件)
*/
function B() {
const ctx = useContext(TestContext);
return (
<>
{ctx.name}---{ctx.age}
</>
);
}
补充: 上述示例中 Context 中值属于固定值,在实际开发中推荐采用 Xxx.Provider 的方式
// 采用Provider传递value的方式遵循最近原则, 但是最近的Provider的value会全部覆盖上层
function App(props) {
return (
<TestContext.Provider value={{ name: '沙和尚', age: 33 }}>
<A></A>
{/* 沙和尚---33 */}
</TestContext.Provider>
);
}
function App1(props) {
return (
<TestContext.Provider value={{ name: '沙和尚', age: 33 }}>
<TestContext.Provider value={{ name: '唐僧' }}>
<A></A>
{/* 唐僧--- */}
</TestContext.Provider>
</TestContext.Provider>
);
}
function App2(props) {
// 同一个Context 可以根据使用场景设置不同的值,互不干扰
return (
<>
<TestContext.Provider value={{ name: '沙和尚', age: 33 }}>
<A></A>
{/* 沙和尚---33 */}
</TestContext.Provider>
<TestContext.Provider value={{ name: '唐僧', age: 44 }}>
<B></B>
{/* 唐僧---44 */}
</TestContext.Provider>
</>
);
}
十四、effect
函数式组件渲染时存在的问题
示例如下:
// 示例一
import React, { useState } from 'react';
export default function TestEffect() {
const [total, setTotal] = useState(0);
setTotal(1);
return <div>TestEffect---{total}</div>;
}
上述写法会抛出异常: Too many re-renders,原因是组件渲染时 state 更新又重新触发组件渲染,导致了死循环。
提问: 若是将 setTotal(1) 改成 setTotal(0) 是否会抛出上述异常? 答案为是。 这里就与之前提到的 state 值不变,不会触发组件的重渲染产生了矛盾,那么就先看看 setState 的执行流程
函数式组件中 setState 执行流程
- 函数式组件 setState 会调用 reactDOM 中 dispatchSetState()
- dispatchSetState 执行时会区分组件的所处于的阶段
- 如果组件处于渲染阶段
- setState 不会比较前后数据的不同,会直接对组件进行重新渲染
- 如果组件处于非渲染阶段
- setState 会比较前后数据的不同
- 如果不相同 --> 会将组件挂载到渲染队列,对组件进行重新渲染
- 如果相同 --> 见如下示例
- setState 会比较前后数据的不同
- 如果组件处于渲染阶段
// 父组件
function Parent() {
console.log('父组件重新渲染了!');
const [count, setCount] = useState(0);
const onClick = () => {
console.log('点击按钮');
setCount(1);
};
return (
<div>
{count}
<Child></Child>
<button onClick={onClick}>按钮</button>
</div>
);
}
// 子组件
function Child() {
console.log('子组件重新渲染了!');
return <div>子组件</div>;
}
count --> 0
- 第一次点击按钮 count --> 1
- "点击按钮"
- "父组件重新渲染了!"
- "子组件重新渲染了!"
- 第二次点击按钮 count --> 1
- "点击按钮"
- "父组件重新渲染了!"
- 第三次点击按钮 count --> 1
- "点击按钮"
由上可知,在组件非渲染阶段前后数据相同时,react 会在某些情况下继续执行当前组件的渲染,此次渲染不会产生实际的效果,也不会触发子组件的渲染
回到示例一,当我们需要在组件渲染过程中做某些判断时应该怎么处理,假设我们将 setTotal(1)放到异步队列里,在执行 setTotal 时组件的同步渲染任务已经执行完毕,也就是说此时的组件处于非渲染状态,那么 setState 会对 state 的值进行比较,从而避免死循环
// 尝试解决方案一:
function TestEffect() {
const [total, setTotal] = useState(0);
setTimeout(() => {
setTotal(1); //可以正常执行,不会死循环
}, 0);
return <div>TestEffect---{total}</div>;
}
// 尝试解决方案二:
function TestEffect() {
const [total, setTotal] = useState(0);
Promise.resolve().then(() => {
setTotal(1); //可以正常执行,不会死循环
});
return <div>TestEffect---{total}</div>;
}
// 解决方案三:
import { useEffect } from 'react';
function TestEffect() {
const [total, setTotal] = useState(0);
useEffect(() => {
setTotal(1); //可以正常执行,不会死循环
});
return <div>TestEffect---{total}</div>;
}
useEffect 简介
useEffect 是一个钩子函数,有两个参数,第一个参数为函数,第二个参数为数组,可用于定义其依赖的所有变量。
依赖项:
- 依赖项可以不指定。即不传第二个参数,在不指定依赖项的情况下,每一次组件渲染都会执行 Effect (即 useEffect 第一个参数)
- 依赖项为空数组。依赖项空数组表示 Effect 仅在组件挂载时执行一次
- 指定了非空数组。仅数组中任意一项变化时才会触发 Effect 的执行,通常会将 Effect 中涉及的变量作为依赖
注:
-
由 useState()钩子生成的 setState 方法,useState 会确保组件每次重渲染都是一样的,可以不写入依赖
-
Effect 中可指定一个返回函数作为 Effect 的清理函数
- Effect 清理函数 示例如下
useEffect(() => { // 这里是Effect const timer = setTimeout(() => { console.log('执行了'); }, 1000); return () => { // 这里的代码会在下一次Effect执行前执行 window.clearTimeout(timer); }; });-
注意事项
Effect 无法使用异步函数,如果 Effect 需要使用异步函数,可在 Effect 中创建一个异步函数,并进行调用 示例如下
useEffect(() => { const fn = async () => { return await Promise.resolve(123); }; fn(); });
useInsertionEffect、useLayoutEffect、useEffect 区别
执行时机
-
useEffect: react18 里该钩子执行时机会动态的判断
组件挂载 -> state 改变 -> DOM 改变 -> 绘制屏幕 -> useEffect
-
useInsertionEffect:
组件挂载 -> state 改变 -> useInsertionEffect -> DOM 改变 -> 绘制屏幕
-
useLayoutEffect:
组件挂载 -> state 改变 -> DOM 改变 -> useLayoutEffect -> 绘制屏幕
十五、reducer
在 react 项目变得复杂时,函数式组件中 state 会越来越多,后续维护也不是很方便,于是可以采用 reducer 进行优化
示例
import React, { useReducer } from 'react';
function TestReducer() {
// useReducer(reducer, initialArg, init)
/**
* useReducer存在三个参数
* - reducer 函数,该函数的返回值会作为state的新值
* - initialArg 类似于setState初始值
* useReducer返回值和setState一样,返回一个数组,数组第一个元素类似于state;第二个元素是派发器(dispatch),用来指挥reducer执行
* - reducer有两个参数,第一个是state最新值,第二个是dispatch传递的参数
* - dispatch最多只能传递一个参数
*/
const [count, countDispatch] = useReducer((pre, action) => {
// pre 为上一次count值,action是countDispatch传递的参数(这里是事件对象)
console.log(action);
return pre + 1;
}, 1);
return (
<div>
{count}
<button onClick={countDispatch}>点击</button>
</div>
);
}
十六、React.memo
- 使用场景。
- 在 react 中,父组件 state 发生变化后,子组件也会重新渲染,即使子组件没有发生变化,子组件的渲染就会导致性能的浪费,使用 React.memo 后可以解决这种问题
- 介绍
- React.memo() 是一个高阶组件,它接收一个组件,并返回包装后的组件,包装后的组件具有缓存功能
- 包装后的组件仅在组件 props 发生变化时(不影响组件本身 state 的改变引起的重渲染)才触发组件的重渲染,否则每次均返回缓存的组件
示例:
// 案例一
import React, { useState } from 'react';
function B(props) {
console.log('B组件重新渲染了');
return <div>B组件----{props.value}</div>;
}
const C = React.memo(B);
export default function A() {
console.log('A组件重新渲染了');
const [state, setstate] = useState(1);
return (
<div>
A组件----{state}
<button onClick={() => setstate((pre) => pre + 1)}>点击</button>
<C value={state}></C>
</div>
);
}
若是通过 props 传递函数,例如案例二
// 案例二
import React, { useState } from 'react';
function B(props) {
console.log('B组件重新渲染了');
return (
<div>
B组件----{props.value}
<button onClick={props.update}>点击B</button>
</div>
);
}
const C = React.memo(B);
export default function A() {
console.log('A组件重新渲染了');
const [state, setstate] = useState(1);
const update = = useCallback( () => {
setstate((pre) => pre + 1);
}, [])
return (
<div>
A组件----{state}
<button onClick={update}>点击</button>
<C value={state} update={update}></C>
</div>
);
}
那么这里 memo 的缓存就失效了, 因为在 A 组件每一次重渲染后,update 就相当于重新创建了一遍,也就是说 B 组件的 props 发生了改变。有没有办法解决这个问题呢?
useCallback
介绍:
- useCallback 是一个钩子函数,用来创建 react 中的回调函数,使用 useCallback 创建的回调函数不会总在组件重渲染时重新创建
- useCallback 类似于 useEffect,也存在第二个参数(依赖项数组)
若将示例二中 update 方法改成
// 示例三
const update = useCallback(() => {
setstate((pre) => pre + 1);
}, []);
那么,组件 A 重渲染后,update 方法将不会改变,因此 B 组件也不会重渲染
补充:useMemo
场景
function sum(a, b) {
console.log('函数执行了');
return a + b;
}
let a = 10;
let b = 20;
const Test = () => {
const [count, setCount] = useState(1);
if (count % 3 === 0) {
a += count;
}
const cout = sum(a, b);
return (
<div>
<p>和: {cout}</p>
<p>count: {count}</p>
<button onClick={() => setCount((pre) => pre + 1)}>点击</button>
</div>
);
};
在上述场景下,按钮每次点击都会执行 sum 函数, 但是 sum 的返回值一致,当 sum 为一个复杂函数时,对于性能有不好的影响,因此,可以采用 useMemo 来缓存 sum 的执行结果
// useMemo 使用示例
// 需要返回值,两个参数,回调函数和依赖项
const cout = useMemo(() => {
return sum(a, b);
}, [a, b]);
十七、自定义钩子
react 中自定义钩子函数:
如果函数的名字以 use 开头,并且调用了其他的 Hook ,则就称其为一个自定义 Hook 。 Hook 是一种复用状态逻辑的方式,它不复用 state 本身,事实上 Hook 的每次调用都有一个完全独立的 state 。
注意点:自定义钩子函数必须采用 use 开头
简单示例
// 创建自定义钩子,用来修改input值
function useTest(initValue = '') {
const [value, setValue] = useState(initValue);
let onChange = function (event) {
setValue(event.target.value);
};
return {
value,
onChange,
};
}
// 使用
export default function TestUse() {
const inputObj = useTest('这是测试');
return <input {...inputObj}></input>;
}
十八、redux
redux 基本用法
// 创建reducer,更新state
const reducer = (state, { type }) => {
// reducer含两个参数,state和action,action必须为对象
// 若state是复杂数据类型,需要更新指针
switch (type) {
case 'ADD':
return state + 1;
case 'SUB':
return state - 1;
default:
return state;
}
};
// 创建仓库store, 第一个参数是reducer, 第二个参数给定state初始值,初始值必须给
const store = Redux.createStore(reducer, 1);
store.subscribe(() => {
// state变化时执行函数
store.getState(); // 该方法用于获取state最新值
});
// 派发state更新操作
store.dispatch({
type: 'ADD',
});
在简单使用 redux 后会发现一些问题
- 如果 state 比较复杂,将变得难以维护
- 若是 state 更新的方式特别多,dispatch 中分发的 case 会比较复杂,难以维护
- state 每次操作时都需要对 state 进行复制,然后再去修改
对于问题一,redux 中可以针对 state 进行分组,然后进行整合
import { combineReducers, createStore } from 'redux';
const reducer1 = (state, action) => {};
const reducer2 = (state, action) => {};
// 整合reducer
const reducer = combineReducers({
reducer1,
reducer2,
});
但是对于问题二和问题三,就需要借助 RTK(redux toolkit)了
-
RTK 1、创建仓库 store
import { configureStore, createSlice } from '@reduxjs/toolkit'; /** * createSlice 创建reducer切片 * 参数:对象,通过对象的不同属性来指定配置 * */ const stuSlice = createSlice({ name: 'stu', // 唯一值,用来自动生成action中的type initialState: { // state初始值 name: '张三', age: 14, }, reducers: { // 指定state的各种操作,直接在对象中添加方法 setName(state, action) { console.log(action, 'action'); /** * state: 代理对象,可以对state直接进行修改 * action: */ state.name = '李四'; }, }, }); /** * 切片对象会自动生成action * slice的属性actions中存储的是slice自动生成action的创建器(函数),调用函数后会自动生成redux的action对象,函数的参数会 作为action对象的payload * action结构 {type: name/函数名, payload: 函数的参数} */ export const { setName } = stuSlice.actions; /** * configureStore 用来创建store,需要一个配置对象作为参数 */ const store = configureStore({ reducer: { student: stuSlice.reducer, }, }); export default store;2、全局挂载仓库
import App from './App.js'; import { createRoot } from 'react-dom/client'; import store from './store/index.js'; import { Provider } from 'react-redux'; const dom = document.getElementById('root'); const root = createRoot(dom); const app = ( <Provider store={store}> <App></App> </Provider> ); root.render(app);3、在组件中使用 RTK
import React from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { setName } from '../../store/index.js'; export default function RTKTest() { /** * RTK使用 * 1、在入口文件里引入store,并全局挂载 * 2、使用useSelector选择我们想要使用的state * 注意点: * 1)useSelector需要传入一个回调函数,回调函数的返回值即我们获取到的state * 2) 回调函数参数是store中所有state的集合 * 3)可以通过store创建时给定reducer的名字选择对应state * */ const student = useSelector((state) => state.student); /** * 修改state数据 * 1、引入store中导出的action构造器,useDispatch钩子 * 2、采用useDispatch创建dispatch * 3、在dispatch中传入构造器创建的action */ const dispatch = useDispatch(); console.log(dispatch); return ( <div> {JSON.stringify(student)} <button onClick={() => { dispatch(setName()); }} > 点击 </button> </div> ); }
十九、React router
5.X 版本
1、路由基本使用
- 使用示例
// 全局挂载
import { BrowserRouter as Router } from 'react-router-dom';
const app = (
<Router>
<App></App>
</Router>
);
root.render(app);
// 组件中使用
function App() {
return (
<>
<Route path="/" component={Home}></Route>
<Route path="/about" component={About}></Route>
</>
);
}
function Home() {
return <p>Home</p>;
}
function About() {
return <p>About</p>;
}
在上述案例中,可以看到,当路由为 '/' 时,仅 Home 组件进行了渲染,但是当路由为 '/about' 时,Home 组件和 About 组件同时渲染,这是因为在 router 的匹配中,以 '/' 为间隔,由前向后进行匹配,若前面的匹配成功,那么组件便会渲染,子路由不会影响匹配。怎么让路由为'/about'时,仅 About 组件渲染呢?可以使用 'exact'
- 使用示例
<Route path="/" exect component={Home}></Route>
2、Link 与 NavLink
-
使用示例
function Menu() { return ( <> <Link to="/">Home</Link> <NavLink to="/about">About</NavLink> </> ); }
Link 和 NavLink 的基本功能是一致的 不同点:NavLink 中可以使用 activeClassName 在当前路由属于激活状态时,设置上 class 类名;也可以使用 activeStyle 设置样式
3、两种 Router(BrowserRouter 和 HashRouter)
-
相同点 开发环境中,两种路由模式使用体验基本一致
-
不同点
-
路径: HashRouter 通过 url 地址中 hash 值对地址进行匹配,因此路径中存在 '#'
-
跳转方式: BrowserRouter 直接通过 url 地址进行跳转
-
部署:
- react router 可以将 url 地址和组件进行映射,当用户访问某个地址时,与其对应的组件会自动的挂载,当我们通过点击 Link 或 NavLink 构建的链接进行跳转时,跳转不会经过服务器,但是当我们刷新页面或通过普通链接进行跳转时,会向服务器发送请求加载数据。在单页面应用中,服务器中仅存在 index.html 文件
- 在 BrowserRouter 路由模式下,路由跳转后向服务器请求数据,由于不存在对应的 html 文件,因此会报 404;但是在 HashRouter 模式下,服务器不会解析'#'后面的内容,因此可以正常跳转
- 若是必须采用 BrowserRouter 路由模式,也可以在 nginx 服务器配置中,加入如下配置(将所有请求都转发到 index.html),解决刷新后页面 404 的问题
location / { root html; try_files $uri /index.html }
-
4、Route 组件
-
使用 component 挂载组件
-
component 属性用来指定路由匹配后被挂载的组件
-
component 需要直接传递组件的类
-
通过 component 构建的组件它会自动创建组件,并且会自动传递参数
// 参数介绍 - match(匹配的信息) isExact 检查路径是否完全匹配 params 动态路由参数 path 路由的 path url 实际路由 url - location(地址信息) search 请求查询参数,及'?'后面的部分 state 可以记录上一个请求传递的参数 - history(控制页面的跳转) push() 跳转页面 -
push 参数可以为跳转的路由地址,也可以为 location 对象
function go(props) { // location对象的传参方式 props.history.push({ // replace() 替换页面 pathname: '/test', state: { name: 'xx', }, }); }
-
-
使用 render
render 也可以用来指定挂载的组件,它需要一个回调函数作为参数,回调函数的返回值会最终被挂载到 route 中,render 不会自动传递三个属性,render 的参数里包含了三个属性 使用示例
function Test() { const renderComponent = (props) => { // A是组件 props是match,history,location return <A {...props}></A>; }; return <Route path="/" render={renderComponent}></Route>; } -
children 属性
children 也可以用来指定被挂载的组件,children 用法有两种
-
和 render 类似,传递回调函数。当 children 设置回调函数时,该组件无论路径是否匹配都会挂载
-
可以传递组件本身
function Test() { return <Route path="/children" children={<A></A>}></Route>; }children 传递组件本身时,无法在 props 中获取到 route 的三个属性,于是可以通过钩子的形式获取
- useRouteMatch() 获取 match
- useLocation() 获取 location
- useHistory() 获取 history
- useParams() 获取 params
-
5、Prompt 组件
用户跳转时可以给予提示
function Test() {
// 可以通过 when 属性动态设置是否展示提示信息
return (
<>
<Prompt when={true} message="确认离开?"></Prompt>
<input type="text" />
</>
);
}
6、redirect 重定向
function Test() {
// 默认采用 replace 方式跳转, 也可以加入push参数改成push跳转; from 用来指定需要跳转的路由
return (
<>
<Redirect to="/about"></Redirect>
</>
);
}
6.x 版本
1、Routes 与 Route 组件
-
1、Route 组件的容器,在 Routes 中的 Route 只有一个会被匹配,且 Route 必须包含在 Routes 内
-
2、Route 中 component、render 删除,组件不可以直接以标签体的形式放置在 Route 中,改用 element 的方式挂载
-
3、Route 默认采用严格匹配, 若采用不严格匹配,path 后面添加 '*', 示例: '/about/*'
function Test() { return ( <Routes> <Route path="/A" element={<A></A>}></Route> {/* 错误示范 <Route path="/A"><A></A></Route> */} </Routes> ); }
2、获取路由相关信息的钩子
-
useLocation() 获取当前地址信息
-
useParams() 获取参数
-
useMatch(路由) 检查当前路由是否匹配某个路由,若不匹配则返回 null
-
useNavigate() 用于获取跳转页面的函数
// 使用示例 const nav = useNavigate(); /** * @params * to 需要去往的链接地址 * options 对象,可不传,默认采用push方式跳转 */ nav('/about'); //push跳转 nav('/about', { // replace跳转 replace: true, });
3、嵌套路由
-
方式一
function App() { return ( <Routes> <Route path="/test/*" element={<Test></Test>}></Route> </Routes> ); } function Test() { return ( <Routes> <Route path="/A" element={<A></A>}></Route> </Routes> ); } -
方式二
function App() { return ( <Routes> <Route path="/test" element={<Test></Test>}> <Route path="A" element={<A></A>}></Route> {/* 这里 path='A'前面不要加斜杠,加了之后无法确定 '/A' 是否属于 '/test' 的组件; 或者写成 '/test/A' */} </Route> </Routes> ); } function Test() { // Outlet 作用类似于 vue-router 里的 router-view 组件, 用来表明子路由展示的位置 return <Outlet></Outlet>; }
4、其他组件
-
Navigate
function Test() { // 自动跳转组件,默认push跳转 return <Navigate to="/about" replace></Navigate>; } -
NavLink
function Test() { // activeStyle,activeClassName 不再被支持, 更新为 style 和 className return ( <NavLink to="/test" style={({ isActive }) => { return isActive ? { color: 'yellow' } : null; }} className={({ isActive, isPending }) => { return isActive ? 'active' : ''; }} > 测试 </NavLink> ); }
二十、其他api
1、useImperativeHandle
-
使用场景 需要获取子组件内的元素或方法,常规的 useRef 没法满足要求
使用 ref 获取子组件内的信息
function Parent() { const ref = useRef(); // 这里的ref里的current 对应的就是 p 标签 return <Child ref={ref}></Child>; } // forwardRef 可用来指定向外部暴露的组件 const Child = forwardRef((props, ref) => { return <p ref={ref}>子节点</p>; }); -
如何将限制 Parent 组件通过 ref 对 Child 组件的操作
const Child = forwardRef((props, ref) => { useImperativeHandle(ref, () => { // 该函数的返回值即作为ref return { name: 'Child', }; }); return <p ref={ref}>子节点</p>; });
2、useDeferredValue
示例
function Test() {
console.log('组件重新渲染了!');
const [count, setCount] = useState(1);
/**
* 在默认情况下,count的每次改变都会触发Test的重新渲染,也就是会打印一次 '组件重新渲染了!'
* 使用useDeferredValue后,每次state修改时都会先后触发两次重新的渲染
* useDeferredValue 必须以 state 值作为参数
* 这两次渲染对于其他的部分没有区别,但是延迟值两次执行的值不相同
* 第一次执行时,延迟值时state的旧值;第二次执行时,延迟值时state的新值
* 延迟值总是会比原版state值慢一步更新
*/
const deferValue = useDeferredValue(count);
console.log(count, deferValue);
return (
<div>
<p>{count}</p>
<button onClick={() => setCount((pre) => pre + 1)}>点击</button>
</div>
);
}
使用场景
function Parent() {
const [text, setText] = useState('');
const change = (e) => {
setText(e.target.value);
};
/**
* 当多个组件共用一个state时,如果其中一个组件比较卡顿,那么会对所有组件产生影响,
那么可以采用 useDeferredValue 结合 memo 或 useMemo 来缓解,也只能是缓解
*/
const value = useDeferredValue(text);
const component = useMemo(() => {
return <Child text={value}></Child>;
}, [value]);
return (
<div>
<input value={text} onChange={change} />
{component}
</div>
);
}
function Child(props) {
let begin = Date.now();
while (true) {
if (Date.now() - begin > 2000) {
break;
}
}
return <p>{props.text}</p>;
}
针对上述卡顿的场景,也可以采用下面的方式缓解
function Parent() {
const [text, setText] = useState('');
const [text1, setText1] = useState('');
const change = (e) => {
setText(e.target.value);
// startTransition 中设置的setState 会在 其他的setState生效后才执行
startTransition(() => {
setText1(e.target.value);
});
};
const component = useMemo(() => {
return <Child text={text1}></Child>;
}, [text1]);
return (
<div>
<input value={text} onChange={change} />
{component}
</div>
);
}
function Child(props) {
let begin = Date.now();
while (true) {
if (Date.now() - begin > 2000) {
break;
}
}
return <p>{props.text}</p>;
}
3、useId
用于生成一个唯一 id 使用示例
function Test() {
const id = useId();
return <p>{id}</p>;
}