Vite+React+TS基础学习,看这里就够了!(上)

4,191 阅读34分钟

Vite+React+TS基础学习,看这里就够了!(上).png 话不多说,作为程序员,我们必须拥有的一个重要能力是阅读文档的能力,因此在开篇,先上一些可能有用的文档/文章链接,方便读者查阅:

【一篇让你完全够用的TS指南】:juejin.cn/post/708830…

【一名 vueCoder 总结的 React 基础】:juejin.cn/post/696055…

【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相比,有以下几点优势:

  1. 更快的开发服务器:Vite开发服务器可以利用现代浏览器的原生ES模块加载,从而避免了Webpack热替换机制中常见的模块重编译导致的性能损耗。这意味着Vite可以提供更快的开发服务器,并且支持即时刷新,可以大大提高开发效率。
  2. 更快的构建速度:由于Vite只需要针对修改的内容进行重新编译,而不是像Webpack一样把整个项目打包起来,因此,Vite的构建速度比Webpack更快,特别是在大型项目中。
  3. 原生ES模块支持:Vite能够利用原生ES模块的特性,在浏览器中实现更快的模块加载速度,在减少代码大小方面也能够比Webpack更加有效。
  4. 容易配置:Vite的配置相对简单,不需要像Webpack那样用户必须拥有深入的配置知识才能对其进行定制。

总之,Vite相对于Webpack更快、更轻量,性能表现更佳,同时也具备更加容易配置的特点,这些都使得开发者更加方便快捷地实现高效的开发和构建。

2. 配置vite和ts的alias别名@/

2-1 配置的流程

  1. 在项目根目录下创建 tsconfig.json 文件,并添加以下代码:
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]
    }
  }
}

这会将 @/ 路径别名映射到 src/ 目录。

  1. 在 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(.......之后,发生了什么?

  1. React解析组件标签,找到了MyComponent组件。

  2. 随后调用该函数,将返回的虚拟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具有以下优势:

  1. 可以获取到组件实例,而字符串的ref只能获取到dom节点
  2. 在函数组件中使用回调函数式的ref更为方便,而字符串的ref在函数组件中使用会产生问题
  3. 回调函数式的ref可以避免命名冲突,因为它是一个函数,而字符串的ref需要独立命名
  4. 回调函数式的ref适用于动态创建组件,字符串的ref则不适用

总的来说,回调函数式的ref相对于字符串的ref更为灵活,功能更加强大,因此在开发中建议使用回调函数式的ref。同时,也需要注意回调函数式的ref可能会在渲染期间被多次调用,需要合理处理。

3-1-3 事件处理

  1. 在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>
    );
  }
}
  1. 使用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>
    );
  }
}
  1. 使用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>
    );
  }
}
  1. 通过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()触发---初次渲染

  1.  constructor() 构造器

  2.  getDerivedStateFromProps 在state的值在任何时候都取决于props时,可以使用

  3.  render 用于插入虚拟DOM回调

  4.  componentDidMount 组件挂载完毕的钩子,已经插入回调=====> 常用

一般在这个钩子中做一些初始化的事,例如:开启定时器、发送网络请求、订阅消息

更新阶段: 由组件内部this.setSate()或父组件重新render触发

  1.  getDerivedStateFromProps 在state的值在任何时候都取决于props时,可以使用

  2.  shouldComponentUpdate 控制组件更新的“阀门”

  3.  render

  4.  getSnapshotBeforeUpdate 在更新之前获取快照

  5.  componentDidUpdate 组件更新完毕的钩子

3. 卸载组件: 由ReactDOM.unmountComponentAtNode(div)触发

  1.  componentWillUnmount  =====> 常用

一般在这个钩子中做一些收尾的事,例如:关闭定时器、取消订阅消息

3-2-2-2 旧生命周期流程

初始化阶段: 由ReactDOM.render()触发---初次渲染

  1.  constructor() 构造器

  2.  componentWillMount() 组件将要挂载的钩子

  3.  render()

  4.  componentDidMount() 组件挂载完毕的钩子=====> 常用

一般在这个钩子中做一些初始化的事,例如:开启定时器、发送网络请求、订阅消息

更新阶段: 由组件内部this.setSate()或父组件render触发

  1.  shouldComponentUpdate() 控制组件更新的“阀门”

  2.  componentWillUpdate() 组件将要更新的钩子

  3.  render() =====> 必须使用的一个

  4.  componentDidUpdate() 组件更新完毕的钩子

卸载组件: 由ReactDOM.unmountComponentAtNode()触发

  1.  componentWillUnmount()  组件将要卸载的钩子=====> 常用

一般在这个钩子中做一些收尾的事,例如:关闭定时器、取消订阅消息

3-2-3 常用的周期

render: 必须重写, 返回一个自定义的虚拟DOM

constructor: 初始化状态, 绑定this(可以箭头函数代替)

componentDidMount : 只执行一次, 已经在dom树中, 适合启动/设置一些监听

4. 学习hook组件

4-1 react hooks引入

「React 进阶」 React 全部 Hooks 使用大全 (包含 React v18 版本 )

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 的步骤:

  1. 定义普通的 JavaScript 函数,命名以 use 开头。
  2. 在函数内部使用 React 提供的钩子函数,如 useState、useEffect 等,实现需要的功能逻辑。
  3. 将需要共享给其他组件使用的状态或函数返回,以便在组件中调用。

自定义一个监听窗口大小变化的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 中,以避免多次调用的情况。