Skip to content

React 扩展

鱼骨编程 2023-08-15基础React探索 35560 个字 132 分钟

1. setState

setState 更新状态的 2 种写法

注意:setState 是同步的方法,但是引起 react 更新状态的动作是异步的!react 会合并对同一属性进行操作的多个 setState,执行最后一个。

json
	(1). setState(stateChange, [callback])------对象式的setState
            1.stateChange为状态改变对象(该对象可以体现出状态的更改)
            2.callback是可选的回调函数, 它在状态更新完毕、界面也更新后(render调用后)才被调用

	(2). setState(updater, [callback])------函数式的setState
            1.updater为返回stateChange对象的函数。
            2.updater可以接收到state和props。
            4.callback是可选的回调函数, 它在状态更新、界面也更新后(render调用后)才被调用。
总结:
		1.对象式的setState是函数式的setState的简写方式(语法糖)
		2.使用原则:
				(1).如果新状态不依赖于原状态 ===> 使用对象方式
				(2).如果新状态依赖于原状态 ===> 使用函数方式(会方便一些)
				(3).如果需要在setState()执行后获取最新的状态数据,
					要在第二个callback函数中读取

Demo.jsx

jsx
import React, { Component } from "react";

export default class Demo extends Component {
  state = { count: 0 };

  add = () => {
    //对象式的setState:
    //1.获取原来的count值
    const { count } = this.state;
    //2.更新状态(可以有回调函数,回调函数在 render 之后调用)——> react 中的 setState 实际上是一个异步的更新,不能立马得到结果,所以依赖性的内容需要放到回调函数里面
    this.setState({ count: count + 1 }, () => {
      console.log(this.state.count);
    });
    //console.log('12行的输出',this.state.count); //0 */

    //函数式的setState:不用写 this(也可以有回调函数)
    this.setState((state) => ({ count: state.count + 1 }));
  };

  render() {
    return (
      <div>
        <h1>当前求和为:{this.state.count}</h1>
        <button onClick={this.add}>点我+1</button>
      </div>
    );
  }
}

2. lazy 与 Suspense

React.lazy 函数能让你像渲染常规组件一样处理动态引入的组件。什么意思呢?其实就是懒加载。

懒加载:用的时候再加载

(1) 为什么代码要分割

当你的程序越来越大,代码量越来越多。一个页面上堆积了很多功能,也许有些功能很可能都用不到,但是一样下 载加载到页面上,所以这里面肯定有优化空间。就如图片懒加载的理论。

(2) 实现原理

当 Webpack 解析到该语法时,它会自动地开始进行代码分割(Code Splitting),分割成一个文件,当使用到这个文件的时候会这段代码才会被异步加载。

(3) 解决方案

在 React.lazy 和常用的三方包 react-loadable ,都是使用了这个原理,然后配合 webpack 进行代码打包拆分达到异步加载,这样首屏渲染的速度将大大的提高。

由于 React.lazy 不支持服务端渲染,所以这时候 react-loadable 就是不错的选择。

路由组件的 lazyLoad:Suspense + lazy

这是 React 的功能,而不是 react-router 的

Suspense:悬而未决的

jsx
import React, { Component,lazy,Suspense} from 'react'
import {NavLink,Route,Switch} from 'react-router-dom'
import Loading from './Loading'
//1.通过React的lazy函数配合import()函数动态加载路由组件 ===> 路由组件代码会被分开打包
const Login = lazy(()=>import('@/pages/Login'))

//2.通过<Suspense>指定在加载得到路由打包文件前显示一个自定义loading界面
<Suspense fallback={<Loading/>}>
  <Switch>
    <Route path="/xxx" component={Xxxx}/>
    <Redirect to="/login"/>
	</Switch>
</Suspense>

注意:如果不使用 Suspense 标签的话页面会报错。报错会提示我们,在 React 使用了 lazy 之后,会存在一个加载中的空档期,React 不知道在这个空档期中该显示什么内容,所以需要我们指定它。

image-20230705192732943

例子:

Index.jsx

jsx
import React, { Component,lazy,Suspense} from 'react'
import {NavLink,Route} from 'react-router-dom'

// import Home from './Home'
// import About from './About'

import Loading from './Loading'
const Home = lazy(()=> import('./Home') )
const About = lazy(()=> import('./About'))

export default class Demo extends Component {
  state = {
    visible: false
  }
	render() {
		return (
			<div>
				<div className="row">
					<div className="col-xs-offset-2 col-xs-8">
						<div className="page-header"><h2>React Router Demo</h2></div>
					</div>
				</div>
				<div className="row">
					<div className="col-xs-2 col-xs-offset-2">
						<div className="list-group">
							{/* 在React中靠路由链接实现切换组件--编写路由链接 */}
							<NavLink className="list-group-item" to="/about">About</NavLink>
							<NavLink className="list-group-item" to="/home">Home</NavLink>
						</div>
					</div>
					<div className="col-xs-6">
						<div className="panel">
							<div className="panel-body">
								<Suspense fallback={<Loading/>}>
									{/* 注册路由 */}

									<Route path="/about" component={About}/>
									<Route path="/home" component={Home}/>
								</Suspense>
                <button onClick={() => { this.setState({ visible: true })}}
                  加载OtherComponent组件
                  </button>
                  {/*我们指定了空档期使用Loading展示在界面上面,等OtherComponent 组件异步加载完毕,把OtherComponent 组件的内容替换掉Loading上。*/}
                  <Suspense fallback={<Loading/>}>
                  {
                    this.state.visible && <OtherComponent />
                  }
                	</Suspense>
							</div>
						</div>
					</div>
				</div>
			</div>
		)
	}
}

Loading.jsx

jsx
import React, { Component } from "react";

export default class Loading extends Component {
  render() {
    return (
      <div>
        <h1 style={{ backgroundColor: "gray", color: "orange" }}>
          Loading....
        </h1>
      </div>
    );
  }
}

3. Hooks

1. React Hook/Hooks 是什么?

1. Hook是React16.8的新增特性,它可以让我们在不编写class的情况下使用state以及其他的React特性(比如生命周期、state)
2. Hook指的类似于useState、useEffect这样的函数,Hooks是对这类函数的统称

react@16.8之前,函数式组件没有 state ,没有 this,没有生命周期,没什么大本事,但是之后就完全不同了!

react@16.8 之前 class 组件相对于函数式组件有什么优势

  • class 组件可以定义自己的 state,用来保存组件自己内部的状态
  • class 组件有自己的生命周期,我们可以在对应的生命周期中完成自己的逻辑
  • class 组件可以在状态改变时只会重新执行 render 函数以及我们希望重新调用的生命周期函数 componentDidUpdate 等

class 组件存在的问题

  • 随着业务的增多,class 组件变得非常复杂
  • 学习难度较大
  • 组件复用状态难度大

使用场景

  • Hook 的出现基本可以代替我们之前所有使用 class 组件的地方(除了一些非常不常用的场景)
  • 但是如果是一个旧的项目,你并不需要直接将所有的代码重构为 Hooks,因为它完全向下兼容,你可以渐进式的来使用它
  • Hook 只能在函数组件中使用,不能在类组件,或者函数组件之外的地方使用

使用 Hook 的规则

  • 只在最顶层使用 Hook,不要在循环,条件或嵌套函数中调用 Hook确保总是在你的 React 函数的最顶层调用他们。遵守这条规则,你就能确保 Hook 在每一次渲染中都按照同样的顺序被调用。这让 React 能够在多次的 useState 和 useEffect 调用之间保持 hook 状态的正确。

    根本原因:在每次组件渲染时,Hook 的调用顺序必须保持一致。这意味着在函数组件中,不能根据条件或循环等动态情况改变 Hook 的调用顺序,否则会导致状态混乱或渲染错误。

  • 只在 React 函数中调用 Hook,不要在普通的 JavaScript 函数中调用 Hook(除了自定义 hook 时)

怎么理解只能在最顶层使用 Hook?

也就是说,只能在函数式组件的第一层使用,hook 调用的外面不要包裹别的代码!

错误示范:

jsx
export default function App() {
  if (Math.random() > 0.5) {
    useState(10000);
  }
  const [value, setValue] = useState(0);

  return (
    <div>
      <button onClick={() => setValue(value + 1)}>+</button>
      {value}
    </div>
  );
}

会报错:

22082630-55e9c9e9d9d3b89a

更具体一点的情况就是,有时候状态不满足我们预期条件的时候,希望通过 if else 去控制更新。很遗憾这样是不可以的。

为什么要这样限制呢?

因为,React 它是通过链表去实现 hooks 的调用的。也可以简单理解为通过数组下标去访问实现的。本质上就是,去顺序调用不同的 hooks。第一个 hook 执行完了之后,通过 .next 指向下一节点,然后就可以执行下一 hook。

为什么设计为链表呢?

就很多人下意识肯定都会想,那 react 为什么不用 key 做映射呢,类似与 Map 类型,每个 hook 都用唯一的 key 对应,这样的话不就可以不用顺序调用了吗?

很关键的一点就是,你使用 key 值取映射 hook 的话。那么自定义的 hook 被多个组件调用的话,你很难不保证之前有没有同名的 key 在其他组件内。

那我要是用 Symbol 呢,这样就不重复了吧。很遗憾,那就不能很好地复用了,因为 key 值的唯一性使得总是同一个 key 调用了 hook。

详细解释:由 Dan (react 核心开发,redux 作者)写的:为什么顺序调用对 React Hooks 很重要?

2. 三个常用的 Hook

(1). State Hook: React.useState()
(2). Effect Hook: React.useEffect()
(3). Ref Hook: React.useRef()

求和例子:使用类式组件

jsx
import React from "react";
import ReactDOM from "react-dom";

//类式组件
class Demo extends React.Component {
  state = { count: 0 }; //直接定义 state 对象

  myRef = React.createRef(); //使用React.createRef()这个api得到 dom 实例

  add = () => {
    this.setState((state) => ({ count: state.count + 1 }));
  };

  unmount = () => {
    ReactDOM.unmountComponentAtNode(document.getElementById("root")); //卸载组件
  };

  show = () => {
    alert(this.myRef.current.value);
  };

  componentDidMount() {
    //在组件挂载完毕的钩子中开启定时器
    this.timer = setInterval(() => {
      this.setState((state) => ({ count: state.count + 1 }));
    }, 1000);
  }

  componentWillUnmount() {
    clearInterval(this.timer);
  }

  render() {
    return (
      <div>
        <input type='text' ref={this.myRef} />
        <h2>当前求和为{this.state.count}</h2>
        <button onClick={this.add}>点我+1</button>
        <button onClick={this.unmount}>卸载组件</button>
        <button onClick={this.show}>点击提示数据</button>
      </div>
    );
  }
}

export default Demo;

求和例子:使用函数式组件

jsx
import React from "react";
import ReactDOM from "react-dom";
//import { useState, useRef, useEffect} from 'react' //这样就不需要 React.方法了!

function Demo() {
  //console.log('Demo');

  //数组的解构赋值得到两个变量:
  const [count, setCount] = React.useState(0); //使用React.useState()这个api创建一个 state 数据变量和它的 操作器 ——> vue3 中为 const count = ref(0);
  //

  const myRef = React.useRef(); //使用React.useRef()这个api得到 dom 实例 ——> vue3 中为 const myRef = ref(null);

  React.useEffect(() => {
    //使用React.useEffect()这个api创建生命周期 ——> vue3 中为 onMounted(()=>{})
    let timer = setInterval(() => {
      setCount((count) => count + 1); //使用 setCount 操作器方法 ——> vue3 中为 count.value = count.value + 1;
      //或者:setCount(count+1) //直接赋值的写法
    }, 1000);
    return () => {
      clearInterval(timer);
    };
  }, []);

  //加的回调
  function add() {
    setCount((count) => count + 1);
  }

  //提示输入的回调
  function show() {
    alert(myRef.current.value); //获取值的方法不变
  }

  //卸载组件的回调
  function unmount() {
    ReactDOM.unmountComponentAtNode(document.getElementById("root")); //卸载组件,这个方法不变
  }

  return (
    <div>
      <input type='text' ref={myRef} />
      <h2>当前求和为:{count}</h2> {/*使用 state 的时候直接 使用这个变量即可,和 vue3 中一样*/}
      <button onClick={add}>点我+1</button>
      <button onClick={unmount}>卸载组件</button>
      <button onClick={show}>点我提示数据</button>
    </div>
  );
}

export default Demo;

注意:

1.反常识的地方:因为函数式组件每次渲染都会重新调用一次 Demo()函数,那么理论上来说 count 的值应该每次都会被初始化为 0,那么为什么没有呢?

因为 const [count,setCount] = React.useState(0)这行代码 React 底层做了处理,第一次调用函数的时候就把 count 存下来了,不会因为再次调用函数就覆盖原来的 count 值(单例模式,底层只调用了一次 useState)

3. State Hook : useState

State Hook 是 React 中的一个特殊 Hook,它用于在函数组件中添加和管理局部状态。

在 React 的函数组件中,原本是没有状态的,但通过使用 State Hook,可以在函数组件中创建和更新局部状态。State Hook 提供了 useState 函数,可以用来定义和获取状态以及更新状态的方法。

使用规则

  1. 只能在函数最外层调用 Hook。不要在循环、条件判断或者子函数中调用
  2. 只能在 React 的函数组件中调用 Hook。不要在其他 JavaScript 函数中调用

使用 State Hook 的基本语法如下:

jsx
import React, { useState } from 'react';

function MyComponent() {
  const [state, setState] = useState(initialState);

  // 在组件渲染期间可以使用 state 来读取当前的状态值
  // 使用 setState 函数来更新状态值

  // 组件的渲染内容
  return (
    // JSX
  );
}


// 使用示例:实现类似类组件中的 state
const [state, setState] = useState({
  user:{
    name: 'John',
  	age: 25,
  	email: 'john@example.com'
  },
  flag:true,
  title:"hao"
});

// 更新state:setState必须赋值整个对象,不能单独赋值某一个属性
// 这里更新了 user 对象的 age 属性和 外面的 title 属性
const updateState = () => {
  setState({
    ...prevState,
    user: {
      ...prevState.user,
      age: 26
    },
    title: 'New Title'
	});
};

在上述代码中,useState(initialState) 的调用返回一个由 state 和 setState 组成的数组。state 是当前的状态值,而 setState 是一个用于更新状态值的函数。initialState 是状态的初始值,可以是任意数据类型。

使用 State Hook,我们可以在函数组件中定义和操作局部状态,当状态发生变化时,React 会自动重新渲染组件,并更新相应的 UI。

State Hook 还可以多次使用,以便在一个组件中管理多个不同的状态。

State Hook 的优势包括:

  1. 简洁明了:使用 State Hook 可以在函数组件中直接定义和更新状态,代码更加简洁和易读。
  2. 自动化更新:当调用 setState 函数更新状态时,React 会自动重新渲染组件,并更新相关的 UI。
  3. 函数式编程:State Hook 鼓励使用纯函数式编程的思维方式,帮助我们编写更容易测试和理解的代码。

需要注意的是,每次组件重新渲染时,所有 useState 函数都会被调用。因此,在函数组件中,每个 useState 的调用都是独立的,它们之间不会共享状态。

总结起来,State Hook 是 React 中用于在函数组件中添加和管理局部状态的机制。通过使用 useState 函数,我们可以轻松地在函数组件中定义和更新状态,以实现动态的 UI 和交互。

useState 的参数还可以是一个函数

在 React 中,使用 useState 时,你可以传入一个函数作为初始状态的值。这种方式被称为 "惰性初始状态"。

传入函数作为初始状态的好处是可以延迟计算初始值,尤其是当初始值的计算需要耗费较多时间或者依赖于其他状态或外部数据时。该函数会在组件首次渲染时被调用,并返回初始状态的值。

下面是一个示例代码:

jsx
import React, { useState } from "react";

