React的个人学习记录

180 阅读25分钟

一、初次接触

  • 如何在html中使用react
  1. 引入 react 核心js (react.development.js)
  2. 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
  • 参数:
  1. 元素名称

    • 若创建的是html标签,则名称应该小写
    • 若名称存在大写元素,则被解析为组件
  2. 属性

    • 所有属性写在一个对象里
    • 属性中的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次')
         }
    
  3. 元素的内容(子元素)

      // 可以有多个子元素,可以传文本,也可以传 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

注意点:

  1. jsx属于js的扩展,直接使用jsx是不被支持的,需要引入babel进行“翻译”
  2. jsx不是字符串,不需要加引号
  3. jsx中html标签必须小写,大写会被判定为React组件
  4. jsx必须有且仅有一个根标签,类似于vue2中template模板
  5. jsx中标签必须正确结束,单标签也得带上"/"
  6. 在jsx中,表达式可以采用 {} 嵌入表达式
  7. 对于 undefined、null、''、布尔值,不显示
  8. 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项目

  • 基础项目结构

image.png

  • 初始化项目 1、执行 npm init -y 此时项目目录下会生成 package.json 文件,使用如下命令安装react依赖

      npm i react react-dom react-scripts -S
    

    2、依赖安装完成后在 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>
  }

注意点:

  1. state不能置于顶级组件
  2. 当我们需要修改state数据时,使用setState进行修改,直接修改不能触发视图更新
  3. 只有state值发生变化,才会触发组件的更新
  4. 对于复杂数据类型,需要修改数据的指针才能触发更新,一般是用新的替换旧的
  5. 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 的维护

处理方式:

  1. 将 listData 进行提升,在 App 组件下进行维护

    const App = () => {
      const [listData, setListData] = useState([]);
      return (
        <div>
          {' '}
          <Form></Form> <LogList listData={listData}></LogList>{' '}
        </div>
      );
    };
    
  2. 在 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同级的元素中

  1. 在index.html中创建一个元素,给一个唯一的id
  2. 在遮罩组件中,采用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

应用场景:不需要创建额外的根节点,但是又必须给一个根节点

方式:

  1. 内置 Fragment 组件
import { Fragment } from 'react';
export default function TestFrag() {
  return (
    <Fragment>
      <div>1</div>
      <div>2</div>
      <div>3</div>
    </Fragment>
  );
}
  1. 空标签
export default function TestFrag() {
  return (
    <>
      <div>1</div>
      <div>2</div>
      <div>3</div>
    </>
  );
}
  1. 自定义组件
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 会比较前后数据的不同
        • 如果不相同 --> 会将组件挂载到渲染队列,对组件进行重新渲染
        • 如果相同 --> 见如下示例
// 父组件
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 是一个钩子函数,有两个参数,第一个参数为函数,第二个参数为数组,可用于定义其依赖的所有变量。

依赖项:

  1. 依赖项可以不指定。即不传第二个参数,在不指定依赖项的情况下,每一次组件渲染都会执行 Effect (即 useEffect 第一个参数)
  2. 依赖项为空数组。依赖项空数组表示 Effect 仅在组件挂载时执行一次
  3. 指定了非空数组。仅数组中任意一项变化时才会触发 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

介绍:

  1. useCallback 是一个钩子函数,用来创建 react 中的回调函数,使用 useCallback 创建的回调函数不会总在组件重渲染时重新创建
  2. 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 后会发现一些问题

  1. 如果 state 比较复杂,将变得难以维护
  2. 若是 state 更新的方式特别多,dispatch 中分发的 case 会比较复杂,难以维护
  3. 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 地址进行跳转

    • 部署:

      1. react router 可以将 url 地址和组件进行映射,当用户访问某个地址时,与其对应的组件会自动的挂载,当我们通过点击 Link 或 NavLink 构建的链接进行跳转时,跳转不会经过服务器,但是当我们刷新页面或通过普通链接进行跳转时,会向服务器发送请求加载数据。在单页面应用中,服务器中仅存在 index.html 文件
      2. 在 BrowserRouter 路由模式下,路由跳转后向服务器请求数据,由于不存在对应的 html 文件,因此会报 404;但是在 HashRouter 模式下,服务器不会解析'#'后面的内容,因此可以正常跳转
      3. 若是必须采用 BrowserRouter 路由模式,也可以在 nginx 服务器配置中,加入如下配置(将所有请求都转发到 index.html),解决刷新后页面 404 的问题
        location / {
          root  html;
          try_files $uri /index.html
        }
      

4、Route 组件

  1. 使用 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',
          },
        });
       
      }
      
  2. 使用 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>;
    }
    
  3. 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>;
}