Vite+React+TS基础学习,看这里就够了!(上)
话不多说,作为程序员,我们必须拥有的一个重要能力是阅读文档的能力,因此在开篇,先上一些可能有用的文档/文章链接,方便读者查阅:
【React中文文档】:zh-hans.reactjs.org
【Vite中文文档】:cn.vitejs.dev
【Redux中文官网】:cn.redux.js.org
【React Router 中文文档】:react-guide.github.io/react-route…
1. 使用vite创建react18+ts的项目
1-1 新建项目
直接使用以下命令新建并运行一个Vite+React+TS的项目:
//新建项目
pnpm create vite
// 运行
pnpm install
pnpm run dev
1-2 为什么用vite不用creat-react-app来构建项目?
creat-react-app是一种脚手架工具,它会自动帮你配置好React项目所需的各种依赖、编译配置等等。但是它也有一些缺点,例如初始化的项目结构比较复杂,且难以修改;在定制化方面比较受限制。
相比之下,使用pnpm create vite来创建Vite+TS+React项目则更加轻量级。Vite是一个现代化的构建工具,它支持开箱即用的ES模块热更新、快速构建、代码拆分等,而且可以非常灵活地扩展和定制构建流程。而且,由于Vite使用了原生ES模块系统,因此它可以在开发时提供非常快速的构建和热更新。
1-3 关于creat-react-app和vue-cli
creat-react-app和vue-cli都是基于Webpack进行封装的。Webpack是一个现代化的JavaScript应用程序构建工具,它提供了一种强大的机制,可以将项目中的所有资源(例如JavaScript、CSS、图片等)转换为可在浏览器中运行的代码。
creat-react-app和vue-cli在内部都使用Webpack来实现这个目标。它们都提供了一些默认配置,同时也允许开发者根据自己的需要进行自定义配置,以适应不同的项目需求。
比较而言,creat-react-app的默认配置相对简单,对于新手开发者来说非常友好,但也因此在一些特殊情况下需要进行手动配置才能满足开发需求;vue-cli则提供了更加灵活、全面的配置选项,但也因此复杂度相对较高一些。
1-4 Vite相比Webpack的优势?
Vite和Webpack都是JavaScript应用程序的构建工具,它们的主要作用是将所需的模块打包成一份或多份文件,以便在浏览器中运行。它们有很多相似之处,例如两者都支持代码拆分、模块热替换、压缩等功能。
不过,Vite与Webpack相比,有以下几点优势:
- 更快的开发服务器:Vite开发服务器可以利用现代浏览器的原生ES模块加载,从而避免了Webpack热替换机制中常见的模块重编译导致的性能损耗。这意味着Vite可以提供更快的开发服务器,并且支持即时刷新,可以大大提高开发效率。
- 更快的构建速度:由于Vite只需要针对修改的内容进行重新编译,而不是像Webpack一样把整个项目打包起来,因此,Vite的构建速度比Webpack更快,特别是在大型项目中。
- 原生ES模块支持:Vite能够利用原生ES模块的特性,在浏览器中实现更快的模块加载速度,在减少代码大小方面也能够比Webpack更加有效。
- 容易配置:Vite的配置相对简单,不需要像Webpack那样用户必须拥有深入的配置知识才能对其进行定制。
总之,Vite相对于Webpack更快、更轻量,性能表现更佳,同时也具备更加容易配置的特点,这些都使得开发者更加方便快捷地实现高效的开发和构建。
2. 配置vite和ts的alias别名@/
2-1 配置的流程
- 在项目根目录下创建 tsconfig.json 文件,并添加以下代码:
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
}
}
这会将 @/ 路径别名映射到 src/ 目录。
- 在 Vite 的配置文件 vite.config.ts 中添加以下代码:
import path from 'path';
export default {
// ...其他配置项
resolve: {
alias: {
'@': path.resolve(__dirname, './src')
}
}
}
这个配置将别名 @ 映射到 ./src 目录。由于 vite 本身就支持 alias 配置,我们只需要在上面的代码中加入 path.resolve 方法来获取正确的路径即可。
2-2 vite.config.ts 文件中,alias的作用
在 vite.config.ts 文件中,alias 是一个配置项,用于指定模块路径的别名。
它的作用是可以让我们在引入模块的时候,使用简短的路径,而不必关注具体的文件结构。例如,我们可以将 import '@components/Button' 映射到实际的文件路径 src/components/Button,这样可以提高代码的可读性和可维护性。
以下是一个示例的 vite.config.ts 配置文件,其中包含了一个 alias 的配置:
(__dirname 是 Node.js 中的一个全局变量,表示当前模块的目录名。)
import { defineConfig } from 'vite';
import path from 'path';
export default defineConfig({
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'),
'@components': path.resolve(__dirname, 'src/components'),
'@utils': path.resolve(__dirname, 'src/utils')
}
}
});
在上面的示例中,我们将 @、@components 和 @utils 分别映射到了对应的目录路径,这样在引入模块时就可以通过这些别名进行引用,而不必使用冗长的相对路径。
例如,在代码中可以这样使用:
import Button from '@components/Button';
import { formatDate } from '@utils/date';
2-3 为什么要同时配置tsconfig.json与vite.config.json
配置了 tsconfig.json 只是告诉 TypeScript 编译器在编译时如何解析别名,而并不会影响到 Vite 的运行时。因此,我们还需要在 Vite 配置文件中进行配置,以便让 Vite 在开发和生产环境下都能正确地解析别名。
在 vite.config.js 中配置别名的好处是,它可以确保你所有使用的别名都被正确地 resolve。比如,在使用 import './styles/global.css' 引入全局 CSS 样式时,如果需要获取某个相对路径资源,就可以使用 @/ 别名代替项目的根路径,而无需手动拼接路径。
总之,tsconfig.json 用于 TypeScript 的静态类型检查和编译,而 vite.config.js 则用于 Vite 的打包和构建过程,两者都是必需的。
3. 学习react类组件,类组件生命周期触发机制和各自的作用
3-1 组件化开发
3-1-1 函数式和类式组件
React既支持函数式组件,也支持类组件。使用哪种组件类型,取决于你的具体需求。
通常来说,如果你只需要渲染静态UI或执行一些简单的逻辑操作时,可以使用函数式组件。函数式组件编写起来比较简单,易于维护和测试,并且可以提供更好的性能。
而当你有一些复杂的状态管理和组件逻辑时,建议使用类组件。类组件可以更好地管理组件自身的状态(state),并通过生命周期方法来处理组件不同阶段的行为。此外,类组件还可以实现一些高级特性,如Error Boundaries和ShouldComponentUpdate等。
3-1-1-1 函数式组件:
interface MyComponentProps{
name:string
}
//1.创建函数式组件【简单组件】
function MyComponent(props:MyComponentProps){
// console.log(this); //报错,因为在函数式组件中,this指向undefined,不能在函数式组件中使用this
//在函数式组件中,props被视为函数的参数,而不是作为类组件中的实例属性。
// 如果需要在函数式组件中使用组件的props,可以将props作为参数传入组件函数中,并直接引用它。
console.log(props); // 直接使用props来访问组件的属性,取代this的作用
return <h2>我是用函数定义的组件(适用于【简单组件】的定义)</h2>
}
export default MyComponent
执行了ReactDOM.render(.......之后,发生了什么?
-
React解析组件标签,找到了MyComponent组件。
-
随后调用该函数,将返回的虚拟DOM转为真实DOM,随后呈现在页面中。
3-1-1-2 类式组件:
import React from 'react';
interface MyComponentProps{
name:string
}
class MyComponent extends React.Component<MyComponentProps> {
// // constructor可选
// constructor(props:MyComponentProps) {
// super(props);
// }
render(){
//render是放在哪里的?—— MyComponent的原型对象上,供实例使用。
//render中的this是谁?—— MyComponent的实例对象 <=> MyComponent组件实例对象。
console.log('render中的this:',this);
return <h2>我是用类定义的组件(适用于【复杂组件】的定义)</h2>
}
}
export default MyComponent
对于React类组件,constructor是一个可选的方法,它主要用于在创建组件实例时进行初始化工作。在constructor中可以对组件的状态(state)进行初始化,并在需要使用props时通过调用super(props)来获得传递给组件的props。
具体地说,constructor中的super(props)会把props参数传递给React.Component并将其绑定到当前组件实例上的this.props属性。这样,在类组件的其他实例方法中就可以通过this.props来访问组件的props参数。
在代码实现中,构造函数constructor并不是必须存在的,如果没有定义constructor,则React框架会在组件挂载前自动添加一个默认的constructor。因此,在本例中,虽然没有显式定义constructor,但React框架会自动创建一个默认的constructor,并在其中调用了父类构造函数。
总之,虽然在一些简单的情况下可以省略constructor方法,但在需要进行状态初始化、绑定方法和其他高级操作时,constructor是非常有用的。
3-1-2 组件实例三大属性
3-1-2-1 state
组件被称为"状态机", 页面的显示是根据组件的state属性的数据来显示。
import React from 'react';
//创建组件
class Weather extends React.Component{
//初始化状态
state = {isHot:false,wind:'微风'}
render(){
const {isHot,wind} = this.state
return <h1 onClick={this.changeWeather}>今天天气很{isHot ? '炎热' : '凉爽'},{wind}</h1>
}
//自定义方法————要用赋值语句的形式+箭头函数
changeWeather = ()=>{
// 读取显示 ==>this.state.isHot
const isHot = this.state.isHot
// 更新状态-->更新界面 : this.setState({stateName1 : newValue})
this.setState({isHot:!isHot})
}
}
export default Weather;
3-1-2-2 props
所有组件标签的属性的集合对象
给标签指定属性,保存外部数据(可能是一个function)
在组件内部读取属性:this.props.propertyName
作用: 从目标组件外部向组件内部传递数据
对props中的属性值进行类型限制和必要性限制
扩展属性: 将对象的所有属性通过props传递:<Person {...person}/>
import React from 'react';
import PropTypes from 'prop-types';
//创建组件
//因为是tsx文件,所以还要用ts的方式定义参数,否则会报错
interface PersonProps {
name: string;
age: number;
sex: string; // 可选属性,即不一定要传递该属性
speak?:Function;
}
export class Person extends React.Component<PersonProps>{
//对标签属性进行类型、必要性的限制
// 定义 propTypes 和 defaultProps 静态属性
static propTypes = {
name: PropTypes.string.isRequired, // 限制 name 必传且为字符串
sex: PropTypes.string, // 限制 sex 为字符串
age: PropTypes.number, // 限制 age 为数值
speak: PropTypes.func, // 限制 speak 为函数
};
//指定默认标签属性值
static defaultProps = {
sex: '男', // sex 默认值为男
age: 18, // age 默认值为18
};
render(){
// console.log(this);
const {name,age,sex} = this.props
//props是只读的
//this.props.name = 'jack' //此行代码会报错,因为props是只读的
return (
<ul>
<li>姓名:{name}</li>
<li>性别:{sex}</li>
<li>年龄:{age+1}</li>
</ul>
)
}
}
3-1-2-3 refs
组件内包含ref属性的标签元素的集合对象
给操作目标标签指定ref属性,打一个标识
在组件内部获得标签对象:this.refs.refName(只是得到了标签元素对象)
作用:找到组件内部的真实dom元素对象,进而操作它
// 1_字符串形式的ref
<input type="text" ref="myInput" />
// 2_回调函数形式的 ref
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.myInput = null;
// 2-1_定义一个回调函数。
this.handleRef = ref => {
this.childRef = ref;
}
}
render() {
// 2-2_将这个回调函数传递给ref属性
return <input type="text" ref={this.handleRef} />;
}
}
需要注意的是,字符串形式的 ref 已经被官方标记为过时,不再推荐使用,应该采用回调函数形式的 ref。
回调函数式的ref相比字符串的ref具有以下优势:
- 可以获取到组件实例,而字符串的ref只能获取到dom节点
- 在函数组件中使用回调函数式的ref更为方便,而字符串的ref在函数组件中使用会产生问题
- 回调函数式的ref可以避免命名冲突,因为它是一个函数,而字符串的ref需要独立命名
- 回调函数式的ref适用于动态创建组件,字符串的ref则不适用
总的来说,回调函数式的ref相对于字符串的ref更为灵活,功能更加强大,因此在开发中建议使用回调函数式的ref。同时,也需要注意回调函数式的ref可能会在渲染期间被多次调用,需要合理处理。
3-1-3 事件处理
- 在JSX中直接绑定事件处理函数,例如onClick,onChange等。
import React from 'react';
class Example extends React.Component {
handleClick() {
console.log('Button clicked');
}
render() {
return (
<button onClick={this.handleClick}>Click me</button>
);
}
}
- 使用class定义组件时,在constructor中绑定this,然后在事件处理函数中使用箭头函数,保证this指向正确。
import React from 'react';
class Example extends React.Component {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
console.log('Button clicked');
}
render() {
return (
<button onClick={() => this.handleClick()}>Click me</button>
);
}
}
- 使用bind()方法来绑定事件处理函数中的this指向,例如在constructor中使用bind方法绑定this。
import React from 'react';
class Example extends React.Component {
handleClick() {
console.log('Button clicked');
}
render() {
return (
<button onClick={this.handleClick.bind(this)}>Click me</button>
);
}
}
- 通过ref来获取DOM元素,然后手动绑定事件处理函数。
import React from 'react';
class Example extends React.Component {
constructor(props) {
super(props);
this.buttonRef = React.createRef();
}
componentDidMount() {
this.buttonRef.current.addEventListener('click', () => {
console.log('Button clicked');
});
}
render() {
return (
<button ref={this.buttonRef}>Click me</button>
);
}
}
需要注意的是,React事件(handleClick)不是直接绑定到DOM上,而是绑定到渲染React树的根DOM容器中统一管理。当事件发生并冒泡至根节点时,React会使用统一的分发函数dispatchEvent执行回调。原生事件和合成事件的区别在于事件命名方式不同,合成事件采用小驼峰式,原生事件是纯小写。同时,React并不是一开始就把所有事件都绑定在document上,而是采用了一种按需绑定的方式,在需要处理事件时才会去绑定到document上
3-2 生命周期
3-2-1 组件的三个生命周期状态
- Mount:插入真实 DOM
- Update:被重新渲染
- Unmount:被移出真实 DOM
3-2-2 生命周期流程
3-2-2-1 新生命周期
初始化阶段: 由ReactDOM.render()触发---初次渲染
-
constructor() 构造器
-
getDerivedStateFromProps 在state的值在任何时候都取决于props时,可以使用
-
render 用于插入虚拟DOM回调
-
componentDidMount 组件挂载完毕的钩子,已经插入回调=====> 常用
一般在这个钩子中做一些初始化的事,例如:开启定时器、发送网络请求、订阅消息
更新阶段: 由组件内部this.setSate()或父组件重新render触发
-
getDerivedStateFromProps 在state的值在任何时候都取决于props时,可以使用
-
shouldComponentUpdate 控制组件更新的“阀门”
-
render
-
getSnapshotBeforeUpdate 在更新之前获取快照
-
componentDidUpdate 组件更新完毕的钩子
3. 卸载组件: 由ReactDOM.unmountComponentAtNode(div)触发
- componentWillUnmount =====> 常用
一般在这个钩子中做一些收尾的事,例如:关闭定时器、取消订阅消息
3-2-2-2 旧生命周期流程
初始化阶段: 由ReactDOM.render()触发---初次渲染
-
constructor() 构造器
-
componentWillMount() 组件将要挂载的钩子
-
render()
-
componentDidMount() 组件挂载完毕的钩子=====> 常用
一般在这个钩子中做一些初始化的事,例如:开启定时器、发送网络请求、订阅消息
更新阶段: 由组件内部this.setSate()或父组件render触发
-
shouldComponentUpdate() 控制组件更新的“阀门”
-
componentWillUpdate() 组件将要更新的钩子
-
render() =====> 必须使用的一个
-
componentDidUpdate() 组件更新完毕的钩子
卸载组件: 由ReactDOM.unmountComponentAtNode()触发
- componentWillUnmount() 组件将要卸载的钩子=====> 常用
一般在这个钩子中做一些收尾的事,例如:关闭定时器、取消订阅消息
3-2-3 常用的周期
render: 必须重写, 返回一个自定义的虚拟DOM
constructor: 初始化状态, 绑定this(可以箭头函数代替)
componentDidMount : 只执行一次, 已经在dom树中, 适合启动/设置一些监听
4. 学习hook组件
4-1 react hooks引入
react-hooks是react16.8以后,react新增的钩子API,目的是增加代码的可复用性,逻辑性,弥补无状态组件没有生命周期,没有数据管理状态state的缺陷。笔者认为,react-hooks思想和初衷,也是把组件,颗粒化,单元化,形成独立的渲染环境,减少渲染次数,优化性能。
4-2 react hooks解决了什么问题?
如果没有 Hooks,函数组件能够做的只是接受 Props、渲染 UI ,以及触发父组件传过来的事件。所有的处理逻辑都要在类组件中写,这样会使 class 类组件内部错综复杂,每一个类组件都有一套独特的状态,相互之间不能复用,即便是 React 之前出现过 mixin 等复用方式,但是伴随出 mixin 模式下隐式依赖,代码冲突覆盖等问题,也不能成为 React 的中流砥柱的逻辑复用方案。所以 React 放弃 mixin 这种方式。
类组件是一种面向对象思想的体现,类组件之间的状态会随着功能增强而变得越来越臃肿,代码维护成本也比较高,而且不利于后期 tree shaking。所以有必要做出一套函数组件代替类组件的方案,于是 Hooks 也就理所当然的诞生了。
所以 Hooks 出现本质上原因是:
- 让函数组件也能做类组件的事,有自己的状态,可以处理一些副作用,能获取 ref ,也能做数据缓存。
- 解决逻辑复用难的问题。
- 放弃面向对象编程,拥抱函数式编程。
4-3 为什么要使用自定义hooks?
自定义 hooks 是在 React Hooks 基础上的一个拓展,可以根据业务需求制定满足业务需要的组合 hooks ,更注重的是逻辑单元。通过业务场景不同,到底需要React Hooks 做什么,怎么样把一段逻辑封装起来,做到复用,这是自定义 hooks 产生的初衷。
自定义 hooks 也可以说是 React Hooks 聚合产物,其内部有一个或者多个 React Hooks 组成,用于解决一些复杂逻辑。
4-4 常用的hooks
useState
useState 可以使函数组件像类组件一样拥有 state,函数组件通过 useState 可以让组件重新渲染,更新视图。
const [ ①state , ②dispatchAction ] = useState(③initData)
① state,目的提供给 UI ,作为渲染视图的数据源。
② dispatchAction 改变 state 的函数,可以理解为推动函数组件渲染的渲染函数。
③ initData 有两种情况,第一种情况是非函数,将作为 state 初始化的值。 第二种情况是函数,函数的返回值作为 useState 初始化的值。
import { useState } from "react"
interface myInterface{
name:string;
}
const DemoState = (props:myInterface) => {
console.log(props)
/* number为此时state读取值 ,setNumber为派发更新的函数 */
let [number, setNumber] = useState(0) /* 0为初始值 */
return (<div>
{/* 这里展示的又是最新的值,因为在整个事件处理结束之后再重新渲染组件,此时state已经更新好的 */}
<span>{ number }</span>
<button onClick={ ()=> {
setNumber(number+1)
console.log(number) /* 由于 useState 是异步的,点击时state还没有更新好,所以 console.log 同步输出的是上一次更新后的值,并不是最新的值。 */
} } ></button>
</div>)
}
export default DemoState
useRef
useRef 基础介绍:
useRef 可以用来获取元素,缓存状态,接受一个状态 initState 作为初始值,返回一个 ref 对象 cur, cur 上有一个 current 属性就是 ref 对象需要获取的内容。
const cur = React.useRef(initState)
console.log(cur.current)
useRef 基础用法:
useRef 获取 DOM 元素,在 React Native 中虽然没有 DOM 元素,但是也能够获取组件的节点信息( fiber 信息 )。
const DemoUseRef = ()=>{
const dom= useRef(null)
const handerSubmit = ()=>{
/* <div >表单组件</div> dom 节点 */
console.log(dom.current)
}
return <div>
{/* ref 标记当前dom节点 */}
<div ref={dom} >表单组件</div>
<button onClick={()=>handerSubmit()} >提交</button>
</div>
}
如上通过 useRef 来获取 DOM 节点。
useRef 保存状态:
可以利用 useRef 返回的 ref 对象来保存状态,只要当前组件不被销毁,那么状态就会一直存在。
useEffect
在React中,useEffect hook的作用是允许函数组件执行副作用操作。副作用指的是一些不直接跟React渲染结果相关的操作,如数据获取、手动更新DOM、订阅事件等。
正常情况下,函数组件每次渲染都会重新运行所有的代码,但是通过useEffect hook,可以在组件渲染时进行副作用操作,从而保证这些操作仅在必要时才会执行。
useEffect具有三种执行方式:
1. 初始化渲染
初始渲染在初始渲染时,useEffect会在组件挂载之后立即执行回调函数。如果指定了依赖项,React会检查每个依赖项是否发生变化,如果有,则重新执行回调函数。
2. 更新渲染
在组件更新时,useEffect会在所有更新完毕后执行回调函数。如果指定了依赖项,React会检查每个依赖项是否发生变化,如果有,则重新执行回调函数。
3. 卸载组件
在组件卸载时,useEffect会执行清除副作用操作的回调函数。这种情况下不需要指定依赖项。
通过使用useEffect,React组件可以在挂载、更新和卸载时执行一些附加操作,例如设置计时器、获取数据、注册事件监听器等等。它在组件的整个声明周期中都可用,并且可以通过指定依赖项来限制其重新执行的时机。
React hooks也提供了 api ,用于弥补函数组件没有生命周期的缺陷。其本质主要是运用了 hooks 里面的 useEffect , useLayoutEffect,还有 useInsertionEffect。其中最常用的就是 useEffect 。
现在的useEffect就相当与这些生命周期函数钩子的集合体。它以一抵三。
可以把 useEffect Hook 看做 componentDidMount,componentDidUpdate 和 componentWillUnmount 这三个函数的组合。
同时,由于前文所说hooks可以反复多次使用,相互独立。所以我们合理的做法是,给每一个副作用一个单独的useEffect钩子。这样一来,这些副作用不再一股脑堆在生命周期钩子里,代码变得更加清晰。
useEffect(()=>{
return destory
},dep)
useEffect 第一个参数 callback, 返回的 destory , destory 作为下一次callback执行之前调用,用于清除上一次 callback 产生的副作用。
第二个参数作为依赖项,是一个数组,可以有多个依赖项,依赖项改变,执行上一次callback 返回的 destory ,和执行新的 effect 第一个参数 callback 。
对于 useEffect 执行, React 处理逻辑是采用异步调用 ,对于每一个 effect 的 callback, React 会向 setTimeout回调函数一样,放入任务队列,等到主线程任务完成,DOM 更新,js 执行完成,视图绘制完毕,才执行。所以 effect 回调函数不会阻塞浏览器绘制视图。
import { useEffect, useRef, useState } from "react"
/* 模拟数据交互 */
function getUserInfo(a:string){
return new Promise((resolve)=>{
setTimeout(()=>{
resolve({
name:a,
age:16,
})
},500)
})
}
interface Props {
a: string;
}
// 在函数组件声明中,这三种传参方式都是可以的,相当于做了一次解构赋值,不过React更推荐第一种
// React.FC 是 TypeScript 中 React 函数组件的类型标记,它是 React.FunctionComponent 的简写,用于声明一个接收props(属性)并返回一个 JSX.Element 的函数组件。
const DemoEffect:React.FC<Props>= ({a}) => {
// const DemoEffect= ({a}:Props) => {
// const DemoEffect= ({a}:{a:string}) => {
const [ userMessage , setUserMessage ] :any= useState({})
const div= useRef()
const [number, setNumber] = useState(0)
/* 模拟事件监听处理函数 */
const handleResize =()=>{console.log("resize")}
/* useEffect使用 ,这里如果不加限制 ,会使里面的函数重复执行,陷入死循环*/
useEffect(()=>{
/* 请求数据 */
getUserInfo(a).then(res=>{
setUserMessage(res)
})
/* 定时器 延时器等 */
const timer = setInterval(()=>console.log("setInterval"),1000)
/* 操作dom */
console.log(div.current) /* div */
/* 事件监听等 */
window.addEventListener('resize', handleResize)
/* 此函数用于清除副作用 */
return function(){
clearInterval(timer)
window.removeEventListener('resize', handleResize)
}
/* 只有当props->a和state->number改变的时候 ,useEffect副作用函数才重新执行 ,如果此时数组为空[],证明函数只有在初始化的时候执行一次相当于componentDidMount */
},[ a ,number ])
return (<div>
<span>{ userMessage.name }</span>
<span>{ userMessage.age }</span>
<div onClick={ ()=> setNumber(1) } >{ number }</div>
</div>)
}
export {DemoEffect}
useMemo
useMemo 可以在函数组件 render 上下文中同步执行一个函数逻辑,这个函数的返回值可以作为一个新的状态缓存起来。那么这个 hooks 的作用就显而易见了:
场景一:在一些场景下,需要在函数组件中进行大量的逻辑计算,那么我们不期望每一次函数组件渲染都执行这些复杂的计算逻辑,所以就需要在 useMemo 的回调函数中执行这些逻辑,然后把得到的产物(计算结果)缓存起来就可以了。
场景二:React 在整个更新流程中,diff 起到了决定性的作用,比如 Context 中的 provider 通过 diff value 来判断是否更新
useMemo 基础介绍:
const cacheSomething = useMemo(create,deps)
- ① create:第一个参数为一个函数,函数的返回值作为缓存值,如上 demo 中把 Children 对应的 element 对象,缓存起来。
- ② deps: 第二个参数为一个数组,存放当前 useMemo 的依赖项,在函数组件下一次执行的时候,会对比 deps 依赖项里面的状态,是否有改变,如果有改变重新执行 create ,得到新的缓存值。
- ③ cacheSomething:返回值,执行 create 的返回值。如果 deps 中有依赖项改变,返回的重新执行 create 产生的值,否则取上一次缓存值。
useMemo 基础用法:
派生新状态:
function Scope() {
const keeper = useKeep()
const { cacheDispatch, cacheList, hasAliveStatus } = keeper
/* 通过 useMemo 得到派生出来的新状态 contextValue */
const contextValue = useMemo(() => {
return {
cacheDispatch: cacheDispatch.bind(keeper),
hasAliveStatus: hasAliveStatus.bind(keeper),
cacheDestory: (payload) => cacheDispatch.call(keeper, { type: ACTION_DESTORY, payload })
}
}, [keeper])
return <KeepaliveContext.Provider value={contextValue}>
</KeepaliveContext.Provider>
}
如上通过 useMemo 得到派生出来的新状态 contextValue ,只有 keeper 变化的时候,才改变 Provider 的 value 。
缓存计算结果:
function Scope(){
const style = useMemo(()=>{
let computedStyle = {}
// 经过大量的计算
return computedStyle
},[])
return <div style={style} ></div>
}
缓存组件,减少子组件 rerender 次数:
function Scope ({ children }){
const renderChild = useMemo(()=>{ children() },[ children ])
return <div>{ renderChild } </div>
}
useLayoutEffect
useLayoutEffect 基础介绍:
useLayoutEffect 和 useEffect 不同的地方是采用了同步执行,那么和useEffect有什么区别呢?
① 首先 useLayoutEffect 是在 DOM 更新之后,浏览器绘制之前,这样可以方便修改 DOM,获取 DOM 信息,这样浏览器只会绘制一次,如果修改 DOM 布局放在 useEffect ,那 useEffect 执行是在浏览器绘制视图之后,接下来又改 DOM ,就可能会导致浏览器再次回流和重绘。而且由于两次绘制,视图上可能会造成闪现突兀的效果。
② useLayoutEffect callback 中代码执行会阻塞浏览器绘制。
useLayoutEffect 基础用法:
const DemoUseLayoutEffect = () => {
const target = useRef()
useLayoutEffect(() => {
/*我们需要在dom绘制之前,移动dom到制定位置*/
const { x ,y } = getPositon() /* 获取要移动的 x,y坐标 */
animate(target.current,{ x,y })
}, []);
return (
<div >
<span ref={ target } className="animate"></span>
</div>
)
}
useContext
useContext 基础介绍
可以使用 useContext ,来获取父级组件传递过来的 context 值,这个当前值就是最近的父级组件 Provider 设置的 value 值,useContext 参数一般是由 createContext 方式创建的 ,也可以父级上下文 context 传递的 ( 参数为 context )。useContext 可以代替 context.Consumer 来获取 Provider 中保存的 value 值。
const contextValue = useContext(context)
useContext 接受一个参数,一般都是 context 对象,返回值为 context 对象内部保存的 value 值。
useContext 基础用法:
import React,{ useContext, useEffect, useRef, useState } from "react"
const Context = React.createContext({name:"",age:0});
/* 用useContext方式 */
const DemoContext1 = ()=> {
const value:any = useContext(Context)
/* my name is alien */
return <div> my name is { value.name }</div>
}
/* 用Context.Consumer 方式 */
const DemoContext2 = ()=>{
return <Context.Consumer>
{/* my name is alien */}
{ (value)=> <div> my name is { value.name }</div> }
</Context.Consumer>
}
export default ()=>{
return <div>
<Context.Provider value={{ name:'alien' , age:18 }} >
<DemoContext1 />
<DemoContext2 />
</Context.Provider>
</div>
}
useReducer
useReducer 是 react-hooks 提供的能够在无状态组件中运行的类似redux的功能 api 。
useReducer 基础介绍:
const [ ①state , ②dispatchAction ] = useReducer(③reducer) 复制代码
① 更新之后的 state 值。
② 派发更新的 dispatchAction 函数, 本质上和 useState 的 dispatchAction 是一样的。
③ 一个函数 reducer ,我们可以认为它就是一个 redux 中的 reducer , reducer的参数就是常规reducer里面的state和action, 返回改变后的state, 这里有一个需要注意的点就是:如果返回的 state 和之前的 state ,内存指向相同,那么组件将不会更新。
useReducer 基础用法:
import React,{ useContext, useEffect, useReducer, useRef, useState } from "react"
type Action = { name: string, [key: string]: any };
// 这里是子组件的参数接口,如果不知道是什么类型,在父组件使用子组件的地方鼠标放上面会有提示
interface IMyChildrenProps{
dispatch : React.Dispatch<Action>,
state:{number:string}
}
// dispatch: 操作行为触发方法,是唯一能执行action的方法,接受一个action作为参数
// action: 操作行为处理模块
// state: 状态管理容器对象
const MyChildren:React.FC<IMyChildrenProps> = (props) => {
// 在()里面传入props,在这里面解构会更优雅一些
const {dispatch, state} = props;
return (
<div>
子组件中的值为:{state.number}
<button onClick={() => dispatch({name: 'add'})}>在子组件中增加</button>
</div>
);
};
const DemoUseReducer = () => {
//number,就是此处的state存储的一个值,在dispatch相应的action后得到改变
const [number, dispatchNumber] = useReducer((state:number, action:Action) => {
const { payload, name } = action; //解构赋值
switch(name) {
case 'add':
return state + 1;
case 'sub':
return state - 1;
case 'reset':
return payload;
default:
return state;
}
}, 0);
return (
<div>
当前值:{number}
<button onClick={() => dispatchNumber({name: 'add'})}>增加</button>
<button onClick={() => dispatchNumber({name: 'sub'})}>减少</button>
<button onClick={() => dispatchNumber({name: 'reset', payload: 666})}>赋值</button>
{/* 子组件的dispatch函数在父组件中触发,设置好state要存储的值 */}
<MyChildren dispatch={dispatchNumber} state={{number}}/>
</div>
);
}
export default DemoUseReducer;
useImperativeHandle
useImperativeHandle 基础介绍:
useImperativeHandle 可以配合 forwardRef 自定义暴露给父组件的实例值。这个很有用,我们知道,对于子组件,如果是 class 类组件,我们可以通过 ref 获取类组件的实例,但是在子组件是函数组件的情况,如果我们不能直接通过 ref 的,那么此时 useImperativeHandle 和 forwardRef 配合就能达到效果。
useImperativeHandle 接受三个参数:
- 第一个参数ref: 接受 forWardRef 传递过来的 ref。
- 第二个参数 createHandle :处理函数,返回值作为暴露给父组件的 ref 对象。
- 第三个参数 deps : 依赖项 deps ,依赖项更改形成新的 ref 对象。
useImperativeHandle 基础用法:
我们来模拟给场景,用useImperativeHandle,使得父组件能让子组件中的input自动赋值并聚焦。
// 声明传递给子组件参数的类型
interface ISonProps{
props:string,
}
// 声明传递给子组件ref的类型,父组件传递ref给子组件就是要用到这个类型
interface ISonRef {
onFocus: () => void,
onChangeValue: (value: string) => void
}
// 知识重点!!!:
// 使用函数式组件时,常用的传参方式是 const Son:React.FC<ISonProps> = (props)=>{const {prop1, prop2} = props;}
// 但是现在这里使用了forwardRef包裹组件就不能这样传参,因为React.FC返回的是React.FunctionComponent类型,但是forwardRef返回的是React.ForwardRefRenderFunction,会导致类型不匹配的问题
// 因此要像现在这样去传参,<>里面的第一个参数是传入给子组件的ref类型,第二个参数是真正需要传入的参数类型
const Son = forwardRef<ISonRef, ISonProps>((SonProps,ref) =>{
const {props} = SonProps
console.log(props)
// 这个inputRef是子组件给自身标签用的ref,所以类型要用HTMLInputElement
const inputRef = useRef<HTMLInputElement>(null)
const [ inputValue , setInputValue ] = useState('')
// 子组件暴露出去的方法,ref相当于接受 forWardRef 传递过来的 ref。
useImperativeHandle(ref,()=>{
const handleRefs = {
/* 声明方法用于聚焦input框,要判空 */
onFocus(){
if (inputRef && inputRef.current) {
inputRef.current.focus()
}
},
/* 声明方法用于改变input的值 */
onChangeValue(value:string){
setInputValue(value)
}
}
return handleRefs
})
return <div>
<input
placeholder="请输入内容"
ref={inputRef}
defaultValue={inputValue}
/>
</div>
})
// 重点:转发 ref 到子组件
// forwardRef 是一个高阶函数,它会接收一个普通的 React 组件作为入参,
// 然后返回一个可以接收 ref 的新组件。新组件可以将 ref 传递给子组件,从而让父组件可以通过 ref 来操作子组件的 DOM 或获取其内部状态等。
class DemoUseImperativeHandle extends React.Component{
// 这个inputRef是父组件传递给子组件的ref,所以类型是ISonRef
inputRef = React.createRef<ISonRef>()
// 父组件获取子组件实例并调用其方法
handelClick (){
console.log(this.inputRef)
// !表示判空操作
const { onFocus , onChangeValue } =this.inputRef.current!
onFocus()
onChangeValue('let us learn React!')
}
render(){
return <div style={{ marginTop:'50px' }} >
<Son props = "UseImperativeHandle" ref={this.inputRef} />
{/* 这里用了bind(this)来绑定子组件,如果用箭头函数就不用 */}
<button onClick={this.handelClick.bind(this)} >操控子组件</button>
</div>
}
}
export default DemoUseImperativeHandle;
4-5 实现一个自定义hook
总结一下实现自定义 hook 的步骤:
- 定义普通的 JavaScript 函数,命名以 use 开头。
- 在函数内部使用 React 提供的钩子函数,如 useState、useEffect 等,实现需要的功能逻辑。
- 将需要共享给其他组件使用的状态或函数返回,以便在组件中调用。
自定义一个监听窗口大小变化的hooks,变化后返回最新的窗口宽高:
const DemoUseWindowSize = ()=>{
// 不能在函数组件外部直接调用自定义 hook,只能在内部,除非对hook进行封装(放在一个独立的 js 文件中,)
function useWindowSize() {
const [windowSize, setWindowSize] = useState({
width: window.innerWidth,
height: window.innerHeight
});
useEffect(() => {
const handleResize = () => {
setWindowSize({
width: window.innerWidth,
height: window.innerHeight
});
};
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
return windowSize;
}
const { width, height } = useWindowSize();
return (
<div>
<p>窗口宽度:{width}px</p>
<p>窗口高度:{height}px</p>
</div>
);
}
export default DemoUseWindowSize;
5. 组件间传值
5-1 父子组件传值
5-1-1 父组件向子组件传值
- 子组件通过props接收父组件传来的数据
import React from "react";
interface IChildProps{
state:{
name:string,
age:number,
gender:string,
count:number
}
hobby:Array<number>
}
// 子组件
const Child = (props:IChildProps) => {
console.log(props);
return <div>
御剑乘风来,除魔天地间!===Child======{props.state.count}
</div>
};
// 父组件
class Parent extends React.Component {
state={
name:'jack',
age:19,
gender:'男',
count:1
}
render() {
return <div >
御剑乘风来,除魔天地间!
<Child state = {{...this.state}} hobby={[1,2,3,4]}></Child>
</div>
}
}
export default Parent;
5-1-2 子组件向父组件传值
总体思路:利用回调函数,父组件提供回调函数,子组件调用,将要传递的数据,作为回调函数的参数
- 父组件提供回调函数,用于接收数据
- 将该函数作为属性值,传递给子组件
- 子组件通过props接收,并调用回调函数
- 将子组件的数据,作为参数传递给回调函数
interface IChildProps{
getM:(data:string)=>void
}
// 父组件
class Parent extends React.Component {
getMessage = (data: string) => {
console.log('父组件接收数据', data)
}
render() {
return (
<div>
<Child getM={this.getMessage} />
</div>
)
}
}
// 子组件
class Child extends React.Component<IChildProps> {
state = {
msg: '子组件向父组件传递数据'
}
handleMessage = () => {
this.props.getM(this.state.msg)
}
render() {
return (
<div>
<button onClick={this.handleMessage}>点击</button>
</div>
)
}
}
export default Parent;
5-2 兄弟组件传值
总体思路:将状态共享,提升到最近的公共父组件中,由父组件管理状态
- 提升公共状态
- 提供操作共享状态的方法
点击按钮,进行计数。按钮进行计数操作,数字进行展示
- 共享状态就是:数字
- 操作共享状态的方法: 点击按钮,进行数字+1
interface IChildProps{
count?:number,
add?:()=>void
}
// 父组件
class Parent extends React.Component {
//共享状态
state = {
count: 0
}
// 操作共享状态的方法
add = () => {
this.setState({
count: this.state.count + 1
})
}
render() {
return (
<div>
<Child1 count={this.state.count} ></Child1>
<Child2 add={this.add} ></Child2>
</div>
)
}
}
// 两个子组件
// 数据展示
const Child1:React.FC<IChildProps> = (props) => {
return (
<div>{props.count}</div>
)
}
// 逻辑操作
const Child2:React.FC<IChildProps> = (props) => {
return (
<div>
{/* 现在只写一个interface供两个组件使用,可选属性可能导致组件调用函数时发生问题:“props.add”可能为“未定义” */}
{/* 因此调用时要判空 */}
{/* 下面这种方式是原生js的方式,与第一种方式等效 */}
{/* <button onClick={() => { if(typeof props.add=='function') props.add() }}>我是按钮+1</button> */}
<button onClick={() => { props.add?.() }}>我是按钮+1</button>
</div>
)
}
export default Parent;
5-3 祖孙组件传值
Context 跨组件传递数据 【类似vue的 provide / inject】
1、首先,在父组件中创建一个 Context 对象:
import React from 'react';
const MyContext = React.createContext();
2、然后,在需要共享数据的组件的父组件中,通过 Provider 提供数据:
import React from 'react';
import MyContext from './MyContext';
function ParentComponent() {
const sharedData = 'Hello, world!';
return (
<MyContext.Provider value={sharedData}>
<ChildComponent />
</MyContext.Provider>
);
}
3、最后,在需要访问共享数据的组件中,可以使用 useContext 获取数据:
import React, { useContext } from 'react';
import MyContext from './MyContext';
function ChildComponent() {
const value = useContext(MyContext);
return <div>{value}</div>;
}
这样,我们就可以在函数组件中更方便地使用 Context 了。注意,类组件仍然可以使用之前的方式来访问 Context 。
完整例子如下:
const MyContext = React.createContext('');//要提供一个默认值
const Parent = ()=> {
const sharedData = '这是祖先组件传给后代组件的值';
return (
<MyContext.Provider value={sharedData}>
<Child />
</MyContext.Provider>
);
}
const Child = ()=> {
const value = useContext(MyContext);
return <div>{value}</div>;
// 和下面这种获得传递值的方法等效,只不过react18多了一个useContext来获取更加方便
// return (
// <MyContext.Consumer>
// {value => <div>{value}</div>}
// </MyContext.Consumer>
// )
}
export default Parent;
5-3 总结
- 函数式组件(无状态组件)通过props就可以取到数据。类组件(有状态组件)中通过this.props来取数据
- 函数式组件,可以是函数声明function A (){} ,也可以是函数表达式和箭头函数的结合体const A = ()=>{}。二者在事件绑定中,函数表达式和箭头函数的结合体,可以省略this的绑定
- demo对于state有很多重复的编写,其实可以用解构的方式来编写 const { msg } = this.state
- 对于回调函数和箭头函数的文字描述。箭头函数从代码上就可辨识为箭头函数,但回调函数是,延迟执行,需要的时候再执行.文中箭头函数充当了延迟执行的功能,所以把有的箭头函数称之为回调函数,这样更能理解组件代码的执行逻辑
6. 使用ref获取类组件实例和函数组件内部方法
对于类组件,可以在组件定义时创建一个ref,并在 componentDidMount 生命周期中将组件实例赋值给 ref,然后就可以在其他地方引用这个 ref,从而获取到该类组件的实例。
以下是一个示例代码:
import React from "react";
class Son extends React.Component{
doSomething() {
console.log('doSomething');
}
render() {
return <div>My Component</div>;
}
}
class Parent extends React.Component {
// 此处ref的类型为Son即可,注意与 函数组件中使用 useImperativeHandle 和 forwardRef 配合的区别
myRef = React.createRef<Son>()
// 在 componentDidMount 生命周期中将组件实例赋值给 ref,然后就可以在其他地方引用这个 ref,从而获取到该类组件的实例。
// 记得判空操作
componentDidMount() {
this.myRef.current!.doSomething(); // 调用类组件的方法
}
render() {
return <Son ref={this.myRef} />;
}
}
export default Parent
对于函数组件,可以使用 useRef hook 来创建一个 ref,然后将函数组件内部的方法绑定到这个 ref 上,从而在其他地方引用这个 ref,就可以获取到该函数组件内部的方法了。
以下是一个示例代码:
// 声明传递给子组件参数的类型
interface ISonProps{
props:string,
}
// 声明传递给子组件ref的类型,父组件传递ref给子组件就是要用到这个类型
interface ISonRef {
onFocus: () => void,
onChangeValue: (value: string) => void
}
// 知识重点!!!:
// 使用函数式组件时,常用的传参方式是 const Son:React.FC<ISonProps> = (props)=>{const {prop1, prop2} = props;}
// 但是现在这里使用了forwardRef包裹组件就不能这样传参,因为React.FC返回的是React.FunctionComponent类型,但是forwardRef返回的是React.ForwardRefRenderFunction,会导致类型不匹配的问题
// 因此要像现在这样去传参,<>里面的第一个参数是传入给子组件的ref类型,第二个参数是真正需要传入的参数类型
const Son = forwardRef<ISonRef, ISonProps>((SonProps,ref) =>{
const {props} = SonProps
console.log(props)
// 这个inputRef是子组件给自身标签用的ref,所以类型要用HTMLInputElement
const inputRef = useRef<HTMLInputElement>(null)
const [ inputValue , setInputValue ] = useState('')
// 子组件暴露出去的方法,ref相当于接受 forWardRef 传递过来的 ref。
useImperativeHandle(ref,()=>{
const handleRefs = {
/* 声明方法用于聚焦input框,要判空 */
onFocus(){
if (inputRef && inputRef.current) {
inputRef.current.focus()
}
},
/* 声明方法用于改变input的值 */
onChangeValue(value:string){
setInputValue(value)
}
}
return handleRefs
})
return <div>
<input
placeholder="请输入内容"
ref={inputRef}
defaultValue={inputValue}
/>
</div>
})
// 重点:转发 ref 到子组件
// forwardRef 是一个高阶函数,它会接收一个普通的 React 组件作为入参,
// 然后返回一个可以接收 ref 的新组件。新组件可以将 ref 传递给子组件,从而让父组件可以通过 ref 来操作子组件的 DOM 或获取其内部状态等。
class DemoUseImperativeHandle extends React.Component{
// 这个inputRef是父组件传递给子组件的ref,所以类型是ISonRef
inputRef = React.createRef<ISonRef>()
// 父组件获取子组件实例并调用其方法
handelClick (){
console.log(this.inputRef)
// !表示判空操作
const { onFocus , onChangeValue } =this.inputRef.current!
onFocus()
onChangeValue('let us learn React!')
}
render(){
return <div style={{ marginTop:'50px' }} >
<Son props = "UseImperativeHandle" ref={this.inputRef} />
{/* 这里用了bind(this)来绑定子组件,如果用箭头函数就不用 */}
<button onClick={this.handelClick.bind(this)} >操控子组件</button>
</div>
}
}
export default DemoUseImperativeHandle;
需要注意的是,对于函数组件来说,如果要在 useEffect 钩子中调用函数组件内部的方法,需要将 myRef 作为第二个参数传入 useEffect 中,以避免多次调用的情况。
转载自:https://juejin.cn/post/7235279096312463421