function Example() {
  const [count, setCount] = useState(() => {
    // 这里的函数可以是任意的计算逻辑,返回初始状态的值
    return expensiveInitialStateCalculation();
  });

  // 在组件中使用 count 状态和 setCount 函数

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

在上述示例中,useState 的初始状态值被计算的逻辑包装在一个函数中。这样,在组件首次渲染时,会调用该函数来获取初始状态值

使用函数作为初始状态值的方式可以帮助你更灵活地定义初始状态,并根据需要进行计算。它只在组件的初始渲染阶段被调用一次,可以避免在每次渲染时都重新进行昂贵的计算获取初始状态值,提高性能。

总结

(1). State Hook让函数组件也可以有state状态, 并进行状态数据的读写操作
(2). 语法: const [xxx, setXxx] = React.useState(initValue) //名字可以随便起
(3). useState()说明:
        参数: 第一次初始化指定的值在内部作缓存(如果不设置为undefined),也可以是一个函数进行复杂的计算设置初始值
        返回值: 是1个包含2个元素的数组, 第1个为内部当前状态值, 第2个为更新状态值的函数
(4). setXxx()2种写法: ——> 和 setState() 类似
        setXxx(newValue): 参数为非函数值, 直接指定新的状态值, 内部用其覆盖原来的状态值
        setXxx(value => newValue): 参数为函数, 接收原本的状态值, 返回新的状态值, 内部用其覆盖原来的状态值
(5). 注意:setXxx()修改值应展开原State再传入新元素,直接覆盖原本的数据

4. Effect Hook : useEffect

Effect Hook 是 React 中的一个特殊 Hook,它用于处理副作用和订阅外部数据源。它允许在函数组件中执行与生命周期方法类似的操作。

Effect Hook 提供了在组件渲染期间处理副作用的机制。副作用包括数据获取、订阅或手动修改 DOM 等操作,这些操作通常在组件生命周期方法中进行。

执行规则

  1. 在 React 执行完更新 DOM 操作之后,就会回调这个函数
  2. 默认情况下,无论是第一次渲染之后还是每次更新之后,都会执行这个回调函数
  3. return 一个回调函数,在取消事件订阅时自动调用
  4. 定义多个 useEffect 时按其定义的先后顺序执行

使用 Effect Hook,可以在函数组件中执行以下操作:

  1. 数据获取和订阅:可以使用 Effect Hook 在组件渲染后获取数据或订阅数据源。通过在 effect 函数中执行异步操作,并在 effect 的清理函数中取消订阅,可以管理数据获取和订阅的生命周期。
  2. 手动修改 DOM:通过 Effect Hook,可以在组件渲染后执行操作来手动修改 DOM。例如,添加事件监听器、执行动画效果或操作第三方 DOM 库等。
  3. 清理操作:Effect Hook 还提供了清理副作用的能力。可以在 effect 函数中返回一个清理函数,该函数在组件卸载前或重新渲染前执行,用于清理副作用产生的资源,如取消订阅、清除定时器等。

使用 Effect Hook 的基本语法如下:

jsx
import React, { useEffect } from 'react';

function MyComponent() {
  useEffect(() => {
    // 在组件渲染后执行的副作用操作
    // 返回一个清理函数用于清理副作用产生的资源
    return () => {
      // 在组件卸载前执行的清理操作
    };
  }, [dependency]);

  // 组件的渲染内容
  return (
    // JSX
  );
}

在 useEffect 中,第一个参数是 effect 函数,第二个参数是一个依赖数组(可选),用于指定何时重新运行 effect。当依赖数组中的值发生变化时,effect 函数将被重新运行。如果依赖数组为空,则 effect 仅在组件首次渲染后运行一次。

Effect Hook 提供了一种更简洁和灵活的方式来处理副作用,使函数组件具有了类似于类组件的生命周期方法的能力。它可以帮助我们更好地组织和管理组件中的副作用逻辑,提高代码的可读性和可维护性。

总结

(1). Effect Hook 可以让你在函数组件中执行副作用操作(用于模拟类组件中的生命周期钩子)
(2). React中的副作用操作:
        发ajax请求数据获取
        设置订阅 / 启动定时器
        手动更改真实DOM
(3). 语法和说明:
        useEffect(() => {
          // 在组件渲染后在此可以执行任何带副作用操作
          // 返回一个清理函数用于清理副作用产生的资源
          return () => { // 在组件卸载前执行
            // 在组件卸载前执行一些收尾工作, 比如清除定时器/取消订阅等
          }
        }, [stateValue]) // 如果指定的是[], 回调函数只会在第一次render()后执行 ——> 等价于componentDidMount()
        //如果不为空,会在第一次render()后和依赖变化后都执行 ——> 等价于componentDidMount()+加了限制的componentDidUpdate()

(4). 可以把 useEffect Hook 看做如下三个函数的组合
        componentDidMount()
        componentDidUpdate()
    		componentWillUnmount()

useEffect 的闭包性

例子:

jsx
useEffect(() => {
  if (userInfo.isLogin) {
    setIsAuthenticated(true);
  } else {
    setIsAuthenticated(false);
  }
  console.log(isAuthenticated); //这里获取不到更新后的值
}, [userInfo.isLogin]);

当在 useEffect 的回调函数中访问状态和 props 时,实际上是在创建一个闭包。所以在 useEffect 内部,对于状态和 props 的访问是闭包值,即在 useEffect 中直接访问的变量会保持初始值,不会实时更新。因此,在你的代码中,console.log(isAuthenticated) 打印的值实际上是在组件渲染时的值,而不是 useEffect 中更新后的值。

修改代码如下:

jsx
import { useEffect } from "react";

useEffect(() => {
  if (userInfo.isLogin) {
    setIsAuthenticated(true);
  } else {
    setIsAuthenticated(false);
  }
}, [userInfo.isLogin]);

console.log(isAuthenticated); //可以在这里打印最新的 isAuthenticated 值

// 或者在这里打印最新的 isAuthenticated 值,因为useEffect会按顺序执行
useEffect(() => {
  console.log(isAuthenticated);
}, [isAuthenticated]);

通过在第二个 useEffect 中监听 isAuthenticated,你将会在 userInfo.isLogin 发生变化时,打印最新的 isAuthenticated 值。

5. Ref Hook : useRef

Ref Hook 是 React 中的一个特殊 Hook,它用于在函数组件中创建和访问引用(ref)对象。

在 React 中,引用对象(ref)用于获取组件或 DOM 元素的引用。它可以在函数组件中用于访问和操作组件实例、DOM 元素或其他需要引用的对象。

Ref Hook 提供了 useRef 函数,用于创建一个 ref 对象,并可以在函数组件中持久地保存引用对象的值。与类组件中的 ref 属性类似,Ref Hook 提供了一种在函数组件中访问和操作引用对象的方式。

使用 Ref Hook 的基本语法如下:

jsx
import React, { useRef } from 'react';

function MyComponent() {
  const ref = useRef(initialValue);

  // 在组件渲染期间可以使用 ref.current 来访问引用对象的值
  // 通过修改 ref.current 的值,可以实现对引用对象的更新

  // 组件的渲染内容
  return (
    // JSX
  );
}

//使用示例:当组件加载完成后,<input> 元素将自动获取焦点
import React, { useRef, useEffect } from 'react';

function MyComponent() {
  const inputRef = useRef(null); //或者写useRef()也可以

  useEffect(() => {
    // 在组件加载完成后,设置输入框获取焦点
    inputRef.current.focus();
  }, []);

  return (
    <div>
      <input type="text" ref={inputRef} />
      <button>Submit</button>
    </div>
  );
}

//使用示例:通过 ref.current 来读取和修改 ref 对象中数组的元素
import React, { useRef } from 'react';

function MyComponent() {
  const ref = useRef([1, 2, 3]);

  console.log(ref.current); // [1, 2, 3]

  ref.current.push(4);
  console.log(ref.current); // [1, 2, 3, 4]

  return null;
}

在上述代码中,useRef(initialValue) 的调用返回一个 ref 对象,其中的 current 属性保存了引用对象的值。initialValue 是可选的初始值,它可以是任意数据类型。

通过 useRef 创建的 ref 对象在整个组件的生命周期中保持不变,即使组件重新渲染,ref 对象的引用也不会改变。因此,可以使用 ref.current 来访问和更新引用对象的值。

Ref Hook 的一些常见用途包括:

  1. 访问 DOM 元素:可以使用 Ref Hook 获取 DOM 元素的引用,以便进行操作,如修改样式、触发事件等。

  2. 保存组件实例:在某些情况下,可能需要在函数组件中访问组件实例,可以使用 Ref Hook 来保存组件实例的引用。

  3. 缓存值:可以使用 Ref Hook 缓存某些计算结果,以避免重复计算或在函数组件重新渲染时丢失值。——> 重要(useRef 和 useState 一样,会在 react 内部作缓存,重新渲染之后值不会丢失)

    ref.current 的值在整个组件生命周期中保持不变(只有我们手动改变才可以),即使组件重新渲染,ref.current 也不会初始化和改变。这使得我们可以在组件中持久地保存和访问引用对象的值。

总结起来,Ref Hook 是 React 中用于在函数组件中创建和访问引用对象的机制。通过使用 useRef 函数,我们可以在函数组件中持久地保存引用对象的值,并在整个组件的生命周期中访问和更新引用对象的值。

useRef 和 useState 的区别:

  • 使用 useRef 时,你通常希望在函数组件中创建一个持久的引用变量,用于访问 DOM 元素或其他引用对象,不会触发组件重新渲染
  • 使用 useState 时,你希望在函数组件中添加和管理组件状态,当状态发生变化时,会触发组件重新渲染。

特别注意,修改 ref.current 的值并不会引发组件的重新渲染。因此,如果修改了 ref.current,但希望组件重新渲染以反映这些变化,需要使用其他机制,例如在 useEffect 中监听 ref.current 的变化

具体怎么监听?

要在 useEffect 中监听 ref.current 的变化,你可以使用 useEffect 的依赖项数组来实现。

下面是一个示例代码,演示了如何在 useEffect 中监听 ref.current 的变化:

jsx
import React, { useRef, useEffect } from "react";

function MyComponent() {
  const inputRef = useRef(null);

  useEffect(() => {
    // 在组件加载完成后 和 inputRef.current 发生变化时执行回调函数
    console.log("inputRef.current 发生变化:", inputRef.current);
  }, [inputRef.current]);

  return (
    <div>
      <input type='text' ref={inputRef} />
      <button>Submit</button>
    </div>
  );
}

在上述代码中,我们在 useEffect 的依赖项数组中指定了 inputRef.current,这意味着当 inputRef.current 发生变化时,useEffect 的回调函数将被执行。

const data = useRef([1, 2, 3]) 和 const [data, setData] = useState([1, 2, 3]) 的区别

const data = useRef([1, 2, 3])const [data, setData] = useState([1, 2, 3]) 的区别在于它们的用途和对组件渲染的影响。

  1. const data = useRef([1, 2, 3])
    • 用途:useRef 用于在 React 组件中保存和管理可变的引用类型数据。
    • 组件渲染的影响:使用 useRef 时,组件的重新渲染不会改变 data.current 的值。data 可以在组件渲染之间保持数据的一致性,因为它的值在不同的渲染之间保持不变。
  2. const [data, setData] = useState([1, 2, 3])
    • 用途:useState 用于在 React 组件中创建和管理状态(state)。
    • 组件渲染的影响:使用 useState 创建的状态会影响组件的重新渲染。当调用 setData 更新状态时,组件会重新渲染,并使用新的状态值。

总结:

  • useRef 适用于保存和管理可变的引用类型数据,不会触发组件重新渲染。
  • useState 适用于创建和管理组件的状态,当状态发生改变时会触发组件的重新渲染。

根据你的需求,选择适合的方式来管理数据和状态。如果你需要在组件重新渲染之间保持数据的一致性,或者需要进行一些与渲染无关的操作,可以使用 useRef。如果你需要在组件内部管理和更新状态,并希望触发组件重新渲染,可以使用 useState

总结

(1). Ref Hook可以在函数组件中存储/查找组件内的标签或任意其它数据
(2). 语法: const refContainer = useRef()
(3). 作用: 保存标签对象, 功能与React.createRef()一样

6. useContext

useContext 是 React 提供的一个 Hook,用于在函数组件中访问 Context。(当做消费者)

Context 是 React 中一种跨组件层级共享数据的方式。通过使用 Context,我们可以在组件树中的任何地方传递数据,而不必通过逐层传递 props(仅限于有层级关系之间的组件)。

useContext 接受一个 Context 对象作为参数,并返回该 Context 的当前值。它允许我们在函数组件中访问和使用 Context 的值。

以下是 useContext 的基本用法:

  1. 首先,在创建 Context 时,使用 React.createContext 来定义一个 Context 对象。例如:

    jsx
    const MyContext = React.createContext();
  2. 在 Context 的上层组件中,使用 MyContext.Provider 来提供 Context 的值。例如:

    jsx
    const contextValue = "Hello, Context!"; //把这个字符串向后代传递
    <MyContext.Provider value={contextValue}>{/* 子组件 */}</MyContext.Provider>;

    这里的 contextValue 是要共享的数据或状态。

  3. 在需要访问 Context 的组件中,使用 useContext Hook 来获取 Context 的当前值。例如:

    jsx
    const contextValue = useContext(MyContext); //这样就不必使用<MyContext.Consumer>了

    然后,你可以在组件中使用 contextValue 来访问共享的数据。

请注意,useContext 只能在函数组件的顶层使用。如果需要在其他地方(如自定义 Hook 或普通 JavaScript 函数)访问 Context,可以考虑使用 MyContext.ConsumerMyContext.Providervalue 属性。

通过使用 useContext,我们可以轻松地在 React 组件中访问和共享 Context 的值,避免了通过 props 层层传递的繁琐过程。

7. useReducer

useReducer 是 React 提供的一个 Hook,用于管理复杂的状态和数据逻辑(状态的逻辑比较复杂)。它可以替代 useState 来管理状态,并提供了更强大的状态更新控制。

useReducer 接受两个参数:reducer 函数和初始状态。reducer 函数负责根据不同的操作类型来更新状态,并返回新的状态。初始状态可以是任意类型的值。

使用 useReducer 的基本语法如下:

jsx
const [state, dispatch] = useReducer(reducer, initialState);

其中,state 是当前的状态值,dispatch 是一个函数,用于触发状态更新操作。

reducer 是一个纯函数,接收当前的状态和一个 action 对象作为参数,根据 action 的类型执行相应的操作,并返回新的状态。reducer 函数的定义通常采用 switch/case 结构,根据 action 的类型来决定如何更新状态。

下面是一个简单的示例:

jsx
const initialState = { count: 0 };

function reducer(state, action) {
  switch (action.type) {
    case "increment":
      return { count: state.count + action.payload };
    case "decrement":
      return { count: state.count - action.payload };
    default:
      throw new Error("Unknown action type");
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <div>
      Count: {state.count}
      <button onClick={() => dispatch({ type: "increment", payload: 5 })}>
        Increment
      </button>
      <button onClick={() => dispatch({ type: "decrement", payload: 3 })}>
        Decrement
      </button>
    </div>
  );
}

在上面的示例中,我们定义了一个简单的计数器组件 Counter,使用 useReducer 来管理状态。reducer 函数根据传入的 action 来更新状态,然后通过 dispatch 函数来触发状态更新操作。点击按钮时,我们分别触发了 incrementdecrement 两种操作,从而改变了状态值。

通过使用 useReducer,我们可以更好地管理复杂的状态逻辑,并将状态的更新操作集中在一个地方,使代码更易于理解和维护。

理解

  • useReducer 并非是 Redux 的替代品,而是类似 useState 的替代方案 ——> 只是操作数据的方式类似于 redux 的模式!和 redux 完全是两种东西!
  • 在某些场景下,如果 state 的处理逻辑比较复杂,我们可以通过 useReducer 来对其进行拆分和统一设置处理方法 ——> initialState 相当于 useState 的源数据,reducer 则是 useState 所不具备的一个与源数据配套的多功能操作器
  • 不同的 initialState 数据是不会共享的,它们只是使用了相同的 counterReducer 的函数(即 reducer)而已

useReducer 和 Redux 的关系?

useReducerRedux 是两种不同的状态管理方案,但它们之间存在一些关系和相似之处。

  1. 相似性:useReducer 和 Redux 都是用于管理状态的工具,它们都可以帮助你在 React 应用中进行状态管理和数据流控制。它们都采用了类似的 reducer 函数来处理状态更新,通过操作类型来触发状态的改变。
  2. Redux 的灵感来源:useReducer 的设计灵感来自于 Redux。Redux 是一个独立的状态管理库,它提供了更丰富的功能和工具,用于管理全局状态。Redux 中的核心概念包括 store、action 和 reducer,而 useReducer 通过类似的 reducer 函数和 action 对象来管理组件级的状态。
  3. Redux 的规模和生态系统:Redux 在状态管理方面更适用于中大型的应用程序,它提供了强大的工具和中间件来处理复杂的数据逻辑和异步操作。Redux 有着庞大的生态系统,许多插件和库可以与之配合使用,以满足不同的需求。
  4. useReducer 的局限性:useReducer 是 React 自带的一个 Hook,它在某些情况下可以替代 Redux 来进行简单的状态管理,但对于复杂的应用程序和全局状态管理,Redux 提供了更完善的解决方案。Redux 还具有时间旅行调试、持久化存储等高级功能,而 useReducer 只是一个简化的状态管理工具。

总而言之,useReducer 是 React 提供的一个用于管理组件级状态的工具,它在某些情况下可以替代简单的 Redux 应用。但对于大型应用或复杂的状态管理需求,Redux 提供了更强大的工具和生态系统。选择使用 useReducer 还是 Redux 取决于你的项目需求和规模。

useReducer 可以在多个组件之间共享状态吗?

useReducer 是用于在单个组件中管理状态的 Hook,它的作用范围仅限于当前组件。因此,每个组件都需要自己的 useReducer 来管理其独立的状态。

如果你需要在多个组件之间共享状态,可以考虑使用其他的状态管理方案,如 Redux、Context API 或其他第三方的状态管理库。这些工具提供了全局状态管理的机制,可以让多个组件共享和访问相同的状态。

8. useCallback 与 memo(重点)

useCallback 是一个用于优化性能的 Hook,它用于创建记忆化的回调函数。——> 用在父组件或本组件中

在 React 组件中,当父组件重新渲染时,会传递 props 给子组件,它会导致所有子组件重新渲染。

上面这句话的理解:默认情况下,React 会认为父组件的所有通过 props 传递给子组件了的属性(包括回调函数)都可能会影响子组件的输出结果,因此在父组件重新渲染时,会将这些属性重新传递给子组件,并触发子组件的重新渲染。即使这些属性的值没有发生实际变化,子组件也会被重新渲染。

如果回调函数没有被优化,每次父组件重新渲染时都会创建一个新的函数实例(新的函数会传递给子组件,子组件检测到变化会重新渲染),这可能会导致子组件进行不必要的重新渲染。 ——> 每次组件重新渲染时函数都要重新实例化,如果此时是被 props 传递的,那么还会导致子组件跟着一起重新渲染

useCallback 可以帮助我们优化这种情况。它接受一个回调函数和一个依赖项数组,并返回一个记忆化的回调函数。只有在依赖项发生变化时,才会重新创建回调函数。这样,可以确保回调函数的实例在依赖项未发生变化时被重复使用,避免不必要的重新渲染。——> 包裹函数之后,这个函数不会因为组件重新渲染而重新实例化,即使被传递到了子组件中也是一样

下面是一个示例,展示了如何使用 useCallback

jsx
function MyComponent() {
  const [count, setCount] = useState(0);

  //不会由于重新渲染被重新实例化,状态只依赖于依赖项数组
  const handleClick = useCallback(() => {
    setCount((prevCount) => prevCount + 1);
  }, []); // 依赖项数组为空,表示回调函数不依赖任何状态

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={handleClick}>Increment</button>
    </div>
  );
}

在上面的示例中,handleClick 是一个回调函数,它通过 setCount 更新状态。使用 useCallbackhandleClick 包裹起来,确保它的实例在组件重新渲染时保持不变。

依赖项数组为空 [] 表示回调函数不依赖任何状态,因此只会在组件首次渲染时创建一次,而不会受到其他状态的变化而重新创建。

通过使用 useCallback,可以避免不必要的回调函数重新创建,提高组件的性能。特别是在将回调函数传递给子组件时,使用 useCallback 可以减少子组件的重新渲染次数。

将回调函数传递给子组件时,使用 useCallback ,怎么做?——> 大重点

以下是一个示例,演示了如何使用 useCallback 来优化子组件的性能:

jsx
function ParentComponent() {
  const [count, setCount] = useState(0);

  const handleClick = useCallback(() => {
    // 处理点击事件
    setCount((prevCount) => prevCount + 1);
  }, [count]); // 依赖项为count

  return (
    <div>
      <p>Count: {count}</p>
      <ChildComponent onClick={handleClick} />
    </div>
  );
}

function ChildComponent({ onClick }) {
  console.log("ChildComponent 渲染");

  return <button onClick={onClick}>Increment</button>;
}

在上述示例中,ParentComponent 组件包含一个状态 count 和一个回调函数 handleClick,通过 useCallback 创建并记忆化了 handleClick。然后将 handleClick 作为 onClick 属性传递给子组件 ChildComponent

当点击子组件中的按钮时,会触发 handleClick 回调函数,从而更新父组件的状态 count。这里的关键是,由于使用了 useCallback,只有在 count 发生变化时,handleClick 才会重新创建。否则,子组件 ChildComponent 不会因为父组件的重新渲染而重新渲染。

通过使用 useCallback可以避免将新的回调函数实例传递给子组件,从而减少子组件的重新渲染次数,提高性能。

理解

  • useCallback 实际目的是为了进行性能的优化
  • 将回调函数传入 useCallback 中,当所依赖值发生变化时才渲染该函数

注意事项:如果除了函数之外 props 还传递了其他属性值,那么子组件还是会被重新渲染,即使用了 useCallback

useCallback 主要用于优化函数的重新创建,而不会对其他属性值进行优化。

如果父组件传递给子组件的属性值中包含了除回调函数外的其他属性,当这些属性值发生变化时,子组件仍然会被重新渲染,即使使用了 useCallback

useCallback 主要是用来避免不必要的函数重新创建,以提高性能。它可以确保在依赖项没有变化时,返回相同的函数实例。这样,当父组件重新渲染时,如果传递给子组件的回调函数实例保持不变,React 就会跳过子组件的重新渲染。 ——> 所以说useCallback 是用在父组件中的,之所以一般用于优化回调函数的重新创建,是因为大多数情况下传递给子组件的 props 是一个回调函数!

进一步的优化

如果你希望进一步优化子组件的渲染,可以考虑使用 React.memo(对于函数组件)或 shouldComponentUpdate、PureComponent(对于类组件)。

所以说:

  • 子组件为函数式组件时需要用 memo()方法进行包裹
  • 子组件为类组件时需要使用 shouldComponentUpdate 钩子 或者 继承 PureComponent 类(封装了 shouldComponentUpdate)

memo 简介

  • 为啥起 memo 这个名字?

在计算机领域,记忆化是一种主要用来提升计算机程序速度的优化技术方案。它将开销较大的函数调用的返回结果 存储起来,当同样的输入再次发生时,则返回缓存好的数据,以此提升运算效率。

  • 作用

**组件仅在它的 props 发生改变(即使 props 中的内容被重新创建但是没有改变,那么不会视为改变)的时候进行重新渲染。**通常来说,在组件树中 React 组件,只要有变化就会走一遍渲染流程。但是用了 React.memo()之后,我们可以仅仅让某些组件进行渲染。

  • 与 PureComponent 区别

PureComponent 只能用于 class 组件,memo 用于 functional 组件

  • 用法
jsx
import {memo} from 'react'

const Child = memo(()=>{
  return <div>
		<input type="text" />
	</div>
})

//或者
const Child = ()=>{
  return <div>
		<input type="text" />
	</div>
})
const MemoChild = memo(Child)

实例:

1.当结合 useCallbackReact.memo 使用时,可以进一步优化函数组件的性能。

下面是一个示例:

jsx
import React from "react";

const ChildComponent = React.memo(({ name, age, handleClick }) => {
  console.log("ChildComponent rendered");
  return (
    <div>
      <p>Name: {name}</p>
      <p>Age: {age}</p>
      <button onClick={handleClick}>Click me</button>
    </div>
  );
});

const ParentComponent = () => {
  const [name, setName] = React.useState("John");
  const [age, setAge] = React.useState(25);

  const handleClick = React.useCallback(() => {
    setName("Jane");
    setAge(30);
  }, [name, age]);

  return (
    <div>
      <ChildComponent name={name} age={age} handleClick={handleClick} />
    </div>
  );
};

在上面的例子中,ParentComponent 包含一个名字和年龄的状态,并通过 ChildComponent 子组件进行渲染。ChildComponent 使用了 React.memo 进行缓存,只有当组件的 props 发生变化时才会重新渲染(仅仅父组件重新渲染是不会触发子组件重新渲染的,避免不必要的重新渲染)。

ParentComponent 中的 handleClick 回调函数使用了 useCallback 进行缓存,确保在每次重新渲染时函数不会重新实例化。并且仅当 nameage 属性值发生变化时子组件才会重新渲染。

2.当结合 useCallbackshouldComponentUpdate 使用时,可以进一步优化类式组件的性能。

下面是一个示例:

jsx
import React from "react";

class ChildComponent extends React.Component {
  shouldComponentUpdate(nextProps) {
    if (
      this.props.name !== nextProps.name ||
      this.props.age !== nextProps.age
    ) {
      return true; //发生了变化就重新渲染
    }
    return false;
  }

  render() {
    const { name, age, handleClick } = this.props;
    console.log("ChildComponent rendered");
    return (
      <div>
        <p>Name: {name}</p>
        <p>Age: {age}</p>
        <button onClick={handleClick}>Click me</button>
      </div>
    );
  }
}

const ParentComponent = () => {
  const [name, setName] = React.useState("John");
  const [age, setAge] = React.useState(25);

  const handleClick = React.useCallback(() => {
    setName("Jane");
    setAge(30);
  }, []);

  return (
    <div>
      <ChildComponent name={name} age={age} handleClick={handleClick} />
    </div>
  );
};

在上面的例子中,ChildComponent 是一个类组件,它通过 shouldComponentUpdate 生命周期方法进行性能优化。在 shouldComponentUpdate 中,我们检查当前属性和下一个属性的值是否有变化,只有当属性值发生变化时才会返回 true,表示组件需要重新渲染。

ParentComponent 中的 handleClick 回调函数使用了 useCallback 进行缓存,确保在每次重新渲染时函数不会重新实例化。并且仅当 nameage 属性值发生变化时子组件才会重新渲染。

如果类组件使用 PureComponent 呢?——> 相当于 shouldComponentUpdate 的语法糖

PureComponent 是 React 提供的一个默认实现了 shouldComponentUpdate 方法的基类,用于优化类组件的渲染性能。PureComponent 会对组件的 propsstate 进行浅比较,只有在它们发生变化时才会触发重新渲染,否则会跳过渲染过程,从而提升性能。

jsx
class ChildComponent extends React.PureComponent {
  render() {
    const { name, age, onClick } = this.props;
    return (
      <div>
        <h2>Name: {name}</h2>
        <p>Age: {age}</p>
        <button onClick={onClick}>Click</button>
      </div>
    );
  }
}

通过继承 PureComponent,您无需手动编写 shouldComponentUpdate 方法,React 会自动处理组件的渲染优化,只在必要时进行重新渲染。这种方式更加简洁和方便。

9. useMemo(注意与 memo 区分)

useMemo 是 React 提供的一个 Hook,用于在函数组件中进行记忆化计算。它接收一个依赖数组和一个回调函数,并返回计算结果。useMemo 会在组件渲染过程中执行回调函数,并将计算结果缓存起来。只有当依赖数组中的值发生变化时,才会重新执行回调函数。

useMemo 的使用场景通常是在需要进行昂贵的计算或处理时,用于避免不必要的重复计算,从而提升性能。

下面是一个使用 useMemo 的示例:

jsx
import React, { useMemo } from "react";

const ExpensiveComponent = ({ data }) => {
  // 进行昂贵的计算
  const result = useMemo(() => {
    // 对 data 进行处理或计算
    // 这里只是做一个示例,可以根据实际需求进行具体的计算逻辑
    const processedData = data.map((item) => item * 2);
    // 返回计算结果
    return processedData;
  }, [data]); // 依赖数组为 data

  return (
    <div>
      <h2>Expensive Component</h2>
      <p>Result: {result}</p>
    </div>
  );
};

const ParentComponent = () => {
  const data = [1, 2, 3, 4, 5];

  return (
    <div>
      <h1>Parent Component</h1>
      <ExpensiveComponent data={data} />
    </div>
  );
};

在上述示例中,ExpensiveComponent 是一个昂贵的计算组件,它接收一个 data 属性,并对该属性进行处理或计算。使用 useMemo 可以避免在每次组件渲染时都重新执行昂贵的计算。只有当 data 发生变化时,才会重新执行回调函数,否则会直接使用之前缓存的计算结果。——> 和 useState、useRef 一样会将数据进行缓存

通过使用 useMemo,可以有效地优化组件的性能,避免不必要的重复计算,提升应用的响应速度。

实例:利用 useMemo 方法实现前端的过滤模糊搜索(根据名字或者地址)

jsx
const [list, setList] = useState(store.getState().list);

const [mytext, setMytext] = useState("");

//相当于定义了一个名为getList的 state 数据
const getList = useMemo(() => {
  //因为 name 和 address 可能有英文,所以用toUpperCase()转换为大写字母!
  list.filter(
    (item) =>
      item.name.toUpperCase().includes(mytext.toUpperCase()) ||
      item.address.toUpperCase().includes(mytext.toUpperCase())
  );
}, [list, mytext]); //监听list和mytext的变化

useEffect(() => {
  let unsubscribe = store.subscribe(() => {
    //订阅数据的变化
    setList(store.getState().list);
  });
  return () => {
    unsubscribe(); //取消订阅
  };
}, []);

return (
  <div>
    <input
      value={mytext}
      onChange={(evt) => {
        setMytext(evt.target.value);
      }}
    />
    {getList.map((item) => (
      <dl key={item.id}>
        <dt>{item.name}</dt>
        <dd>{item.address}</dd>
      </dl>
    ))}
  </div>
);

理解

  • useMemo 实际目的也是为了进行性能的优化

  • 相当于是一个通过昂贵的计算生成的 state 数据(而不是我们直接定义出来),并进行缓存

  • useMemo 和 memo()完全不同,完全是两种为了性能优化而设计的工具

useMemo 和 memo()结合使用

useMemomemo() 可以结合使用以进一步优化函数组件的性能。

useMemo 主要用于在函数组件内部进行记忆化计算,它可以缓存计算结果并在依赖项发生变化时重新计算。

memo() 是一个高阶组件(Higher-Order Component),用于包裹函数组件并提供浅层比较的组件更新优化。

当将 memo()useMemo 结合使用时,可以同时享受两者的性能优化。

下面是一个示例:

jsx
import React, { memo, useMemo } from "react";

const ExpensiveComponent = memo(({ data }) => {
  // 进行昂贵的计算
  const result = useMemo(() => {
    // 对 data 进行处理或计算
    // 这里只是做一个示例,可以根据实际需求进行具体的计算逻辑
    const processedData = data.map((item) => item * 2);
    // 返回计算结果
    return processedData;
  }, [data]); // 依赖数组为 data

  return (
    <div>
      <h2>Expensive Component</h2>
      <p>Result: {result}</p>
    </div>
  );
});

const ParentComponent = () => {
  const data = [1, 2, 3, 4, 5];

  return (
    <div>
      <h1>Parent Component</h1>
      <ExpensiveComponent data={data} />
    </div>
  );
};

在上述示例中,ExpensiveComponent 使用了 memo() 包裹,以确保只有当 data 发生变化时才会触发重新渲染。同时,使用了 useMemodata 进行记忆化计算,以避免不必要的重复计算。

10. useImperativeHandle

useImperativeHandle 是 React 提供的一个自定义 Hook,用于在函数组件中向父组件暴露特定的实例值或函数,以便父组件可以通过 ref 来访问和调用子组件的方法(可以进行非常细粒度的暴露)。——> 类似于 vue 中的暴露方法

它的语法如下:

jsx
useImperativeHandle(ref, createHandle, [deps]);
  • ref:用于接收子组件实例或函数的 ref 对象。
  • createHandle:一个回调函数,用于创建要暴露给父组件的实例值或函数。它返回一个对象,该对象包含要暴露给父组件的属性和方法。
  • deps(可选):一个依赖数组,用于控制何时更新 createHandle 的返回值。如果省略该参数,则每次组件重新渲染时都会更新。

下面是一个示例:

jsx
import React, { useRef, useImperativeHandle, forwardRef } from "react";

// 子组件
const ChildComponent = forwardRef((props, ref) => {
  const inputRef = useRef(); //这里获取实例对象是为了在useImperativeHandle中把 input 实例的内容给父组件

  // 定义要暴露给父组件的方法
  //父组件可以通过 ref 属性将一个 ref 对象传递给子组件,子组件接收到这个 ref 对象后,可以在 useImperativeHandle 中将需要暴露给父组件的实例或功能与该 ref 关联起来。这样在父组件中就可以通过 ref 得到子组件的实例或者功能了!
  useImperativeHandle(ref, () => ({
    //这个ref就是父组件传过来的 ref 仓库
    focusInput: () => {
      inputRef.current.focus(); //暴露子组件 input 实例的状态控制方法
    },
    getValue: () => {
      return inputRef.current.value; //暴露子组件 input 实例的值
    },
  }));

  return <input ref={inputRef} type='text' />;
});

// 父组件
const ParentComponent = () => {
  const childRef = useRef(); //得到的是上面子组件的 useImperativeHandle 暴露的方法focusInput和getValue(而不是实例对象)

  const handleClick = () => {
    childRef.current.focusInput();
    const value = childRef.current.getValue();
    console.log(value);
  };

  return (
    <div>
      <ChildComponent ref={childRef} />
      <button onClick={handleClick}>Focus Input</button>
    </div>
  );
};

在上述示例中,ChildComponent 使用 useImperativeHandle 来定义了 focusInputgetValue 两个方法,这些方法可以通过父组件的 ref 访问和调用。父组件中的按钮点击事件会调用子组件暴露的 focusInput 方法来使输入框获取焦点,并通过 getValue 方法获取输入框的值。

useImperativeHandle 可以帮助你在函数组件中与类组件类似地暴露方法和属性,使得父组件可以与子组件进行交互和通信。

理解

  • 在使用 ref 时是默认将整个组件进行暴露,这就导致其他组件拿到该组件后可以进行任意修改
  • useImperativeHandle()可以对暴露的进行限制 ——> 本质上是用来限制的而不是用来暴露的
  • useImperativeHandle 必须结合 forwardRef 来使用,以拿到父组件给过来的 ref 仓库

对于 ref 的进一步理解(重点)

当 ref 属性绑定到普通标签上时,获取的就是标签的 dom 实例

但是当 ref 属性绑定到子组件上时,父组件可以获取子组件的实例、或者子组件中具体的 dom 实例(通过 forwardRef)、或者子组件中想要暴露的特定方法或者实例(通过 useImperativeHandle+forwardRef)

所以说通过 useRef() 创建的 ref 对象本质上就是一个仓库,可以存储包括 dom 实例,组件实例,方法,属性在内的任意数据,绑定在组件上时主要取决于组件想给我们什么东西!

11. useLayoutEffect 和 react 生命周期常识

useLayoutEffect 与 useEffect 的区别

  1. 执行时机: useLayoutEffect 在 DOM 更新(或首次创建)完成后同步执行,但在浏览器进行下一次绘制之前执行(页面渲染之前),会阻塞 DOM 的更新;而 useEffect 则是在页面渲染完成后异步执行,不会阻塞 DOM 的更新。
  2. 性能开销: useLayoutEffect 的执行开销较高,因为它会在每次渲染时同步触发,可能会阻塞浏览器的渲染。而 useEffect 是异步执行的,不会阻塞渲染过程,性能开销较低。
  3. 使用场景: 由于 useLayoutEffect 在 DOM 更新后同步执行,适用于需要立即处理 DOM 布局结果的操作,例如测量元素尺寸或位置(为了获取准确的测量结果),进行 DOM 操作等。而 useEffect 适用于一般的副作用操作,如数据获取、订阅事件、网络请求等。
  4. 依赖关系: useLayoutEffectuseEffect 都接收一个依赖数组作为第二个参数,用于指定副作用函数的依赖项。当依赖项发生变化时,useLayoutEffectuseEffect 的行为是不同的。useLayoutEffect 会在每次渲染之后同步触发副作用函数,而 useEffect 则会在下一次渲染时异步触发副作用函数。

总的来说,这两个钩子函数主要的区别在于执行的时间点不同,而其他方面的用法和功能基本上是相似的。

时间线:dom 创建(虚拟+真实) ——> 【dom 更新(真实)】 ——> useLayoutEffect 同步执行 ——> 页面渲染 ——> useEffect 异步执行(四者按照顺序执行,不会重叠)

useLayoutEffect:页面渲染之前(存在 dom)

useEffect:页面渲染之后(存在 dom)

如果你需要在 DOM 更新后立即执行副作用操作,并且这些操作依赖于 DOM 布局结果,那么可以使用 useLayoutEffect

否则,如果副作用操作不需要同步执行,或者不依赖于 DOM 布局结果,那么可以使用 useEffect,它的性能开销较低。

useLayoutEffect 和 created(vue 中)

在 React 中,组件的生命周期分为两个阶段:mount 阶段和 update 阶段。useLayoutEffectmount 阶段的最后执行,在组件实例被创建并添加到 DOM 树之后立即执行。它会在浏览器绘制之后、页面渲染之前执行,因此可以获取到最新的 DOM 结构并进行相应的操作。——> useLayoutEffect 是 mount 的一部分(有 dom)

而 Vue 的 created 生命周期则是在组件实例被创建后立即执行,它并不涉及到浏览器的绘制和页面渲染。在 created 生命周期中,组件的 DOM 结构还未完全生成,因此不能进行与 DOM 相关的操作。——> created 在 mount 之前(无 dom)

created(vue 中)对应 react 的什么?

Vue 中的created 钩子函数被用于在组件实例创建后进行一些初始化操作,但此时组件尚未被渲染到 DOM。它类似于 React 类式组件中的 constructor 构造函数。

对于函数式组件(Functional Components)而言,在 React 中没有类似于 Vue 的 created 钩子函数的内置概念。

useEffect 和 mounted(vue 中)

useEffect 是 React 中用于处理副作用操作的钩子函数,它会在组件的渲染完成后执行,不仅在组件挂载阶段执行一次,还会在每次组件更新后执行(除非通过依赖项数组控制)。

而 Vue 的 mounted 生命周期只在组件首次挂载到 DOM 后执行一次,不会在组件更新时再次执行。它主要用于执行一次性的初始化操作或与 DOM 相关的操作。

因此,可以说 useEffect 在某种程度上扩展了 mounted 生命周期的功能,它不仅可以在组件挂载后执行一次,还可以根据需要在组件更新后执行。

useEffect 的执行时机和 mounted 一样,都是在组件的渲染完成后执行!

useEffect = mounted + updated

DOM 创建、DOM 更新和页面渲染

在 React 中,"DOM 更新"和"页面渲染"可以理解为两个不同的概念:

  1. DOM 创建: 当组件首次被渲染时,React 会生成对应的虚拟 DOM,并将其转化为真实的 DOM 元素插入到页面中。(虚拟和真实 DOM 的创建)

  2. DOM 更新: DOM 更新指的是当 React 组件状态或属性发生变化时,React 会根据新的数据生成对应的 Virtual DOM,并与旧的 Virtual DOM 进行比较,找出需要进行更新的部分,然后将这些更新应用到实际的 DOM 上。这个过程包括了计算 diff、更新节点、处理事件等操作,最终使得页面上的 DOM 元素与组件的状态和属性保持一致。(通过虚拟 DOM 实现 真实 DOM 的更新)

  3. 页面渲染: 页面渲染指的是将更新后的 DOM 结构展示在用户的浏览器中,呈现给用户看到的页面。这个过程包括了浏览器的渲染引擎解析 HTML、构建渲染树、进行样式计算、布局和绘制等步骤,最终在浏览器窗口中显示出页面的内容。(浏览器渲染 DOM)

在 React 的组件更新过程中,DOM 更新是在组件内部进行的,React 会根据组件的状态和属性变化,生成新的 Virtual DOM,并更新实际的 DOM 元素,使其保持与组件的状态和属性一致。而页面渲染则是由浏览器负责的,它将更新后的 DOM 结构解析和渲染,最终呈现给用户。

需要注意的是,DOM 更新是 React 框架内部的概念,而页面渲染是浏览器进行的过程。React 通过 Virtual DOM 和 diff 算法来优化 DOM 更新的效率,使得只有需要更新的部分才会被修改,从而减少页面渲染的开销,提高应用的性能。

注意:页面渲染 = 组件渲染到页面 = 组件渲染 = 组件挂载(vue 中)

DOM 创建、DOM 更新、页面渲染是 vue 和 react 所共有的

12.自定义 Hook

自定义 Hook 是一种在 React 中共享逻辑的方式。它允许你将一些常见的逻辑抽象为可重用的函数,并在函数组件中使用。通过自定义 Hook,你可以将组件之间的共享逻辑提取出来,提高代码的可维护性和复用性。

一个自定义 Hook 是一个以 "use" 开头的函数,它可以调用其他的 Hook。它可以访问 React 的内置 Hook,如 useState、useEffect、useContext 等,也可以访问其他自定义 Hook。

下面是一个简单的示例,演示如何创建一个自定义 Hook:

jsx
//封装了一个根据 url 发送 ajax 请求并返回数据的 Hook
import { useState, useEffect } from "react";

function useFetchData(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchData = async () => {
      try {
        const response = await fetch(url);
        const jsonData = await response.json();
        setData(jsonData); //给 data 赋值
        setLoading(false);
      } catch (error) {
        setError(error); //给 error 赋值
        setLoading(false);
      }
    };
    fetchData();
  }, [url]);

  return { data, loading, error };
}

export default useFetchData;

在上述示例中,我们创建了一个名为 useFetchData 的自定义 Hook。它接收一个 URL 参数,并使用 useStateuseEffect 来处理数据的获取过程。在 useEffect 中,我们发起了异步请求,获取数据,并根据请求的结果更新状态。最后,我们将数据、加载状态和错误信息作为返回值返回。

在组件中使用自定义 Hook 时,只需在函数组件中调用它即可:

jsx
import React from "react";
import useFetchData from "./useFetchData";

function MyComponent() {
  const { data, loading, error } = useFetchData("https://api.example.com/data");

  //根据不同状态进行不同的渲染
  if (loading) {
    return <div>Loading...</div>;
  }

  if (error) {
    return <div>Error: {error.message}</div>;
  }

  return (
    <div>
      {/* 使用获取的数据进行渲染 */}
      {data && <div>{data}</div>}
    </div>
  );
}

通过自定义 Hook,我们将数据获取逻辑封装到了一个可重用的函数中,并在需要的组件中使用它。这样,我们可以在多个组件中共享相同的数据获取逻辑,提高代码的可维护性和复用性。

需要注意的是,自定义 Hook 只是一种约定,React 并不会对以 "use" 开头的函数做特殊处理。因此,在编写自定义 Hook 时,确保遵循 Hook 的规则。

理解

自定义 Hook 本质上是普通的 JavaScript 函数,但它们具有特定的命名规范和用法约定。自定义 Hook 是为了在函数组件中重用状态逻辑和副作用逻辑而创建的函数。

自定义 Hook 可以包含内部的状态和副作用逻辑,并且可以在多个组件中共享使用。但需要注意的是,自定义 Hook 仍然必须遵循 React Hook 的规则。

React 自定义 Hook 规则要求:

  1. 只在 React 函数组件中使用 Hook。
  2. 在自定义 Hook 中可以使用其他内置 Hook 或其他自定义 Hook。
  3. 自定义 Hook 必须以 "use" 开头命名。
  4. 自定义 Hook 不应该在普通的 JavaScript 函数或非 React 组件中调用。

13.自定义 Hook 案例

自定义 LocalStorage Hook(暴露 set 方法)

实现的功能:通过直接操作 state 实现简化的本地存储操作,并且可以多组件共同使用一个存储值

1.通过useState生成 name 状态,每次都会自动从本地存储中取值,实现了多个组件共用一个本地存储的 key-value 值。

2.通过在 useEffect 中监听 name 的变化,并在变化时将其存储到本地,实现了自动存储到本地的功能。

具体过程:第一次渲染使用了 LocalStorage Hook 的组件的时候,会调用自定义 hook 中的 useState 获取初始值,react 内部进行数值的缓存。后续每当在组件中调用 setName 修改 name 的值时,会触发组件的重新渲染,这样就会重新调用 自定义 hook 函数,就会触发 hook 中的 useEffect ,将新的 name 值保存在本地存储中,更新了本地存储。

local-tore-hook.js

jsx
import { useState, useEffect } from "react";

function useLocalStorage(key) {
  //只会调用一次,后续直接把 state 缓存起来了
  const [name, setName] = useState(() => {
    const name = window.localStorage.getItem(key); //可能可以取到,因为可能有的组件已经在本地存储了
    return name ? JSON.parse(name) : ""; // 提供一个默认值,在第一次运行时,即使本地存储中没有对应的值,useState 仍会使用提供的默认值,避免了 JSON.parse 报错
    //const name = JSON.parse(window.localStorage.getItem(key));
    //return name;
  });

  //每当组件中使用 setName 方法修改值的时候都会调用,并把最新数据进行本地存储
  useEffect(() => {
    window.localStorage.setItem(key, JSON.stringify(name));
  }, [name]); //这里 useEffect 是一个监听并更新的功能,当 name 发生变化时,useEffect 中的副作用函数才会被触发(hook 首次被调用的时候也会触发)

  return [name, setName]; //set 方法暴露,让组件操作 hook 中的 state
}

export default useLocalStorage;

在组件中使用:

jsx
import React, { useState, useEffect } from "react";

import useLocalStorage from "../hooks/local-store-hook";

export default function CustomDataStoreHook() {
  const [name, setName] = useLocalStorage("name");

  return (
    <div>
      <h2>CustomDataStoreHook: {name}</h2>
      <button onClick={(e) => setName("TaoLoading")}>设置name</button>
    </div>
  );
}

setName 方法被嵌入到组件了吗?

setName 方法并没有被嵌入到组件中。它是在 useLocalStorage 自定义 Hook 中定义的一个状态更新函数。

当你在组件中使用 useLocalStorage 自定义 Hook,并解构出 namesetName,实际上是将 setName 函数引用返回给了组件。

在组件中调用 setName 函数时,它会触发对应的状态更新,并且会触发组件的重新渲染。

所以,setName 并没有被嵌入到组件中,而是作为一个函数引用通过自定义 Hook 返回给了组件,供组件在需要更新状态时调用。

自定义 hook 的理解:每个组件在调用的时候都产生独立的状态,以实现 hook 逻辑的复用

在 React 中,每个组件都拥有自己的状态。当在多个组件中使用同一个自定义 Hook,比如 useLocalStorage,每个组件都会独立地创建自己的状态。

这是因为每个函数组件在运行时都是独立的实例。当一个组件使用 useLocalStorage 自定义 Hook 时,React 会在组件的每次渲染中调用该 Hook。每次调用都会创建一个独立的状态,与其他组件实例完全隔离。

这种独立的状态机制使得每个组件能够独立管理自己的状态,并保证状态在组件之间的隔离和封装。通过自定义 Hook,可以方便地将共享的状态逻辑提取出来,并在多个组件中进行复用,同时保持每个组件的独立性。

自定义 Context Hook

实现的功能:快捷地获取多个 context 共享文件,获取 user 信息和 token 值

在 App.jsx 中传递顶层 context:

jsx
import React, { useState, createContext } from "react";
import CustomContextShareHook from "./11-自定义Hook/02-自定义Hook练习-Context共享";

export const UserContext = createContext(); //创建 context 对象,并 export 导出
export const TokenContext = createContext();

function App() {
  const [show, setShow] = useState(true);

  return (
    <div>
      <UserContext.Provider value={{ name: "TaoLoading", age: 18 }}>
        <TokenContext.Provider value='123456'>
          <CustomContextShareHook />
        </TokenContext.Provider>
      </UserContext.Provider>
    </div>
  );
}

export default App;

user-hook.js

jsx
import { useContext } from "react";
import { UserContext, TokenContext } from "../App";

function useUserContext() {
  const user = useContext(UserContext);
  const token = useContext(TokenContext);

  return [user, token];
}

export default useUserContext;

在组件中使用:

jsx
import React, { useContext } from "react";
import useUserContext from "../hooks/user-hook";

export default function CustomContextShareHook() {
  const [user, token] = useUserContext();
  console.log(user, token);
  return (
    <div>
      <h2>CustomContextShareHook</h2>
    </div>
  );
}

自定义 ScrollPosition Hook(不暴露 set 方法)

实现功能:

执行过程:

ScrollPosition Hook 首次被调用的时候,给整个页面注册了滚动事件,后面每当滚动的时候,回调函数就会自动调用 setScrollPosition 更新 state 值,然后触发页面重新渲染,重新调用 useScrollPosition 这个 hook,重新得到最新的 scrollPosition 值

scroll-position-hook.js

js
import { useState, useEffect } from "react";

function useScrollPosition() {
  const [scrollPosition, setScrollPosition] = useState(0);

  useEffect(() => {
    const handleScroll = () => {
      setScrollPosition(window.scrollY); //当滚动时更新scrollPosition的值为当前的 Y 坐标值
    };
    document.addEventListener("scroll", handleScroll); //设置动作监听和回调函数(直接给整个页面对象设置)——> 不需要将 set 方法返回给组件,因为已经被设置为监听回调函数了!

    return () => {
      //自动在组件卸载时调用
      document.removeEventListener("scroll", handleScroll);
    };
  }, []); //依赖数组为空 [],因此useEffect只会在调用 Hook 时执行一次,而不会在其他时刻触发(由于没有依赖,所以只会在组件首次调用 Hook 时执行这个useEffect)

  return scrollPosition; //set 方法没有暴露,通过事件监听在自己调用 set 方法
}

export default useScrollPosition;

在组件中使用:

jsx
import React, { useEffect, useState } from "react";
import useScrollPosition from "../hooks/scroll-position-hook";

export default function CustomScrollPositionHook() {
  const position = useScrollPosition();

  return (
    <div style={{ padding: "1000px 0" }}>
      <h2 style={{ position: "fixed", left: 0, top: 0 }}>
        CustomScrollPositionHook: {position}
      </h2>
    </div>
  );
}

理解:setScrollPosition 方法没有暴露的情况下,如何实现重新渲染的?

由于设置了事件监听,每当页面滚动的时候,回调函数就会自动调用 setScrollPosition 方法。

setScrollPosition 方法是在 hook 内部调用的,当状态发生变化时,会触发组件的重新渲染。

scrollPosition 是在 useScrollPosition 这个自定义 Hook 内部定义的状态。虽然状态是在 Hook 内部管理的,但是当状态发生变化时,会触发组件的重新渲染。

为什么会这样呢?

因为 React 会追踪组件函数体中使用到的状态(包括来自 Hook 的状态),并在状态变化时自动更新组件。所以,当 scrollPosition 发生变化时,React 会重新执行组件函数体,生成新的 UI,并将其渲染到页面上。

组件和 hook 中的 useEffect 执行时机

组件和 hook 中的 useEffect 执行时机是一致的,但是组件以渲染为基准,hook 以调用为基准

如果依赖为空数组[],那么就只会在首次渲染(调用)时候执行 useEffect

自定义 hook 的 useEffect 被嵌入到组件了吗?

在使用自定义 Hook 的组件中,实际上并没有将自定义 Hook 中的 useEffect 直接嵌入到组件中。

当你在组件中使用自定义 Hook,例如在 useScrollPosition 中使用了 useEffect,该 useEffect 只是在组件渲染过程中被调用,并不会直接嵌入到页面中。

4. Fragment

Fragment 是 React 提供的一种组件包装方式,用于在不添加额外 DOM 元素的情况下,将多个子元素组合在一起。它是一个无需渲染的包装器,类似于一个占位符。

还可以使用 React 的简写语法来表示 Fragment,即使用空标签 <></>

使用

jsx
<Fragment><Fragment>
<></>

作用

可以不用必须有一个真实的 DOM 根标签了

5. Context

Context 是 React 中用于在组件树中共享数据的一种机制(可以称作“共享文件”)。它可以让你在组件之间传递数据,而不需要通过逐层传递 props。

使用 Context,你可以在父组件中创建一个 Context 对象,并通过该对象向下传递数据给所有子组件。子组件可以通过订阅 Context 来获取该数据,并在需要时使用它。

Context 主要包含以下几个部分概念:

  1. 创建 Context 对象:使用 React.createContext() 来创建一个 Context 对象。例如:const MyContext = React.createContext();

  2. 提供器(Provider)组件:在父组件中使用 MyContext.Provider 组件来提供需要共享的数据。通过 value 属性传递数据给子组件。例如:

    jsx
    <MyContext.Provider value={data}>{/* 子组件 */}</MyContext.Provider>
  3. 消费者(Consumer)组件:在子组件中使用 MyContext.Consumer 组件来订阅 Context 数据。通过函数作为子组件(render prop)的方式,接收 Context 数据并进行使用。例如:

    jsx
    <MyContext.Consumer>
      {(value) => (
        // 使用 value 进行操作
      )}
    </MyContext.Consumer>

    或者,你可以使用 useContext 钩子来在函数式组件中订阅 Context 数据。

    jsx
    const value = useContext(MyContext);
    // 使用 value 进行操作

通过使用 Context,你可以在组件之间共享数据,而不需要一级一级地传递 props。它非常适用于在应用程序中传递全局配置、用户认证状态、主题样式等常见的共享数据。

需要注意的是,Context 不应滥用,应该谨慎使用。过多的使用 Context 可能会导致组件之间的耦合性增加,使代码难以理解和维护。在大多数情况下,最好的做法是将共享的数据和逻辑提取到单独的容器组件或状态管理工具(如 Redux)中。

理解

一种组件间通信方式, 常用于【祖组件】与【后代组件】间通信

使用

jsx
1) 创建Context容器对象:
	const xxxContext = React.createContext();

2) 渲染子组时,外面包裹xxxContext.Provider, 通过value属性给后代组件传递数据:
	<xxxContext.Provider value={数据}>
		子组件
  </xxxContext.Provider>
	// 示例:
  // 祖组件
	const MyContext = React.createContext();
  function ParentComponent() {
    const contextValue = "Hello, Context!";

    return (
      <MyContext.Provider value={contextValue}>
        <FunctionComponent />
        <ClassComponent />
      </MyContext.Provider>
    );
  }

3) 后代组件读取数据:

	//第一种方式: 仅适用于类组件
	static contextType = xxxContext  // 声明接收context(必须定义为整个类的属性,静态的)
	this.context // 读取context中的value数据
	//示例:
	// 类式组件
  class MyClassComponent extends React.Component {
    static contextType = MyContext;

    componentDidMount() {
      const contextValue = this.context;
      // 使用 contextValue 进行操作
    }

    render() {
      const contextValue = this.context;
      // 渲染组件
    }
  }



	//第二种方式: 函数组件与类组件都可以
	  <xxxContext.Consumer>
	    {
	      value => ( // value就是context中的value数据
	        要显示的内容
	      )
	    }
	  </xxxContext.Consumer>
	//示例:
  // 类式组件
  class ClassComponent extends React.Component {
    render() {
      return (
        <MyContext.Consumer>
          {(contextValue) => <p>{contextValue}</p>}
        </MyContext.Consumer>
      );
    }
  }

注意

在应用开发中一般不用 context, 一般都用它的封装 react 插件

例如:Redux、MobX、Zustand、Recoil

将 context 对象放置在一个单独的文件的策略

React.createContext() 通常在组件外部的顶层进行调用,以创建一个 Context 对象。

你可以将其放置在需要使用该 Context 的组件文件的顶部,或者将其放置在一个单独的文件中,供整个应用程序使用。

以下是一种常见的做法,将 createContext() 放置在单独的文件中:

MyContext.js

jsx
import React from "react";

const MyContext = React.createContext();

export default MyContext;

然后,在需要使用该 Context 的组件文件中,可以导入该 Context:

Component.js

jsx
import React from 'react';
import MyContext from './MyContext';

function Component() {
  const contextValue = React.useContext(MyContext);

  // 组件逻辑

  return (
    // JSX
  );
}

export default Component;

context 的传递使用没有组件的限制

可以是任意类型的组件之间,都是类式组件,都是函数式组件,或者类式和函数式混用!

6. 组件优化 shouldComponentUpdate

Component 的 2 个问题

  1. 只要执行 setState(),即使不改变状态数据,组件也会重新 render() ==> 效率低

  2. 只要当前组件重新 render(),就会自动重新 render 子组件(因为 props 会重新传递),纵使子组件没有用到父组件的任何数据 ==> 效率低

效率高的做法

只有当组件的 state 或 props 数据发生改变时才重新 render()

原因

Component 中的 shouldComponentUpdate()总是返回 true

解决

我们可以监听特定数据的变化,如果没变就不需要重新渲染(加了一层屏障)

办法1:
	重写shouldComponentUpdate()方法
	比较新旧state或props数据, 如果有变化才返回true, 如果没有返回false
办法2:
	使用PureComponent
	PureComponent重写了shouldComponentUpdate(), 只有state或props数据有变化才返回true
	注意:
		只是进行state和props数据的浅比较, 如果只是数据对象内部数据变了, 返回false
		不要直接修改state数据, 而是要产生新数据
项目中一般使用PureComponent来优化

理解:useCallback 优化函数,memo/shouldComponentUpdate(PureComponent)优化属性

7. render props

Render Props 是一种在 React 中共享代码逻辑的技术。它通过将一个函数作为组件的 prop 传递,并在组件内部调用该函数来共享逻辑。这个函数接收组件的内部状态或其他数据作为参数,并返回一个 React 元素或组件,以实现动态渲染。——> 相当于子到父的通信,只不过是直接通过子的数据构造并返回页面了(父组件直接使用子组件的数据,不用再去用自定义事件触发),而不是接收子的数据再去构造页面

使用 Render Props,可以将通用的逻辑封装在一个函数中,并将该函数作为 prop 传递给其他组件,使得这些组件能够共享这个逻辑并使用它来渲染内容。这种模式使得组件之间的逻辑复用更加灵活。

以下是一个示例,演示了 Render Props 的使用:

jsx
import React from "react";

// 定义一个使用 Render Props 的组件 ——> 子组件
class MouseTracker extends React.Component {
  // 组件内部状态
  state = { x: 0, y: 0 };

  // 更新鼠标位置的方法
  handleMouseMove = (event) => {
    this.setState({ x: event.clientX, y: event.clientY });
  };

  render() {
    return (
      <div style={{ height: "100vh" }} onMouseMove={this.handleMouseMove}>
        {/* 将 Render Props 函数作为 prop 传递给模版,动态生成内容(相当于动态插槽) */}
        {/*
          即动态的、可被赋值操作的:下面这个结构
          <div>
              <p>Mouse position: {mouse.x}, {mouse.y}</p>
          </div>
        */}
        {this.props.render(this.state)}{" "}
        {/*相当于子到父的通信,只不过是直接通过子的数据构造并返回页面了,而不是接收子的数据再去构造页面*/}
      </div>
    );
  }
}

// 使用 MouseTracker 组件,并传递 Render Props 函数 ——> 父组件
function App() {
  return (
    <div>
      <h1>Mouse Tracker</h1>
      <MouseTracker
        render={(
          mouse //这里直接用子传递的数据,构造页面
        ) => (
          <div>
            <p>
              Mouse position: {mouse.x}, {mouse.y}
            </p>
          </div>
        )}
      />
    </div>
  );
}

在上述示例中,我们定义了一个 MouseTracker 组件,它追踪鼠标的位置并将位置信息存储在组件的内部状态中。通过将一个 Render Props 函数作为 render prop 传递给 MouseTracker 组件,我们可以在 Render Props 函数中使用鼠标位置信息,并返回需要渲染的内容。

Render Props 提供了一种灵活的方式来实现组件之间的逻辑复用,使得代码更加可维护和可扩展。它在许多 React 库和组件中被广泛使用,如路由库 react-router 的 <Route> 组件就使用了 Render Props 模式来渲染匹配的路由。

如何向组件内部动态传入带内容的结构(标签)?

Vue中:
	使用slot技术, 也就是通过组件标签体传入结构  <A><B/></A>
React中:
	使用children props: 通过组件标签体传入结构
	使用render props: 通过组件标签属性传入结构,而且可以携带数据,一般用render函数属性
	这两个属性是每个组件都有的,他们的功能就类似于插槽

children props ——> 简单的传递内容的插槽

//这时我们在 C 组件,A 组件为子组件(给里面传入 B 组件)
<A>
  <B>xxxx</B>
</A>
A 组件:{this.props.children} //在 A 组件中取到 B 组件
B 组件:什么也干不了

问题: 如果B组件需要A组件内的数据, ==> 做不到

render props ——> 可以根据参数构建内容的插槽

//这时我们在 C 组件,A 组件为子组件(给里面传入 B 组件)
<A render={(data) => <B data={data}></B>}></A>
A组件: {this.props.render(内部state数据)} //调用 C 组件给传入的方法,给 B 组件传入自身的state,构建并取到 B 组件
B组件: 读取A组件传入的数据显示 {this.props.data} //使用 A 组件的数据进行一些操作

8. 错误边界

在 React 中,getDerivedStateFromErrorcomponentDidCatch 是用于处理错误的边界方法。它们一起使用可以创建错误边界(Error Boundary),用于捕获和处理子组件中发生的错误

以下是它们的作用和用法:

  1. static getDerivedStateFromError(error)

getDerivedStateFromError 是一个静态方法,用于在子组件抛出错误后捕获并返回一个新的状态对象。它可以用来更新组件的状态,从而显示错误信息或触发其他行为。这个方法应该返回一个状态对象,或者返回 null 以表示不需要更新状态。

jsx
class ErrorBoundary extends React.Component {
  state = { hasError: false, error: null };

  static getDerivedStateFromError(error) {
    return { hasError: true, error };
  }

  render() {
    if (this.state.hasError) {
      // 在错误边界中渲染错误信息
      return <div>Error: {this.state.error.message}</div>;
    }

    return this.props.children;
  }
}

在上述示例中,我们定义了一个错误边界组件 ErrorBoundary,它通过 getDerivedStateFromError 方法捕获子组件抛出的错误,并将错误信息存储在组件的状态中。如果发生错误,ErrorBoundary 组件会渲染错误信息,否则会渲染子组件。

  1. componentDidCatch(error, info)

componentDidCatch 是一个实例方法,用于在子组件抛出错误后执行一些额外的处理逻辑。它接收两个参数:error 表示捕获到的错误对象,info 是一个包含错误堆栈信息的对象。

jsx
class ErrorBoundary extends React.Component {
  state = { hasError: false, error: null };

  static getDerivedStateFromError(error) {
    //会自动接收error
    return { hasError: true, error };
  }

  componentDidCatch(error, info) {
    //会自动接收error和info
    // 在控制台输出错误信息和堆栈信息
    console.log("Error:", error);
    console.log("Info:", info);
    // 可以在这个方法中执行一些错误处理、日志记录或发送错误报告等操作
  }

  render() {
    if (this.state.hasError) {
      return <div>Error: {this.state.error.message}</div>;
    }

    return this.props.children;
  }
}

在上述示例中,我们在 componentDidCatch 方法中使用 console.log 输出捕获到的错误信息和堆栈信息。你可以根据实际需求,在这个方法中执行一些错误处理、日志记录或发送错误报告等操作。

通过结合使用 getDerivedStateFromErrorcomponentDidCatch,我们可以创建错误边界组件来保护应用程序的其他部分免受子组件错误的影响。错误边界能够捕获子组件中的错误,并提供自定义的错误处理机制,从而提高应用程序的健壮性和用户体验。

理解:

错误边界(Error boundary):用来捕获后代组件错误,渲染出备用页面

一般将错误边界组件套在所有组件的最外面,如果出错了,就渲染出错页面并输出保存错误信息!

特点:

只能捕获后代组件生命周期产生的错误,不能捕获自己组件产生的错误和其他组件在合成事件、定时器中产生的错误。

使用方式:

getDerivedStateFromError 配合 componentDidCatch,直接写在类式组件里面即可

js
// 生命周期函数,一旦后台组件报错,就会触发
static getDerivedStateFromError(error) {
    console.log(error);
    // 在render之前触发
    // 返回新的state
    return {
        hasError: true,
    };
}

componentDidCatch(error, info) {
    // 统计页面的错误。发送请求发送到后台去
    console.log(error, info);
}

在函数式组件中使用两个错误边界方法

本质上来说,getDerivedStateFromError 和 componentDidCatch 这两个方法最初是为类组件设计的,但如果使用相应的技术,也可以在函数式组件中达到类似的效果。

对于函数式组件,可以使用 useErrorBoundary 自定义 Hook 或第三方库(如 react-error-boundary)来实现错误边界,并提供类似的功能。这些库会封装底层逻辑,使得在函数式组件中也能处理错误。

jsx
import { useErrorBoundary } from "your-error-boundary-library";

function MyComponent() {
  const { ErrorBoundary, catchError } = useErrorBoundary();

  return (
    <ErrorBoundary>
      <SomeComponentWithErrorHandling catchError={catchError} />
    </ErrorBoundary>
  );
}

通过这种方式,你可以在函数式组件中使用类似 getDerivedStateFromErrorcomponentDidCatch 的功能,并处理子组件中的错误。

9. 组件通信方式总结

组件间的关系:

  • 父子组件
  • 兄弟组件(非嵌套组件)
  • 祖孙组件(跨级组件)

几种通信方式:

	1.props:
		(1).直接传递属性、方法
		(2).children props
		(3).render props
	2.消息订阅-发布:
		pubs-sub、event等等
	3.集中式管理:
		redux、dva(Dva 是一个基于 React 和 Redux 的前端框架,简化 redux)等等
	4.conText:
		生产者-消费者模式
  5.ref对象:
    直接获取全部组件内容,或者选择性地暴露内容

比较好的搭配方式:

	父子组件:props、ref对象
	兄弟组件(任意组件):消息订阅-发布、集中式管理
	祖孙组件(跨级组件):消息订阅-发布、集中式管理、conText(开发用的少,封装插件用的多)

10.高阶组件

高阶组件(Higher-Order Component,HOC)是一个在 React 中用于复用组件逻辑的高级技术。

在 React 中,一个高阶组件是一个函数,它接受一个组件作为参数,并返回一个新的组件。这个新的组件包装了原始组件,可以添加一些额外的功能或逻辑。高阶组件通过组合组件和函数的方式,提供了一种灵活的方式来共享代码、抽象共同的逻辑,以及实现横切关注点(cross-cutting concerns)。

高阶组件可以用于以下场景:

  1. 代码复用:通过将共享的逻辑抽象成高阶组件,可以在多个组件中复用该逻辑,避免代码重复。
  2. 功能增强:高阶组件可以在原始组件的基础上添加额外的功能,例如处理表单验证、处理身份验证、添加动画效果等。
  3. 条件渲染:高阶组件可以根据特定条件选择性地渲染组件,例如根据用户登录状态渲染不同的界面。

使用高阶组件的步骤如下:

  1. 创建一个函数,接受一个组件作为参数。
  2. 在函数内部,创建一个新的组件,并在该组件中添加所需的逻辑或功能。
  3. 返回新的组件作为高阶组件的结果。
  4. 在其他组件中使用高阶组件包装原始组件,以获得增强后的组件。

需要注意的是,高阶组件不是 React 的官方概念,而是一种常见的设计模式。它是基于函数式编程的概念,利用函数的组合和闭包特性来实现代码复用和增强功能的目的。在 React 社区中,高阶组件已经成为一种常见的模式,可以在许多第三方库和框架中找到它的应用。

使用示例

jsx
import React, { PureComponent } from "react";

class App extends PureComponent {
  render() {
    return <div>App: {this.props.name}</div>;
  }
}

// 类式
function enhanceComponent(WrappedComponent) {
  class NewComponent extends PureComponent {
    render() {
      return <WrappedComponent {...this.props} />;
    }
  }
  // 组件重命名
  NewComponent.displayName = "newName";
  return NewComponent;
}
const EnhanceComponent = enhanceComponent(App); //这个EnhanceComponent就是NewComponent

// 函数式
function enhanceComponent2(WrappedComponent) {
  //WrappedComponent为 App
  function NewComponent(props) {
    return <WrappedComponent {...props} />; //NewComponent就是封装了的App
  }
  // 组件重命名
  NewComponent.displayName = "newName";
  return NewComponent;
}

const EnhanceComponent2 = enhanceComponent2(App); //这个EnhanceComponent2就是NewComponent,所以本质上EnhanceComponent2就是封装了的 App

export default EnhanceComponent;

要使用这个高阶组件,你可以将它应用于你想增强的组件,例如:

jsx
const EnhancedComponent = enhanceComponent(OriginalComponent);
const EnhancedComponent2 = enhanceComponent2(OriginalComponent);

注意:displayName 属性

displayName属性用于在开发工具中标识组件的名称。它对于调试和开发工具非常有用,因为它可以在组件层次结构中准确地显示组件的名称。

当你在开发工具中查看组件层次结构或进行性能分析时,会看到组件的名称。默认情况下,函数式组件的名称是函数名或"Anonymous"。通过设置displayName属性,你可以为组件提供一个更有意义的名称,以便在开发过程中更容易地识别和调试。

在上面的代码中:

NewComponent 被赋予了displayName属性为'newName',因此NewComponent被重命名为'newName'

EnhancedComponent就是被重命名为'newName'NewComponent,而不是原始的MyComponent

具体应用 1:增强 props

jsx
import React, { PureComponent } from "react";

/**
 * 需求分析:
 * 向Home和About两个子组件中插入“区域:中国”的文字
 * 并且为防止多次传值的复杂,不使用在App组件中传递props的方式
 *
 * 实现方式:
 * 在高阶组件中增强props,进行传值
 */

// 1.定义高阶组件
function enhanceRegionProps(WrappedComponent) {
  return (props) => {
    // 一样的属性:统一在我们的高阶组件内传递
    return <WrappedComponent {...props} region='中国' />;
  };
}

class Home extends PureComponent {
  render() {
    return (
      <h2>
        Home:{" "}
        {`昵称: ${this.props.nickname} 等级: ${this.props.level} 区域: ${this.props.region}`}
      </h2>
    );
  }
}
class About extends PureComponent {
  render() {
    return (
      <h2>
        About:{" "}
        {`昵称: ${this.props.nickname} 等级: ${this.props.level} 区域: ${this.props.region}`}
      </h2>
    );
  }
}

// 2.使用高阶组件处理子组件
const EnhanceHome = enhanceRegionProps(Home);
const EnhanceAbout = enhanceRegionProps(About);

class App extends PureComponent {
  render() {
    return (
      <div>
        App
        {/*不一样的属性在这里传递*/}
        <EnhanceHome nickname='LHT' level={90} />
        <EnhanceAbout nickname='HT' level={99} />
      </div>
    );
  }
}

export default App;

具体应用 2:增强 context

jsx
import React, { PureComponent, createContext } from "react";

/**
 * 需求分析:
 * 当使用createContext时
 * 每个子组件都要在<UserContext.Consumer>编写类似的代码,使用高阶组件实现代码的复用
 *
 * 实现方式:
 * 使用高阶函数返回<UserContext.Consumer>代码,并对其中的传值做处理以提高组件使用灵活性 ——> 这样就不用在每个子组件外部都写<UserContext.Consumer>代码
 *
 * 使用高阶组件复用的代码(即子组件中的<UserContext.Consumer>):
 * <UserContext.Consumer>
 *   {
 *     user => {
 *       return <h2>Home: {`昵称: ${user.nickname} 等级: ${user.level} 区域: ${user.region}`}</h2>
 *     }
 *   }
 * </UserContext.Consumer>
 */

// 1.创建Context
const UserContext = createContext({
  nickname: "默认",
  level: -1,
  区域: "中国",
});

// 2.创建子组件
class Home extends PureComponent {
  render() {
    return (
      <h2>
        Home:{" "}
        {`昵称: ${this.props.nickname} 等级: ${this.props.level} 区域: ${this.props.region}`}
      </h2>
    );
  }
}
class About extends PureComponent {
  render() {
    return (
      <h2>
        About:{" "}
        {`昵称: ${this.props.nickname} 等级: ${this.props.level} 区域: ${this.props.region}`}
      </h2>
    );
  }
}
class Detail extends PureComponent {
  render() {
    return (
      <ul>
        <li>{this.props.nickname}</li>
        <li>{this.props.level}</li>
        <li>{this.props.region}</li>
      </ul>
    );
  }
}

// 3.创建高阶组件
function withUser(WrappedComponent) {
  return (props) => {
    return (
      //统一接收
      <UserContext.Consumer>
        {(user) => {
          // 传递了所有接受到的props和后添加的user
          return <WrappedComponent {...props} {...user} />;
        }}
      </UserContext.Consumer>
    );
  };
}

// 4.使用高阶组件处理子组件
const UserHome = withUser(Home);
const UserAbout = withUser(About);
const UserDetail = withUser(Detail);

class App extends PureComponent {
  render() {
    return (
      <div>
        {/*统一传递*/}
        App
        <UserContext.Provider
          value={{ nickname: "why", level: 90, region: "中国" }}>
          <UserHome />
          <UserAbout />
          <UserDetail />
        </UserContext.Provider>
      </div>
    );
  }
}

export default App;

具体应用 3:鉴权(是否登录)——> 路由拦截器的原理

jsx
import React, { PureComponent } from 'react'

class LoginPage extends PureComponent {
  render() {
    return <h2>LoginPage</h2>
  }
}

// 负责鉴权操作的高阶组件
function withAuth(WrappedComponent) {
  const NewCpn = props => {
    const { isLogin } = props;
    if (isLogin) {
      return <WrappedComponent {...props} /> {/*登录了就返回想去的那个页面*/}
    } else {
      return <LoginPage /> {/*没登录就返回登录页*/}
    }
  }

  NewCpn.displayName = 'AuthCpn'
  return NewCpn
}

class CartPage extends PureComponent {
  render() {
    return <h2>CartPage</h2>
  }
}
class ProfilePage extends PureComponent {
  render() {
    return <h2>ProfilePage</h2>
  }
}

// 将需要鉴权的组件使用高阶组件进行处理
const AuthCartPage = withAuth(CartPage)
const AuthProfilePage = withAuth(ProfilePage)

export default class App extends PureComponent {
  render() {
    return (
      <div>
        <AuthCartPage isLogin={true} />
        <AuthProfilePage isLogin={false} />
      </div>
    )
  }
}

11.styled-components

styled-components 是一个流行的用于在 React 应用中创建样式化组件的库。它允许您以一种类似于编写普通 CSS 的方式来创建和管理组件的样式。

以下是使用 styled-components 的基本示例:

  1. 首先,确保已安装 styled-components 包:
shell
npm install styled-components
  1. 在您的代码中导入所需的库:
jsx
import React from "react";
import styled from "styled-components";
  1. 使用 styled-components 创建样式化组件:
stylus
// 创建一个样式化的标题组件
const Title = styled.h1`
  color: #333;
  font-size: 24px;
  font-weight: bold;
`;

// 创建一个样式化的按钮组件
const Button = styled.button`
  background-color: #e91e63;
  color: #fff;
  padding: 8px 16px;
  border: none;
  border-radius: 4px;
  cursor: pointer;
`;

const StyleApp = styled.div`
  background:yellow;
  border:1px solid black;
  ul{
    li{
      color:red;
    }
  }
  &:hover{
    background:pink
	}
`

// 在组件中使用样式化组件
function App() {
  return (
		<StyleApp>
      <Title>Hello, World!</Title>
      <Button>Click Me</Button>
			<ul>
				<li>1111</li>
				<li>22222</li>
			</ul>
		</StyleApp>
  );
}

在上面的示例中,我们使用 styled-components 创建了一个名为 Title 的样式化标题组件和一个名为 Button 的样式化按钮组件。我们可以像使用普通的 HTML 元素一样在组件中使用它们。

styled-components 的语法类似于模板字面量,其中的 CSS 样式规则被写入反引号内,并通过标签模板的方式传递给 styled 函数。您可以在样式规则中使用普通的 CSS 属性和值,它们将被自动注入到对应的组件中。

使用 styled-components,您可以轻松地创建具有可重用样式的组件,并在应用中以一种直观且可维护的方式管理样式。此外,styled-components 还支持动态样式、样式继承和全局样式等功能,让您能够更加灵活地处理样式化组件的需求。

理解:

它是通过JavaScript改变CSS编写方式的解决方案之一,从根本上解决常规CSS编写的一些弊端。

通过JavaScript来为CSS赋能,我们能达到常规CSS所不好处理的逻辑复杂、函数方法、复用、避免干扰。样式书写将直接依附在JSX上面,HTMLCSSJS三者再次内聚。all in js的思想。

利用 styled-components 可以方便地在 js 里面写 css(使用 css 的语法在 js 中写 css,否则需要.style.样式这样去写),利用 js 中的数据(外部给我们组件传递的 props),实现可复用的动态 css 样式组件!

语法:通过'styled-components'使用 js 语法写 css,返回一个 react 组件,组件的标签为'styled.标签名'中规定的标签

其余技巧

1.ThemeProvider

jsx
export default class App extends PureComponent {
  render() {
    return (
      // styled-components中的ThemeProvider实现了createContext一系列功能,并自动将传入的参数进行共享到其引用的后代组件
      <ThemeProvider theme={{ themeColor: "red", fontSize: "30px" }}>
        <Home />
        <hr />
      </ThemeProvider>
    );
  }
}
jsx
export default class Home extends PureComponent {
  render() {
    return (
      <HomeWrapper>
        <TitleWrapper>我是home的标题</TitleWrapper>
        <div className='banner'>
          <span>轮播图1</span>
          <span className='active'>轮播图2</span>
          <span>轮播图3</span>
        </div>
      </HomeWrapper>
    );
  }
}

文件:style.js

stylus
import styled from 'styled-components'

//可以使用 scss 语法!
export const HomeWrapper = styled.div`
  font-size: 12px;
  color: red;


  .banner {
    background-color: blue;

    span {
      color: #fff;

      /* '&'表示span */
      &.active {
        color: red;
      }

      &:hover {
        color: green;
      }

      &::after {
        content: "aaa"
      }
    }

    /* .active {
      color: #f00;
    } */
  }
`

//接收ThemeProvider的props
export const TitleWrapper = styled.h2`
  text-decoration: underline;
  color: ${props => props.theme.themeColor};
  font-size: ${props => props.theme.fontSize};
`

2.样式继承(扩展样式)

stylus
const HYButton = styled.button`
  padding: 10px 20px;
  border-color: red;
  color: red;
  `

// styled-components支持样式继承,这里HYPrimaryButton继承了HYButton,样式和标签都继承了
const HYPrimaryButton = styled(HYButton)`
  color: #fff;
  background-color: green;
	`

3.attrs 与穿透 props

可以在样式模版中获取传递给标签的 props 或者我们自己在 attrs 中定义的内容!

stylus
// 可以在attrs()中定义属性,传入一个对象,其返回值也是一个函数
const HYInput = styled.input.attrs({
  placeholder: "请输入",
  bColor: "red"
})`
  background-color: lightblue;
  /* 拿到attrs()中定义的bColor */
  border-color: ${props => props.bColor};
  /* 拿到props 给的color*/
  color: ${props => props.color};
`

export default class Profile extends PureComponent {
  constructor(props) {
    super(props);

    this.state = {
      color: 'purple'
    }
  }

  render() {
    return (
      <div>
        <input type="password" />
        {/*传递 props 给自定义标签组件*/}
        <HYInput type="password" color={this.state.color} />
        <h2>我是Profile的标题</h2>
        <ul>
          <li>设置列表1</li>
          <li>设置列表2</li>
          <li>设置列表3</li>
        </ul>
      </div>
    )
  }
}

4.样式化任一组件

styled 其实本质上就是一个高阶组件,把我们的普通组件包裹为一个有样式的组件

注意:一定要写 className={props.className},因为 styled 高阶组件包装的原理就是把样式封装为 class 类,并作为 props 的 className 传入被包装组件。

stylus
const Child = (props)=><div className={props.className}>child</div>

const StyledChild = styled(Child)`
	background:red;
`

<StyledChild/>

5.设置 keyframes 动画模版

stylus
import styled, {keyframes} from 'styled-components' //引入keyframes方法
const rotate360 = keyframes`
  from {
  transform: rotate(0deg);
  }
  to {
  transform: rotate(360deg);
  }
`;

const Rotate = styled.div`
  width:100px;
  height:100px;
  background:yellow;
  animation:${rotate360} 1s linear infinite; //应用到animation属性中
`

更好的方案:Emotion

Emotion 和 styled-components 的区别:

Emotion 和 styled-components 都是用于在 React 应用程序中处理样式的流行库,但它们在实现和使用上有一些区别。

  1. 语法和写法:Emotion 使用 CSS-in-JS 的方式,允许你在 JavaScript 中编写 CSS 样式,使用 CSS 语法或 JavaScript 对象来定义样式。而 styled-components 使用模板字面量语法,允许你在 JavaScript 代码中编写类似 CSS 的样式,通过创建组件模板字面量来定义样式。
  2. Class 名称生成:Emotion 自动为样式生成唯一的类名,并通过类名将样式应用于组件。styled-components 使用内联样式的方式,将样式直接嵌入到组件的style属性中。
  3. 嵌套选择器支持:Emotion 原生支持嵌套选择器,可以在样式中使用嵌套的 CSS 选择器。styled-components 则不支持嵌套选择器,但提供了类似于 BEM(块元素修饰符)的命名约定来管理样式。
  4. 插件和工具生态系统:Emotion 具有较大的插件生态系统,提供了许多插件和工具来增强样式的功能和开发体验。styled-components 也有一些插件和工具可用,但相对较少。
  5. 性能:在性能方面,Emotion 使用了一些优化技术,例如提供了样式提取和哈希缓存,以减少样式重复生成的开销。styled-components 也具有类似的优化措施,并且在某些情况下可以更好地利用浏览器的样式缓存。

选择使用 Emotion 还是 styled-components 取决于个人偏好和项目需求。它们都提供了简化样式处理的解决方案,并且具有相似的功能,但在语法、写法和工具生态系统上有一些差异。你可以根据自己的需求和喜好选择其中之一来进行样式管理。

具体的语法区别

Emotion 的语法:

  • Emotion 使用css函数来定义样式。你可以将 CSS 代码直接写在模板字符串中,也可以使用 JavaScript 对象来定义样式规则。
  • 使用模板字符串的 CSS 语法,可以像写普通 CSS 一样编写样式规则,包括选择器、属性和值。
  • 可以使用&符号来引用父选择器,用于嵌套选择器。
  • 使用css函数生成的样式类名可以通过css属性应用到组件上,例如:<button css={buttonStyles}>按钮</button>

styled-components 的语法:

  • styled-components 使用模板字面量(template literals)语法来定义样式。你可以创建一个 styled 组件模板字面量,并在其中定义 CSS 样式。
  • 在 styled 组件模板字面量中,你可以编写类似 CSS 的代码,使用选择器、属性和值。
  • 可以使用${}语法来插入动态的 JavaScript 表达式或传递组件的 props,以根据组件状态或属性来动态设置样式。
  • styled-components 返回一个 React 组件,你可以直接使用该组件并传递 props。

以下是一个简单的比较示例,演示了 Emotion 和 styled-components 的不同语法:

jsx
// Emotion语法示例
import { css } from "@emotion/react";

const Button = () => {
  const buttonStyles = css`
    color: ${(props) => (props.primary ? "blue" : "red")};
    background-color: yellow;
    padding: 10px;
    font-size: 16px;
  `;

  return <button css={buttonStyles}>按钮</button>;
};

// styled-components语法示例
import styled from "styled-components";

const Button = styled.button`
  color: ${(props) => (props.primary ? "blue" : "red")};
  background-color: yellow;
  padding: 10px;
  font-size: 16px;
`;

上述示例中,Emotion 使用css函数来定义样式规则,styled-components 使用styled.button语法来创建样式化的按钮组件。两者都使用动态的 JavaScript 表达式来设置样式,但具体的语法和代码结构略有不同。

总体而言,Emotion 和 styled-components 提供了不同的语法风格,用于在 React 应用程序中处理样式。你可以根据自己的喜好和项目需求选择其中之一,它们都提供了简化样式管理的强大工具。

注意:实际上区别并不大,Emotion 相对更好用

12.react-transition-group 过渡

13.dangerouslySetInnerHtml 渲染富文本

dangerouslySetInnerHTML 是 React 中的一个属性,它允许你直接设置组件的 HTML 内容(渲染富文本)。它被称为 "危险",是因为它绕过了 React 的内置渲染机制,如果使用不当,可能会导致安全漏洞。如果允许未经适当净化的用户生成内容被渲染,可能会导致跨站脚本攻击(XSS)的风险。

dangerouslySetInnerHTML 属性接受一个包含一个键名为 __html 的对象,该键名表示要注入到组件中的原始 HTML 内容。以下是在 React 中使用它的示例:

jsx
function MyComponent() {
  const htmlContent = "<strong>Hello, world!</strong>";

  return (
    <div
      dangerouslySetInnerHTML={{
        __html: htmlContent,
      }}
    />
  );
}

在这个示例中,htmlContent 变量包含一个 HTML 代码的字符串。通过将其传递给 <div> 组件的 dangerouslySetInnerHTML 属性,React 将原样渲染 HTML 内容,包括任何标签或元素。

相当于 vue 中的 v-html

14.immutable 深拷贝

immutable:不可变

深复制

深复制(Deep Copy)是一种复制对象的操作,它创建一个与原始对象具有相同值的新对象,但是在内存中完全独立于原始对象。与之相对的是浅复制(Shallow Copy),浅复制只复制对象的引用,而不是对象本身。

不合理、不完善的复制:

js
//1.直接赋值的浅复制
let obj = {
  name: "haowenhai",
};
let obj2 = obj; //引用复制 ——> 浅复制(浅拷贝),实际上也不算拷贝
obj2.name = "xiaoming"; //会影响 obj 的内容

//2.展开运算符...实现深复制
let myObj = {
  name: "haowenhai",
};
let myObj2 = {
  ...myObj, //深复制(但是只能有一层,也就是只比浅复制多复制了一层,不是真正意义上的深复制)
};
myObj2.name = "xiaoming"; //不会影响 obj 的内容

//3.展开运算符失效的情况
let myObj = {
  name: "haowenhai",
  arr: [1, 2, 3], //这里相当于两层,因为第一层是引用类型
};
let myObj2 = {
  ...myObj, //深复制失效,被展开的时候arr还是指向原来的内存空间
};
myObj2.arr.splice(1, 1); //会影响 myObj 的内容

//4.json 实现深复制
let jsonObj = {
  name: "haowenhai",
  arr: [1, 2, 3],
};
let jsonObj2 = JSON.PARSE(JSON.stringify(jsonObj));
jsonObj2.arr.splice(1, 1); //不会影响 myObj 的内容

//5.json 深复制失效的情况
let jsonObj = {
  name: "haowenhai",
  arr: [1, 2, 3],
  address: undefined, //不能有 undefined
};
let jsonObj2 = JSON.PARSE(JSON.stringify(jsonObj)); //丢了一个 address 字段,不合理

//6.手写递归深复制 ——> 缺点:性能不好,占用内存

合理的深复制方案:Immutable.js

为什么 react 中需要用深复制更新状态呢?状态的不可变性原则

一般来说我们用如下模版进行老状态的修改,实现响应式的变化:

js
const updateState = () => {
  // 进行深层次的状态复制,确保每个层级都是独立的副本
  const updatedState = JSON.parse(JSON.stringify(newState));
  // 修改新状态的某些属性
  updatedState.someProperty = "New Value";
  // 更新新状态
  setNewState(updatedState);
};

为啥一定要用 JSON 赋值一个副本呢,不能直接修改 newState 然后直接赋值吗?

因为在 React 中,状态应该是不可变的(immutable),也就是说,状态对象在更新时应该被视为不可更改的。直接修改 newState 的属性并使用 setNewState 进行赋值是违反 React 中的状态不可变性原则的。

React 使用浅比较(shallow comparison)来检测状态的变化,并根据变化来触发组件的重新渲染。当你直接修改 newState 的属性时,实际上是在改变同一个对象的属性,而不是创建一个新的对象。这样会导致 React 无法检测到状态的变化,从而无法正确地触发重新渲染。

理解:vue 中不需要使用 Immutable.js,那是因为 vue 是直接修改原状态从而被监听变化。而 react 中需要在保持老状态可用的前提下,根据老状态的副本(要复制老状态)去生成新状态,并赋值给原来的老状态(如果复制老状态的层级不够,那么修改副本的属性时就会影响老状态,那么在 setState 的时候二者将会是一样的,当然无法检测到变化!)

如果在更新状态时,复制的层级不够,也就是说,只复制了状态对象的引用而不是进行深层次的复制操作,那么修改新状态可能会影响到旧状态,因为它们共享相同的引用。这意味着对新状态的修改也会反映在旧状态上,从而破坏了状态的不可变性。

为了避免这种情况,通常需要进行深层次的状态复制,确保每个层级都是独立的副本,这样修改新状态就不会影响到旧状态。可以使用工具函数或库(如Object.assignspread运算符或immutable.js)来执行深层次的状态复制操作。

Immutable.js 基本介绍

Immutable.js 是一个在 JavaScript 中实现不可变数据结构的库。它提供了一组持久化的不可变数据类型,包括 List、Map、Set 等。使用 Immutable.js,你可以创建不可变的数据对象,这意味着一旦创建后就不能被修改。每次对不可变数据进行修改时,实际上是创建了一个新的不可变数据对象,而原始对象保持不变。 ——> 高性能的深复制方案

原理

Immutable 实现的原理是 Persistent Data Structure(持久化数据结构),也就是使用旧数据创建新数据时,要保证旧数据同时可用且不变。同时为了避免 deepCopy 把所有节点都复制一遍带来的性能损耗,Immutable 使用了Structural Sharing(结构共享),即如果对象树中一个节点发生变化,只修改这个节点和受它影响的父节点,其它节点则进行共享(延用原来的地址,不创建新的对象)。

——> 类似于 diff 比较:原数据不变,非必要的内容不重新创建

——> 实现深复制(满足原状态不变性原则),减少内存占用,优化性能

Immutable.js 提供了一些便捷的方法来操作不可变数据,比如添加、删除、修改元素等。由于不可变数据是持久化的,这些操作实际上是创建了新的数据结构,而不是在原始数据上进行修改。这种设计有助于避免副作用,提高代码的可预测性和可维护性。

动图演示:https://upload-images.jianshu.io/upload_images/2165169-cebb05bca02f1772

优势

使用 Immutable.js 可以带来一些好处,例如:

  1. 线程安全:由于不可变数据是只读的,可以在多线程环境下安全地共享和访问,无需担心并发修改的问题。
  2. 性能优化:由于不可变数据的持久性特性,可以通过共享结构来减少内存占用,并且在对数据进行修改时,可以进行快速的结构共享和复制(实现深拷贝)。
  3. 简化状态管理:不可变数据结构适用于应用程序的状态管理,例如在 React 组件中,可以通过不可变数据来跟踪状态的变化,并更方便地进行比较和更新。

Immutable 中常用类型(Map,List)

下载:

bash
npm i immutable
  1. Map

作用:把普通对象包裹为不可变对象 ——> 深加工的 object

缺点:只能包裹一层,否则里层还是只能复制引用,多层还要里面再包 Map

js
import { Map } from "immutable";

var obj = {
  name: "kerwin",
  age: 100,
};

var oldImmuObj = Map(obj);
var newImmuObj = oldImmuObj.set("name", "xiaoming");
// console.log(oldImmuObj,newImmuObj) //两个对象完全不一样

//1 get获取immutalble对象某个属性
console.log(oldImmuObj.get("name"), newImmuObj.get("name"));

//2 immutable对象转换为 ===> 普通对象,然后再获取某个属性
console.log(oldImmuObj.toJS().name, newImmuObj.toJS().name);

使用案例:一层结构 ——> 可用…代替

jsx
state = {
  info:{
    name:"kerwin",
    age:100
  }
}

render() {
  return (
    <div>
      <button onClick={()=>{
          var old = Map(this.state.info) //转为immutable 对象
          var newImmu = old.set("name","xiaoming").set("age",18) //只能一个一个修改
          this.setState({
            info:newImmu.toJS() //要设置为普通对象结构
          })
        }}>click</button>
      {this.state.info.name}--
      {this.state.info.age}
    </div>
  )
}

使用案例:多层结构 ——> 较为麻烦

jsx
state = {
  info:Map({
    name:"kerwin",
    select:"aa",
    filter:Map({ //多层的话,还要给引用类型再套一个 Map 才可以!
      text:"",
      up:true,
      down:false
    })
  })
}

componentDidMount() {
  console.log(this.state.info.get("filter"))
}

render() {
  return (
    <div>
      <button onClick={()=>{
          this.setState({
            info:this.state.info.set("name","xiaoming").set("select","dwadwa")
          })
        }}>click</button>
      {this.state.info.get("name")} {/*这时取值要用 get 取*/}
    </div>
  )
}

唯一的好处:无变化时可以复用地址,这样就可以让对象的比较顺利进行

jsx
state = {
  info:Map({
    name:"kerwin",
    select:"aa",
    filter:Map({ //多层的话,还要给引用类型再套一个 Map 才可以!
      text:"",
      up:true,
      down:false
    })
  })
}
{/* 唯一的好处:
如果这个某个属性给另一个组件作为 props用,那么包裹了 Map 之后可以被检测到相关属性并没有更改(因为 immutable 会判断如果没有变化就直接复用,复用的意思是直接使用原来的地址),不需要重新渲染,优化了性能*/}
<button onClick={()=>{
    this.setState({
  		info:this.state.info.set("name","xiaoming") //只改变 name,没有改变 filter
    })
  }}>click</button>
<Child filter={this.state.info.get("filter")}/>

class Child extends Component{
  shouldComponentUpdate(nextProps, nextState) {
    if(this.props.filter === nextProps.filter){ //比较引用地址是否相等,如果发生了变化,那么地址一定不相等,如果没变化,那么 immutable 会复用地址,地址一定相等。
      //不用 immutable 的话,父组件同样要用 json 的方式深复制更新状态(一旦深复制更新,那么就会生成不同地址的属性对象了),这时就要用json.stringify(this.props.filter) === json.stringify(nextProps.filter) 判断字符串内容是否相等,也可以!
      return false
    }
    return true
  }

  render(){
    return <div> child </div>
  }
  componentDidUpdate(){
    console.log("componentDidUpdate")
  }
}

注意:===的方式比较对象

这种比较方式只适用于判断引用是否相等,而不是判断对象的值是否相等。

判断对象值是否相等,需要转换为字符串,或者使用深层次比较的库(如 lodash 的 isEqual 方法)。

  1. List

作用:把普通数组包裹为不可变数组

缺点:只能包裹一层,否则里层还是只能复制引用,多层还要里面再包 List

优点:对于数组的操作方法和原生的 js 完全一样

jsx
import { List } from "immutable";
var arr = List([1, 2, 3]);

var arr2 = arr.push(4); //不会影响老的数组结构
var arr3 = arr2.concat([5, 6, 7]);
console.log(arr.toJS(), arr2.toJS(), arr3.toJS());

//使用案例:
export default class App extends Component {
  state = {
    favor: List(["aaa", "bbb", "ccc"]),
  };
  render() {
    return (
      <div>
        {
          /*immutable 对象的数组,也可以直接用 map 遍历*/
          this.state.favor.map((item) => (
            <li key={item}>{item}</li>
          ))
        }
      </div>
    );
  }
}
//push, set, unshift or splice 都可以直接用,返回一个新的immutable对象
  1. merge , concat

连接 List 的方法

  1. toJS

转换回 js 普通对象的方法

  1. fromJS

创建复杂数据结构 immutable 对象的解决方案,配合 setIn 和 updateIn 方法来使用

案例:个人信息修改

jsx
import { List,Map } from 'immutable'
import React, { Component } from 'react'

export default class App extends Component {
  state = {
    info:Map({
      name:"kerwin",
      location:Map({
        province:"辽宁",
        city:"大连"
      }),
      favor:List(["读书","看报","写代码"])
    })
  }
  render() {
    return (
      <div>
        <h1>个人信息修改</h1>
        <button onClick={()=>{
            this.setState({
              info: this.state.info.set("name","xiaomng").set("location",this.state.info.get("location").set("city","沈阳"))
            })
          }}>修改</button>
        <div>
          {this.state.info.get("name")}
          <br/>
          {
            this.state.info.get("location").get("province")
          }
          -
          {
            this.state.info.get("location").get("city")
          }
          <br/>
          {
            this.state.info.get("favor").map((item,index)=>
                                             <li key={item}>{item}
                                               <button onClick={()=>{
                                                   console.log(index)

                                                   this.setState({
                                                     info:this.state.info.set("favor",
{/*把点击项值改为 1*/}                                                                              this.state.info.get("favor").splice(index,1))
                                                   })
                                                 }}>del</button>
                                             </li>
                                            )
          }
        </div>
      </div>
    )
  }
}

但是,我们发现这样写起来非常复杂,并且有一个问题:本质上我们应该不知道后端返回的数据结构是 obj 还是 list 还是普通类型,我们没法针对性地用 Map 或者 List 包裹!

我们可以用 fromJs 来优化:

jsx
import { fromJS } from 'immutable'
import React, { Component } from 'react'

export default class App extends Component {
  state = {
    info:fromJS({
      name:"kerwin",
      location:{
        province:"辽宁",
        city:"大连"
      },
      favor:["读书","看报","写代码"]
    })
  }

  render() {
    return (
      <div>
        <h1>个人信息修改</h1>
        <button onClick={()=>{
            {/*setIn 方法实现深修改,第一个参数为数组,指定每层的属性*/}
            this.setState({
              info: this.state.info.setIn(["name"],"xiaoming")
              .setIn(["location","city"],"沈阳")
            })
          }}>修改</button>
        <div>
          {this.state.info.get("name")}
          <br/>
          {
            this.state.info.get("location").get("province")
          }
          -
          {
            this.state.info.get("location").get("city")
          }
          <br/>
          {
            this.state.info.get("favor").map((item,index)=>
                                             <li key={item}>{item}
                                               <button onClick={()=>{
                                                   console.log(index)
// 把点击项值改为 1:setIn也可以修改数组,因为数组的本质也是对象,属性就是索引值!但是会报错,因为对象的 key 不应该是数字
// this.setState({
//     info:this.state.info.setIn(["favor",index],1)
// })
                                                   this.setState({
 {/*updateIn 方法实现数组深修改,第二个参数为回调函数,把数组换成我们 return 的值(一层其实用 update 也行)*/}                                     info:this.state.info.updateIn(["favor"],(list)=>list.splice(index,1))
                                                   })
                                                 }}>del</button>
                                             </li>
                                            )
          }
        </div>
      </div>
    )
  }
}

总结

如果只有一层(数组或者对象),没必要用 immutable,用…即可

多层复杂的再使用 immutable

并且在使用 immutable 的时候,最合理的方案是使用 fromJs,这样不用关心这个数据具体是什么数据结构!

使用 immutable 优化 redux 的 reducer

因为 redux 的 reducer 要求是一个纯函数,应当满足不能改写参数数据

  • 不得改写参数数据(因为参数数据 preState,就是先前的状态)

这也是最重要的一点,redux 中的状态更新并不是通过修改先前的状态对象来实现的,而是通过创建一个新的状态对象并将其替换旧的状态对象来实现的。这样做的目的是保持状态的不可变性,以便更好地管理状态变更的追踪和控制。

jsx
import { fromJS } from "immutable";
const CityReducer = (
  prevState = {
    cityName: "北京",
    //  ...
  },
  action
) => {
  //临时转换更好:给外界交代的应该永远是一个js对象
  let newState = fromJS(prevState); //保证prevState不会有任何改变
  switch (action.type) {
    case "change-city":
      // newState.cityName = action.payload
      return newState.set("cityName", action.payload).toJS();
    default:
      return prevState;
  }
};
export default CityReducer;

15.Mobx

注意:redux 才是 react 全家桶中的一员,而 Mobx 用的相对较少

但是 Mobx 使用起来很简单

读音:哞币克斯

Mobx 介绍

(1) Mobx 是一个功能强大,上手非常容易的状态管理工具。

(2) Mobx 背后的哲学很简单: 任何源自应用状态的东西都应该自动地获得。

(3) Mobx 利用 getter 和 setter 来收集组件的数据依赖关系,从而在数据发生变化的时候精确知道哪些组件需要重绘,在界面的规模变大的时候,往往会有很多细粒度更新。(vue 类似)

image-20230704174748165

Mobx 与 Redux 的区别

Mobx 写法上更偏向于 OOP 面向对象

对一份数据直接进行修改操作,不需要始终返回一个新的数据并非单一 store,可以多 store。——> 类似 vue 了

Redux 默认以 JavaScript 原生对象形式存储数据,而 Mobx 使用可观察对象。

优点:

a. 学习成本小

b. 面向对象编程,而且对 TS 友好

缺点:

a. 过于自由:Mobx 提供的约定及模版代码很少,代码编写很自由,如果不做一些约定,比较容易导致团队代码风格不统一。

b. 相关的中间件很少,逻辑层业务整合是问题。

Mobx 的基本使用

安装:

bash
npm i mobx@5

基本类型的观察:

jsx
//对于普通类型数据的监听
var observableNumber = observable.box(10); //监听一个基本类型(发布者)
var observableName = observable.box("kerwin");

// 第一次执行,之后每次改变也会执行
autorun(() => {
  //获取改变的数据(订阅者,autorun:自动执行)——> 注意:只关心和自己相关的数据,也就是只有observableNumber改变才会触发(但是第一次肯定会执行)
  console.log("number改变了", observableNumber.get());
});
autorun(() => {
  console.log("name改变了", observableName.get());
});
setTimeout(() => {
  observableNumber.set(20);
  // observableName.set("xiaoming")
}, 1000);

setTimeout(() => {
  // observableNumber.set(20)
  observableName.set("xiaoming");
}, 2000);

引用类型的观察:

jsx
var myobj = observable.map({
  name: "kerwin",
  age: 100,
});
setTimeout(() => {
  myobj.set("name", "xiaoming");
}, 3000);

//或者:简写方式 ——> 推荐,类似 vue3 的 reactive
var myobj = observable({
  name: "kerwin",
  age: 100,
});
setTimeout(() => {
  myobj.name = "xiaoming"; //和 vue 类似
}, 3000);

autorun(() => {
  console.log("对象的name属性改变了", myobj.name); //只有 name 属性改变了才会执行 ——> 精确制导,细粒度的
});

实际的项目功能:

~/mobx/store.js

js
import { observable, configure, action, runInAction } from "mobx";
configure({
  enforceActions: "always", //严格模式:让 store 的数据只能用这个文件内部的方法修改
});

const store = observable(
  {
    isTabbarShow: true,
    list: [],
    cityName: "北京",
    changeShow() {
      //方法也写在这里,统一管理,在 store 内部进行修改,方便查找 bug
      this.isTabbarShow = true;
    },
    changeHide() {
      this.isTabbarShow = false;
    },
  },
  {
    changeHide: action,
    changeShow: action, //标记两个方法是action,专门修改可观测的value
  }
);

export default store;

修改状态:

jsx
import store from "../mobx/store";
useEffect(() => {
  //路由跳转进来之后执行
  // store.dispatch 通知
  // store.isTabbarShow = false
  store.changeHide(); //修改 store 值为 true
  return () => {
    //路由跳转走之后就会执行这个销毁流程
    console.log("destroy");
    // store.isTabbarShow = true
    store.changeShow(); //修改 store 值为 false
  };
}, []);

在 App.js 中进行状态监听(订阅):

jsx
import store from '../mobx/store'

// store.subsribe 订阅
 state = {
     isShow:false
 }

componentDidMount() {
   autorun(()=>{ //上来就会先执行一次
       console.log(store.isTabbarShow)
       this.setState({
           isShow:store.isTabbarShow //根据 store 中的值改变标志量,从而控制 tabbar 的显示和隐藏
       })
   })
}
render() {
  return (
    <div>
      {/* 其他的内容 */}
      <MRouter>
        {this.state.isShow && <Tabbar></Tabbar>}
        {/*等价于:this.state.isShow ? <Tabbar></Tabbar> : <></>*/}
      </MRouter>
    </div>
  )
}

面向对象的写法 + 异步 action 方法

注意:@符号是 ES7 的装饰器语法

注意:装饰器语法已经不被 Mobx@6 推荐了,主流是函数式编程!

让编辑器支持@符号:

image-20230704193849119

让项目支持装饰器:

bash
npm i @babel/core @babel/plugin-proposal-decorators @babel/preset-env
npm i customize-cra react-app-rewired

创建 .babelrc

json
{
  "presets": ["@babel/preset-env"],
  "plugins": [
    [
      "@babel/plugin-proposal-decorators",
      {
        "legacy": true
      }
    ]
  ]
}

创建 config-overrides.js

js
const path = require("path");
const { override, addDecoratorsLegacy } = require("customize-cra");

function resolve(dir) {
  return path.join(__dirname, dir);
}

const customize = () => (config, env) => {
  config.resolve.alias["@"] = resolve("src");
  if (env === "production") {
    config.externals = {
      react: "React",
      "react-dom": "ReactDOM",
    };
  }
  return config;
};

module.exports = override(addDecoratorsLegacy(), customize());

修改 package.json

json
...
"scripts": {
"start": "react-app-rewired start", "build": "react-app-rewired build", "test": "react-app-rewired test", "eject": "react-app-rewired eject"
},
...

开始写 store 文件:

~/mobx/store.js

jsx
import { observable, configure, action, runInAction } from "mobx";
import axios from "axios";
configure({
  enforceActions: "always",
});
class Store {
  @observable isTabbarShow = true; //定义一个被观察变量
  @observable list = [];

  @action changeShow() {
    this.isTabbarShow = true;
  }

  @action changeHide() {
    this.isTabbarShow = false;
  }

  @action async getList() {
    //异步 action
    var list = await axios({
      url: "https://m.maizuo.com/gateway?cityId=110100&ticketFlag=1&k=7406159",
      method: "get",
      headers: {
        "X-Client-Info":
          '{"a":"3000","ch":"1002","v":"5.0.4","e":"16395416565231270166529","bc":"110100"}',
        "X-Host": "mall.film-ticket.cinema.list",
      },
    }).then((res) => {
      // console.log(res.data.data.cinemas)
      return res.data.data.cinemas;
    });

    runInAction(() => {
      //异步 action 必须用这个方法去修改,保证赋值语句在 action 方法内,否则会被 mobx 认为我们是在外面修改的状态,严格模式报错
      this.list = list;
    });
  }
}
const store = new Store(); //创建出一个对象

export default store;

使用:

jsx
import store from "../mobx/store";

const [cinemaList, setCinemaList] = useState([]);

useEffect(() => {
  if (store.list.length === 0) {
    store.getList(); //如果没有数据就去调用方法获取数据
  }
  let unsubscribe = autorun(() => {
    //创建监听器
    console.log(store.list, store.isTabbarShow);
    setCinemaList(store.list);
  });
  return () => {
    //取消订阅:必须要取消订阅,因为组件销毁的时候并不会取消监听,而再次进来的时候又会重新生成一个监听器,这样会造成有多个监听器!应该用完即毁!App.js中没有取消订阅,是因为 App组件 在不刷新的情况下不会重复渲染也不会销毁!
    unsubscribe();
  };
}, []);

组件的渲染和卸载周期:

子组件这些零散组件,每当路由跳转进入的时候会渲染,而路由跳转走就会销毁!

App 根组件 在不刷新的情况下不会重复渲染也不会销毁,只有一次渲染一次销毁!

每当刷新页面,就会把整个应用重启,生命周期重新开始!

mobx-redux

安装:

bash
npm i mobx-redux@5

使用:

mobx-redux 可以实现自动订阅监听,不用手动 autorun 监听了!并且可以统一引入 store!——>这样组件里面不需要自己的状态值,只需要使用 store 中的即可!

jsx
import { Provider } from "mobx-react";
import store from "./mobx/store";
ReactDOM.render(
  <React.StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </React.StrictMode>,
  document.getElementById("root")
);

使用 store:

inject:注入

jsx
import { inject, observer } from "mobx-react";
@inject("store") //只是把 store 引入到组件中可以用,但是还不具备监听的效果
@observer //一个高阶组件,用于监听 store 的变化
class App extends Component {
  render() {
    return (
      <div>
        {/* 获取 store 通过 this.props */}
        <MRouter>{this.props.store.isTabbarShow && <Tabbar></Tabbar>}</MRouter>
      </div>
    );
  }
}
export default App;

函数式组件:

jsx
import { Observer } from "mobx-react";
import React, { useEffect } from "react";
import store from "../mobx/store"; //必须还要自己引入 store,对于函数式组件来说主要就是会自动监听 store 变化了,不需要有自己的状态

export default function Cinemas(props) {
  useEffect(() => {
    if (store.list.length === 0) {
      store.getList(); //调用方法
    }
    return () => {};
  }, []);

  return (
    <div>
      {/* 使用Observer标签 */}
      <Observer>
        {
          /* 这里必须写成箭头函数并返回 jsx 的形式 */
          () => {
            return store.list.map((item) => (
              <dl key={item.cinemaId} style={{ padding: "10px" }}>
                <dt>{item.name}</dt>
                <dd style={{ fontSize: "12px", color: "gray" }}>
                  {item.address}
                </dd>
              </dl>
            ));
          }
        }
      </Observer>
    </div>
  );
}

16.React 结合 TypeScript

基本介绍

文档地址:https://ts.nodejs.cn/

  1. TypeScript 的定位是静态类型语言,在写代码阶段就能检查错误,而非运行阶段。——> 在编辑器中(预编译阶段)就可以发现错误(就像静态类型语言一样,js 本身是动态语言)

  2. 类型系统是最好的文档,增加了代码的可读性和可维护性。

  3. 有一定的学习成本,需要理解接口(Interfaces)、泛型(Generics)、类(Classes)等。

  4. ts 最后会被编译成 js。

安装

创建一个基于 ts 的 cra 脚手架 项目

bash
create-react-app 项目名 --template typescript
image-20230704220327344

声明

以使用 jquery 库的时候报错的解决为例:

  1. 可以在当前文件加上 declare const $: any;

假设你在 TypeScript 项目中使用了 jQuery,但没有相应的类型定义文件,你可以使用以下声明来解决类型检查问题:

ts
declare const $: any;

这样,你就可以在代码中直接使用 $ 来调用 jQuery 的方法,例如 $.ajax(),而不会触发类型错误。

  1. 或者安装 npm i @types/jquery,即类型定义文件

@types 是 npm 的一个分支,用来存放*.d.ts 文件

注意:ts 文件必须有导出 export 方法

变量声明类型

tsx
//定义单个类型:
var myname: string = "字符";
var mybool: boolean = false;
var mynumber: number = 100;
//定义数组单个类型:
var mylist: Array<string> = ["111", "222", "3333"];
var mylist2: string[] = ["111", "222", "3333"];

//定义多个类型:
var myname2: string | number | boolean = 100;
var myname3: string | number = "kerwin";
//定义数组多个类型:
var mylist2: Array<string | number> = [1, 2, "kerwin"]; //第一种方式:泛型写法
var mylist3: (string | number)[] = [1, 2, "kerwin"]; //第二种方式
var mylist4: string[] | number[] = [1, 2, 3]; //要不然全是数字,要不然全是字符串["1","2","3"]

//定义随意类型:
var myany: any = 100;

string 和 String 的区别:

原生的 string 是不包含属性的值(即没有 properties),包括字面上没有定义类型、字面上定义了 string、字面上定义了 String 和一些从 string 函数调用返回的 strings 也都可以被归为原生类型:

ts
let msg = "Hello world!"; //字面上没有定义类型
let msg2: string = "Hello world!";
let msg3: String = "Hello world!"; //即便定义为 String 对象类型,这也是 string 类型
let msg4: string = returnStr();

以上四个变量的类型(typeof())是 string。

只有let msg3: String = new String('Hello world!');才是 String 对象类型的

主要区别:

1、当用 a1,b1 代表相同值的两个变量的时候,它们是相同的;而当用 new 新建两个对象的时候,即使值相同,它们也是不同的(下图会输出false, true):

image-20230704230905747

2、eval()函数的作用:用来计算表达式的值。如果我们把 eval()直接赋给 string,而 string 里面是计算式的字符串,那么它会返回计算后的值;而如果我们把 eval()赋给 String,因为它不是原生类型,它只会返回 String 这个对象(下图会输出27, :"8 + 20", 28):

image-20230704230928934

3、其次,因为 String 对象可以有属性。我们可以用 String 对象在属性里保留一个额外的值。即使这个用法并不常见,但是仍然是一个特性:

js
var prim = "hello HW";
var obj = new String("hello HW Cloud");

prim.property = "PaaS"; // Invalid
obj.property = "PaaS"; // Valid
console.log(obj.property); //输出为PaaS

两者区别总结:

string 原生类型String 对象
广泛被使用几乎很少被使用
只会保留值有能力除了值之外,还可以保留属性
值是不可变的,因此线程安全String 对象是可变的
没有任何方法String 对象有各种方法
不能创建两个独立的字面上值相同的 string可以用new创建两个对象
是原生的数据类型包装原生数据类型来创建一个对象
传递的值是原生数据本身的拷贝传递的值是实际数据的引用
当使用 eval()函数时,将直接作为源代码进行处理当使用 eval()函数时,将被转换为字符串

定义对象

tsx
interface IObj {
  //接口:用接口来描述对象形状(接口名字一般大写)
  name: string;
  age: number;
  location?: string; //加了?就是可选属性,有没有这个属性都可以!——> 非常有用
  [propName: string]: any; //剩余字段:其他属性(一些用不到的属性,太麻烦了没必要限制类型的属性,不写还不行,那么就都归在这里)——> 非常有用
}
var obj1: IObj = {
  name: "kerwin",
  age: 100,
  location: "大连",
  grade: "7.7",
  isPresale: true,
  isSale: false,
  item: { name: "4D", type: 13 },
};

定义普通函数

tsx
//定义普通函数:
//num 非必传
function mySearch(src: string, sub: string, num?: number): boolean {
  let result = src.search(sub);
  return result > -1;
}
var myFlag: string = mySearch("aaa", "bbb", 100);

//定义函数类型的变量:定义接口,意思是将来赋值给这个变量的函数,必须满足接口的形状!
//对于函数类型的类型检查来说,函数的参数名不需要与接口里定义的名字相匹配!
interface IFunc {
  (source: string, subString: string, number?: number): boolean;
}
var myFun: IFunc;
myFun = function mySearch(src: string, sub: string, num?: number): boolean {
  //普通函数
  let result = src.search(sub);
  return result > -1;
};
var myFun2: IFunc;
myFun2 = (src: string, sub: string, num?: number): boolean => {
  //箭头函数
  let result = src.search(sub);
  return result > -1;
};

//在对象中定义一个函数:
var obj: Iobj = {
  name: "kerwin",
  age: 100,
  getName: (name: string) => string, //和定义在接口中的函数写法略有不同
};

函数的传参

ts
function Test(list: String[], text?: String, ...args: String[]): void {
  //剩余的参数组成一个 string 类型的数组args
  console.log(list, text, args);
}

Test(["1111", "2222"]);
//list:["1111","2222"] text: undefined args: []

Test(["0", "1"], "a", "b", "c");
//list:["0","1"] text: "a" args: ["b","c"]

类型断言

断定一个变量是某个类型,使编辑器信服,从而可以应用这个类型中的相关方法和属性!

用于编辑器不确定某个数据的类型的时候,但是我们确定这个类型,我们就要在代码中去断定它、说明它,让编辑器知道它的类型。

例如:在一个变量可能为确定类型也可能为 null 的时候(比如这个变量接收的是一个方法的某类型的返回值,但函数没执行的时候这个变量是 null),编辑器不会让我们直接用类型的相关方法,因为它担心这个变量为 null,但是我们很确定它不会为 null,就可以用断言,让编辑器知道它一定是这个类型的!

注意:当一个变量没有被确切地赋值的时候,编辑器都会有理由认为它为 null,比如形参、函数返回值。

ts
function Test(mytext: string | number) {
  console.log((mytext as string).length); //或者 console.log((mytext as any).length) //对
  console.log((mytext as string[]).length); //错,原声明没有这个类型,无法断言
}

定义类

标识符约束属性的访问权限:public、private、protected

ts
class Bus {
  public name = "kerwin"; // public标识符定义公有属性
  private _list: any = []; // private标识符定义私有属性(命名时前面一般加个_),list 不需要外面去使用,子类也不能访问
  protected age = 100; //protected标识符定义受保护的属性:子类可以访问,外面不可以访问

  public subscribe(cb: any) {
    this._list.push(cb);
  }

  public dispatch() {
    this._list.forEach((cb: any) => {
      cb && cb(); //如果 cb 不为假,就运行cb函数
    });
  }
}

class Child extends Bus {
  test() {
    console.log(this.name, this.age); //可以访问受保护的属性 age
  }
}

var obj = new Bus();
obj.subscribe(() => {
  console.log("hello world!");
});

console.log(obj.name); //只有 name 可以在外面访问

定义接口约束类的形状:implements 关键字用于实现接口

ts
interface Ifunc {
  getName: () => string;
  getAge: () => number;
  //还可以有属性!
}

class A implements Ifunc {
  //用implements关键字来实现接口,那么类里面就必须至少实现接口的所有方法
  getAge() {
    return 100;
  }
  a1() {}
  a2() {}
  getName() {
    return "AAA";
  }
}

class B implements Ifunc {
  getAge() {
    return 100;
  }
  b1() {}
  b2() {}
  getName() {
    return "CCC";
  }
}

class C implements Ifunc {
  getAge() {
    return 100;
  }
  getName() {
    return "CCC";
  }
}

function init(obj: Ifunc) {
  //obj 必须至少包含 getName 和 getAge 两个方法
  obj.getName();
  obj.getAge();
}
var objA = new A();
var objB = new B();
var objC = new C();

init(objA);
init(objB);
init(objC);

定义 React 类式组件

tsx
interface PropInter {
  name: string | number;
  firstName?: string; //可选属性
  lastName?: string; //可选属性
  // [propName: string]: any 任意属性
}
interface StateInter {
  count: number;
}
//根组件React.Component,接收两个泛型参数用于约束类型,第一个类型为 props 的类型,第二个类型为 state 的类型
//参数可以传any,表示不约束
class HelloClass extends React.Component<PropInter, StateInter> {
  state = {
    //注意:不能直接state:StateInter,因为这样的话用 setState 方法时检测不到 state 被赋值了什么类型
    count: 0,
  }; //setState时候才会检查
  static defaultProps = {
    // 属性默认值,react 默认的一个属性,并且必须定义在类上
    name: "default name",
    firstName: "2",
    lastName: "333",
  };
  render() {
    return <div>child-{this.props.name}</div>;
  }
}
jsx
//传参:
<HelloClass name='111' firstName='222'></HelloClass>

实例:

tsx
import React, { Component } from "react";
interface IState {
  //指定 state 的类型
  text: string;
  list: string[];
}
export default class App extends Component<any, IState> {
  state = {
    text: "",
    list: [],
  };
  myref = React.createRef<HTMLInputElement>(); //必须指定这个类型,createRef 函数是一个泛型函数,它允许我们指定引用(ref)的类型。通过在尖括号中提供类型参数 <HTMLInputElement>,我们告诉 createRef 函数创建一个特定类型的引用,即 HTMLInputElement 类型的引用(但 myref 不一定是HTMLInputElement 类型,还可能为 null)。
  render() {
    return (
      <div>
        <input ref={this.myref} />
        <button
          onClick={() => {
            console.log(this.state.text);
            console.log((this.myref.current as HTMLInputElement).value);

            this.setState({
              //类型断言,从而告诉编辑器这个current对象是HTMLInputElement类型的
              //为什么需要断言?因为current在函数没执行的时候,是空的为 null
              list: [
                ...this.state.list,
                (this.myref.current as HTMLInputElement).value,
              ],
            });
          }}>
          click
        </button>

        {this.state.list.map((item) => (
          <li key={item}>{item}</li>
        ))}
      </div>
    );
  }
}

定义 React 函数式组件+useState

tsx
const App: React.FC = (props) => {
  console.log(props);
  const [name, setname] = useState<string>("kerwin"); //useState这里直接指定返回值state的类型即可
  return <div>app</div>;
};

//定义 props 类型
interface IProps {
  count: number;
}
//写法一:子组件接受属性
const Child: React.FC<IProps> = (props) => {
  return <div>child-{props.count}</div>;
};
//写法二:子组件接受属性
const Child: React.FC = (props: IProps) => {
  return <div>child-{props.count}</div>;
};
tsx
<Child count='100' />

实例:

todolist.tsx

tsx
import { useRef, useState } from "react";

export default function App() {
  const mytext = useRef<HTMLInputElement>(null);
  const [list, setlist] = useState<string[]>([]);
  return (
    <div>
      <input ref={mytext} />

      <button
        onClick={() => {
          console.log((mytext.current as HTMLInputElement).value);
          setlist([...list, (mytext.current as HTMLInputElement).value]);
        }}>
        click
      </button>

      {list.map((item) => (
        <li key={item}>{item}</li>
      ))}
    </div>
  );
}

抽屉.tsx

tsx
import { useState } from "react";

export default function App() {
  const [isShow, setisShow] = useState(true);
  return (
    <div>
      <Navbar
        cb={() => {
          console.log("11111");
          setisShow(!isShow);
        }}
      />
      {isShow && <Sidebar />}
    </div>
  );
}
interface IProps {
  title?: string; //可选
  cb: () => void;
}

function Navbar(props: IProps) {
  //实现点击按钮进行 sidebar 的显示和隐藏 ——> 抽屉
  return (
    <div>
      navbar-
      <button
        onClick={() => {
          props.cb();
        }}>
        click
      </button>
    </div>
  );
}

function Sidebar() {
  return <div>sidebar</div>;
}

useRef

tsx
const mytext = useRef<HTMLInputElement>(null) //指定泛型函数useRef的返回值类型

<input type="text" ref={mytext}/>

useEffect(() => {
  console.log(mytext.current && mytext.current.value)
}, [])

useContext

tsx
interface IContext{
  call:string
}

const GlobalContext = React.createContext<IContext>({ //指定泛型函数createContext的参数类型
  call:"" //定义初始值,按照接口规则
})

<GlobalContext.Provider value={{call:"电话"}}>
....
</GlobalContext.Provider>

type 关键字

type 关键字在 TypeScript 中用于创建类型别名(Type Alias)或联合类型(Union Types)

  1. 类型别名(Type Alias):type 关键字可以用来创建自定义的类型别名,将一个现有的类型赋予一个新的名称。这样可以方便地重用复杂的类型或使代码更加可读。例如:
ts
type MyString = string;
type Point = { x: number; y: number }; //类似于 interface
  1. 联合类型(Union Types):type 关键字还可以用于定义联合类型,表示一个值可以是多个类型之一。使用 | 符号将多个类型组合在一起,表示选择关系。例如:
ts
type MyType = string | number;

这表示 MyType 可以是 string 类型或 number 类型之一。

ts
interface IProps {
  name: string;
  age: number;
}
type HomeProps = IProps & RouteComponentProps;

使用 type 关键字定义了一个新的类型 HomeProps,它是由 IPropsRouteComponentProps 两个类型进行合并得到的。& 符号表示交叉类型,它将两个类型的属性和方法进行合并,形成一个新的类型。

通过使用 type 关键字,我们可以更灵活地定义复杂的类型,并在代码中进行重用。它有助于提高代码的可读性和可维护性,并使类型系统更加强大。

type 和 interface 的区别?

在 TypeScript 中,typeinterface 两个关键字都可以用来定义类型,但它们有一些区别和不同的使用场景。

区别如下:

  1. 定义方式:使用 type 关键字创建类型别名,使用 interface 关键字创建接口。
ts
type MyType = {
  // 类型定义...
};

interface MyInterface {
  // 接口定义...
}
  1. 合并类型:type 可以使用交叉类型(&)和联合类型(|)来合并现有的类型,而 interface 不支持这样的合并方式。
ts
type MyCombinedType = TypeA & TypeB;

type MyUnionType = TypeA | TypeB;
  1. 兼容性:interface 可以进行接口的继承和实现,从而支持更灵活的类型兼容性检查。而 type 无法直接实现继承。
ts
// 定义一个基础接口
interface Animal {
  name: string;
  eat(): void;
}

// 定义一个继承自 Animal 的接口
interface Dog extends Animal {
  breed: string;
  bark(): void;
}
  1. 可读性:对于大多数情况下的对象类型定义,interface 更直观、易读。它更常用于描述对象的形状、属性和方法。
ts
interface Person {
  name: string;
  age: number;
}

const person: Person = {
  name: "John",
  age: 30,
};
  1. 范围:type 可以用于定义任意类型,包括基本类型、联合类型、交叉类型等。而 interface 主要用于定义对象类型,可以描述对象的形状和行为。

在实际使用中,一般可以按照以下原则选择使用 type 还是 interface

  • 当需要定义一个普通的对象类型或函数类型时,优先考虑使用 interface
  • 当需要使用联合类型或交叉类型进行类型组合时,使用 type
  • 如果代码已经大量使用了 interface,并且需要扩展或修改现有的类型定义,可以继续使用 interface
  • 如果需要定义复杂的类型别名,或者需要使用类型推导(type inference),可以使用 type

总的来说,typeinterface 都有各自的特点和适用场景,可以根据具体情况选择使用。在实际开发中,它们可以互补使用,共同构建类型安全的 TypeScript 代码。

路由

注意:使用路由必须安装@types 下的 router 类型声明文档 npm i @types/react-router-dom,否则从 react-router-dom 中引入内容会报错!并且在写代码阶段就会提示和纠错库中的组件有什么属性和方法!类型推断和属性校验!

但是有的库,声明文档就放在根目录下面了,比如 axios,那么就不需要额外安装!

其他的@types 在创建项目的时候就下载好了:

image-20230705125048691

需要使用 history、match、location 对象的时候:

tsx
// 使用编程式导航,需要引入接口配置,这里定义了 history、location、match 的类型
import { RouteComponentProps } from "react-router-dom";

interface IProps {
  name: string;
  age: number;
}

//自定义合并类型:
type HomeProps = IProps & RouteComponentProps; //合并自定义的 props类型 和 Router定义的类型

interface IState {
  data: string[];
}

class Home extends React.Component<HomeProps, IState> {
  private handleSubmit = async () => {
    this.props.history.push("/home"); //这样 history 就存在我们的 props 类型里面了
  };
  public render(): any {
    return <div>Hello</div>;
  }
}

动态路由接收 params 参数的时候:

tsx
import { RouteComponentProps } from "react-router-dom";
interface IParams {
  id: string; //应该传过来一定是 string
}
// 本身RouteComponentProps是一个泛型接口,可以接收一个参数,指定 match.params的类型
class Detail extends Component<RouteComponentProps<IParams>> {
  componentDidMount() {
    console.log(this.props.match.params.id); //接收路由参数
    //或者:console.log((this.props.match.params as any).id) //用断言,也可以不指定 params 类型
  }
  render() {
    return <div>detail</div>;
  }
}
tsx
interface IItem {
    filmId:number,
    name:string
}
this.state.list.map( (item:IItem)=>
                    <li key={item.filmId} onClick={()=>{
                        //传递路由参数:
                        this.props.history.push(`/detail/${item.filmId}`)
                      }}>{item.name}</li>

redux

jsx
import { createStore } from "redux";

interface IAction {
  type: string;
  payload?: any;
}

interface IState {
  isShow: boolean;
}

const reducer = (prevState: IState = { isShow: true }, action: IAction) => {
  const { type } = action;
  const newState = { ...prevState };
  switch (type) {
    case "show":
      newState.isShow = true;
      return newState;
    case "hide":
      newState.isShow = false;
      return newState;
    default:
      return prevState;
  }
};

const store = createStore(reducer);
export default store;

antd-mobile 组件库

实例:

tsx
import React, { Component } from "react";
import axios from "axios";
import { RouteComponentProps } from "react-router-dom";
import { Button, Swiper } from "antd-mobile";
import { SwiperRef } from "antd-mobile/es/components/swiper"; //引入对应的 ref 类型

interface IItem {
  filmId: number;
  name: string;
}

export default class Film extends Component<RouteComponentProps, any> {
  state = {
    looplist: [],
  };
  ref = React.createRef<SwiperRef>();
  render() {
    return (
      <div>
        <Swiper loop autoplay ref={this.ref}>
          {this.state.looplist.map((item: any) => (
            <Swiper.Item key={item.bannerId}>
              <img src={item.imgUrl} style={{ width: "100%" }} />
            </Swiper.Item>
          ))}
        </Swiper>
        <Button
          color='danger'
          onClick={() => {
            this.ref.current?.swipePrev(); //也可以不断言为SwiperRef,在不为空的情况下用swipePrev()方法
          }}>
          上一个
        </Button>
        <Button
          color='primary'
          onClick={() => {
            (this.ref.current as SwiperRef).swipeNext(); //断言类型,从而可以用swipeNext()方法
          }}>
          下一个
        </Button>
      </div>
    );
  }
}

17.单元测试

React 单元测试是用于测试 React 组件的功能和行为的测试方法。它可以帮助开发人员验证组件的正确性、检测潜在的错误和保证代码质量。

以下是进行 React 单元测试的一般步骤:

  1. 安装测试库:首先,你需要选择并安装适合你的测试库。常见的选择包括 Jest、React Testing Library 和 Enzyme。这些库提供了用于编写和运行 React 单元测试的工具和断言库。
  2. 创建测试文件:为要测试的组件创建一个与之对应的测试文件。命名约定通常是将要测试的组件的文件名后缀改为 .test.js.spec.js,例如 MyComponent.test.js
  3. 编写测试用例:在测试文件中,编写一个或多个测试用例来验证组件的不同方面。测试用例应该包括对组件的输入、状态、交互和输出的各种断言。你可以使用测试库提供的断言函数来验证组件的行为是否符合预期。
  4. 渲染组件和触发事件:在测试用例中,使用测试库提供的方法来渲染组件并模拟用户交互行为,例如点击按钮、输入文本等。然后,检查组件的输出和状态是否正确。
  5. 运行测试:使用测试库提供的命令或运行器来运行测试。测试库会自动运行测试文件中的所有测试用例,并提供有关测试结果的报告。
  6. 分析测试结果:检查测试报告,查看测试用例的通过与否。如果有失败的测试用例,你可以检查错误信息和堆栈跟踪以找出问题所在,并进行调试和修复。
  7. 重复步骤 3-6:根据需要,编写更多的测试用例来涵盖更多的场景和功能,以确保组件的完整覆盖和正确性。

React 单元测试的目标是针对组件的每个独立部分进行测试,而不是依赖于其他组件或外部服务。这样可以提高测试的可靠性和独立性。

在编写 React 单元测试时,你可以使用各种断言函数、模拟工具和辅助函数来简化测试的编写和执行过程。了解所选择的测试库的文档和示例将有助于更好地理解和应用单元测试的概念和实践。

总的来说,React 单元测试是一种重要的开发实践,可以帮助确保组件的功能正确性和可靠性,并提高代码的可维护性和可测试性。

理解测试案例的作用:模拟人的操作,只要测试案例跑通了,就不需要人再去测试每一个操作,并且测试案例会在之后的系统扩展过程中更加有用,因为扩展系统可能会影响之前的代码,如何证明不会影响呢,就需要再次跑测试用例,这样就不需要人再去点击测试了,非常高效。——> 相当于对于系统测试过程的一种存档,一种自动化测试方案

react-test-renderer

react-test-renderer 是 react 官方提供的测试库

bash
npm i react-test-renderer

在/test 文件夹下创建测试文件

扩展名必须为.test.js

实例:

js
import ShallowRender from "react-test-renderer/shallow";
import App from "../App";
import ReactTestUtil from "react-dom/test-utils";
describe("react-test-render", function () {
  //需要先判断测试用例的名字 //测试标签的内容(测试组件渲染出来的 html)
  it("判断 App 组件里面的 h1标签 里面的内容是否为 kerwin-todo", function () {
    const render = new ShallowRender();
    render.render(<App />); //把 App 送进去渲染成为虚拟 dom
    // console.log(render.getRenderOutput().props.children[0].type) //输出虚拟 dom 节点
    expect(render.getRenderOutput().props.children[0].type).toBe("h1"); //断言是 h1,如果报错就说明源代码出错了
    expect(render.getRenderOutput().props.children[0].props.children).toBe(
      "kerwin-todo"
    );
  });
  //测试删除功能
  it("删除功能", function () {
    const app = ReactTestUtil.renderIntoDocument(<App />); //把 App 送进去渲染成为真实 dom(只有真实 dom 才可以被点击)
    let todoitems = ReactTestUtil.scryRenderedDOMComponentsWithTag(app, "li"); //查找所有 li
    console.log(todoitems.length);

    let detelteButton = todoitems[0].querySelector("button"); //找到第一个删除按钮

    ReactTestUtil.Simulate.click(detelteButton); //模拟点击事件

    let todoitemsAfterClick = ReactTestUtil.scryRenderedDOMComponentsWithTag(
      app,
      "li"
    );

    expect(todoitems.length - 1).toBe(todoitemsAfterClick.length); //判断是否长度少了 1 个
  });
  //测试添加功能
  it("添加功能", function () {
    const app = ReactTestUtil.renderIntoDocument(<App />);
    let todoitems = ReactTestUtil.scryRenderedDOMComponentsWithTag(app, "li");
    console.log(todoitems.length);

    let addInput = ReactTestUtil.scryRenderedDOMComponentsWithTag(app, "input");
    addInput.value = "kerwinaaaaaa"; //给 input 添加内容
    let addButton = ReactTestUtil.findRenderedDOMComponentWithClass(app, "add");

    ReactTestUtil.Simulate.click(addButton);

    let todoitemsAfterClick = ReactTestUtil.scryRenderedDOMComponentsWithTag(
      app,
      "li"
    );

    expect(todoitemsAfterClick.length).toBe(todoitems.length + 1); //判断是否长度多了 1 个
  });
});

运行命令:npm test

输入 a,会把所有.test.js 结尾的文件都运行

没有报错就是通过了

enzyme

enzyme 是第三方的单元测试模块,更加简单好写

bash
npm i enzyme

需要适配器才可以工作:下面是 react@17 对应的适配器

由社区的个人开发,因为官方没有开发 17 版本的

bash
npm i @wojtekmaj/enzyme-adapter-react-17

实例:

jsx
import App from "../App";
import Enzyme, { shallow, mount } from "enzyme"; //需要适配器
import adpater from "@wojtekmaj/enzyme-adapter-react-17"; //适配器

Enzyme.configure({ adapter: new adpater() });

describe("react-test-render", function () {
  it("app 的名字事kerwin-todo", function () {
    let app = shallow(<App />); //渲染虚拟 dom
    expect(app.find("h1").text()).toEqual("kerwin-todo"); //类似操作 jquery 的方式
  });

  it("删除功能", function () {
    let app = mount(<App />); //渲染真实 dom
    let todolength = app.find("li").length;
    app.find("button.del").at(0).simulate("click");
    expect(app.find("li").length).toEqual(todolength - 1);
  });

  it("添加功能", function () {
    let app = mount(<App />);
    let todolength = app.find("li").length;
    let addInput = app.find("input");
    addInput.value = "kerwinaaaaa";
    app.find(".add").simulate("click");
    expect(app.find("li").length).toEqual(todolength + 1);
  });
});

18.forwardRef

forwardRef 是 React 中的一个函数,用于在函数组件中转发(forward)ref。它是 React 提供的一种高级技术,用于在组件之间传递 ref。

在 React 中,ref 用于引用组件或 DOM 元素,允许我们访问组件的实例或 DOM 节点。通常,ref 只能在类组件中使用,并通过 React.createRef() 或回调函数的方式来创建和传递。然而,函数组件通常是无状态的,没有实例来引用,因此无法直接使用 ref。

并且 ref 是无法通过 props 进行传递的,所以我们使用 forwardRef,让 ref 可以传递给函数式组件!

**forwardRef 的作用就是解决函数组件中无法直接使用 ref 的问题。**通过使用 forwardRef,我们可以将 ref 传递给函数组件,并在组件内部访问 ref。——> 注意,在组件内部使用了 ref,也不一定要给 ref 绑定为 dom,ref 本质上可以绑定任何值(但是 forwardRef 只能用于函数式组件)

使用 forwardRef 的示例:

jsx
import React, { forwardRef } from "react";

const MyComponent = forwardRef((props, ref) => {
  // 在这里可以访问 ref
  return <div ref={ref}>Hello, World!</div>;
});

// 使用 MyComponent
const App = () => {
  const myRef = React.createRef(); //这里的 myRef 对应的就是子组件MyComponent的那个 helloworld 的 div 元素dom,成功取到了子组件的实例对象!

  return <MyComponent ref={myRef} />;
};

在上面的示例中,我们定义了一个函数组件 MyComponent 并使用 forwardRef 包裹它。这样就可以将 ref 参数传递给MyComponent 组件,并在组件内部使用 ref

在使用 MyComponent 的地方,我们创建了一个 refmyRef),然后将它传递给 MyComponentref 属性。MyComponent 在内部使用 ref 来引用 <div> 元素。

通过这种方式,我们可以在函数组件内部使用 ref,并将其传递给需要引用的元素或组件。这对于访问子组件的实例或 DOM 元素非常有用,以便进行操作或获取信息。

实例:ref 绑定的是一个类式组件,最终获取了类式组件的实例对象

forwardRef 中的 return 返回一个类式组件时,它将使函数组件在外部使用时可以接受和传递 ref。这样,你可以通过 ref 属性引用类式组件的实例。

jsx
import React, { forwardRef } from "react";

class MyComponent extends React.Component {
  render() {
    return <div>Hello, World!</div>;
  }
}

const ForwardedComponent = forwardRef((props, ref) => {
  return <MyComponent ref={ref} {...props} />; //返回值是类式组件实例
});

// 使用 ForwardedComponent
const App = () => {
  const myRef = React.createRef(); //成功取到了子组件(类式组件MyComponent)的实例对象

  return <ForwardedComponent ref={myRef} />;
};

构建 forwardRef 功能的高阶组件

使用forwardRef可以确保在包装组件时,ref 被正确地传递给内部组件。这对于构建接受 ref 并将其传递给内部 DOM 节点或其他组件的 HOC 非常有用。

下面是一个使用forwardRef构建 HOC 的示例:

jsx
import React, { forwardRef } from "react";

// 高阶组件
const withForwardedRef = (WrappedComponent) => {
  // forwardRef接收一个函数作为参数
  // 函数的第二个参数ref会被传递进来
  const WithForwardedRef = (props, ref) => {
    // 将ref转发给内部组件
    return <WrappedComponent {...props} ref={ref} />;
  };

  // 使用forwardRef将ref传递给内部组件
  return forwardRef(WithForwardedRef);
};

// 原始组件
const MyComponent = (props, ref) => {
  // 使用ref在组件中执行操作
  // ...

  return <div {...props}>Hello, World!</div>;
};

// 使用HOC包装组件
const MyWrappedComponent = withForwardedRef(MyComponent);

// 使用被包装的组件MyWrappedComponent
const App = () => {
  const myRef = React.createRef();

  return <MyWrappedComponent ref={myRef} />;
};

在上面的示例中,withForwardedRef是一个接受一个组件作为参数的高阶组件。它创建了一个新的函数组件WithForwardedRef,它接收两个参数(props 和 ref)。WithForwardedRef通过将 ref 传递给WrappedComponent,实现了将 ref 转发给内部组件的功能。

通过使用forwardRef将 ref 传递给内部组件,可以确保最终使用MyWrappedComponent时,ref 将正确地传递到MyComponent组件中。

forwardRef 的真实使用场景

引用传递(Ref forwading)是一种通过组件向子组件自动传递 引用ref 的技术。对于应用者的大多数组件来说没什么作用。但是对于有些重复使用的组件,可能有用。

例如某些input组件,需要控制其focus,本来是可以使用ref来 控制,但是因为该input已被包裹在组件中,这时就需要使用Ref forward来透过组件获得该input的引用。可以透传多层