自本文开始,我们将逐步介绍react的必要概念,以备忘录。

react带来了利?

    1. 声明式编码,一种编程范式,关注的是你要做什么,而不是如何做。
    1. react-native中,使用js开发移动端应用
    1. 虚拟dom+优秀的diff算法,尽量减少与真实dom的交互
    1. 组件化开发。基本标准:可组合、可维护、可复用

基础概念

React仅仅是一个UI库。官方对React的定义为:

用于构建用户界面的 JavaScript 库。
其根基思想就是 数据驱动视图

整体逻辑如下:

jsx语法

JavaScript 中夹杂着 HTML 的语句在其中,称之为jsx语法,它是对 JavaScript 语法的扩展,本质是React.createElement的语法糖。其中的React.createElement做的事情很清晰,他有三个参数type、config和children。顾名思义,分别代表节点类型如div、节点所有属性如className和节点的子节点。就是说以jsx文件代码为输入,编译生成虚拟dom,然后通过render方法生成真实的dom节点。

为了生成虚拟dom,两种写法

// jsx
const element = <h1 className="title">Hello, React</h1>;

// React.createElement
const element = React.createElement(
  'h1',                // 标签名/组件
  { className: 'title' },  // 属性(props)
  'Hello, React'       // 子元素(children)
);

渲染逻辑

贴一张react的渲染逻辑图。

条件渲染,类似于vue中的v-if,jsx 中的写法如下:

const Example = () => {
  // 条件判断,随机显示男女
  const greater = Math.random() * 10 > 5;
  return (
    {greater > 5 ? (
      <div style={{ color: 'green' }}>我是男生</div>
    ) : (
      <div style={{ color: 'red' }}>我是女生</div>
    )}
  );
}

React组件

import React from 'react';
import ReactDOM from 'react-dom/client';
// 定义 App 组件 props 的类型
type Props = {
  name?: string;
}
// 定义一个叫App 的组件
function App(props: Props) {
  return (
    <div className="App">
      <h1 style={{ color: 'red', textAlign: 'center' }}>
        Hello {props.name || '未知名字'}
      </h1>
    </div>
  );
}
// 找到组件将要渲染的 html tag 位置(使一个 id ="root" 的标签)
const root = ReactDOM.createRoot(document.getElementById('root')!);
root.render(
  <React.StrictMode>
    {/*大家关注下面的一行,渲染上面写的组件App */}
    <App name={"xx"} />
  </React.StrictMode>
);

export default App;

上面的代码中,我们写了一个函数组件,搭配ts定义了该组件的Props类型。

react中的组件有两种方式:函数组件和类组件,目前公司业务普遍选择前者。
两者的区别

  1. 两者作为组件一致,
  2. 类组件的根基时oop,面向对象编程,函数组件的根基时fp,即函数式编程,前者有this,后者无;前者可以访问生命周期方法,后者不能。
  3. 类组件通过shouldComponentUpdate阻断渲染,函数组件通过React.memo
  4. 组合优于继承,函数组件低耦合逻辑代码包括生命周期,更加的灵活。

几个函数组件的错误案例:


type Props = { // 不了解ts的话,可以忽略下面的Props
  name?: string;
}
// 变量声明写在了return中
function App(props: Props) {
  return (
    const tmp = '123';
    <div className="App">
      <h1 style={{ color: 'red', textAlign: 'center' }}>
        Hello {props.name}
      </h1>
    </div>
  );
}
// 函数写在了return中
function App(props: Props) {
  return (
    function tmp() {};
    <div className="App">
      <h1 style={{ color: 'red', textAlign: 'center' }}>
        Hello {props.name}
      </h1>
    </div>
  );
}
// 同1
function App(props: Props) {
  return (
    <div className="App">
      <h1 style={{ color: 'red', textAlign: 'center' }}>
        Hello {props.name}
      </h1>
    </div>
  );
  // 这个跟其他语言一样,return 之后不会执行
  const a = 123;
}

更进一步的看个样例,让页面交互起来:

import React, { useState } from 'react';
import ReactDOM from 'react-dom/client';

type Props = {
  name?: string;
}
function App(props: Props) {
  return (
    <div className="App">
      <h1 style={{ color: 'red', textAlign: 'center' }}>
        Hello {props.name || '未知名字'}
      </h1>
    </div>
  );
}
type CounterProps = {
  start?: number;
}
const Counter = (props: CounterProps) => {
  // 设置内部状态的初始值,初始值是外部传进来的,当然,如果不传,那就使用默认值 0
  const [count, setCount] = useState(props.start || 0);
  const plus = () => {
    // 更新数据
    setCount(count + 1);
  }
  // return 返回的就是 UI,也是所见即所得,你看到的 dom 结构就是页面渲染后看到内容
  return (
    <div style={{ textAlign: 'center' }}>
      Count: {count}
      <button onClick={plus}>点我加1</button>
    </div>
  );
}
// 找到组件将要渲染的 html tag 位置(使一个 id ="root" 的标签)
const root = ReactDOM.createRoot(document.getElementById('root')!);
root.render(
  <React.StrictMode>
    {/*大家关注下面的一行,渲染上面写的组件 */}
    <App name={"xx"} />
    <Counter start={10} />
  </React.StrictMode>
);

export default App;

实际就是一个计数器,点击++。但我们引入了新的内容 useState,说白了就是组件内部有了状态变量。useState就是所谓的 是 react hooks 中的一种。
根据有无状态,组件可以分为两种:简单组件(simple component) 和 有状态组件(stateful component)。

Hook

hook的本质,就是对逻辑的抽象。拿一个组件显隐的功能举例:

原始版本:

import React, { useState, useRef, useEffect } from 'react'
import './style.css'
interface IProps {
  uids: Array<number>
}

export default function ComputerComponent(props: IProps) {
  const [show, setshow] = useState(true)
  const handleClick = () => {
    setshow(!show)
  }
  return (<div>
    { show && <div className='test'>我是div</div>}
    <button onClick={handleClick}>toggle</button>
  </div>) 
}

效果:
图片描述

抽象化之后:

import React, { useState, useRef, useEffect } from 'react'
import './style.css'
interface IProps {
  uids: Array<number>
}
function useShow() {
  const [show, setshow] = useState(true)
  const handleClick = () => {
    setshow(!show)
  }
  return {
    show,
    setshow,
    handleClick
  }
}
export default function ComputerComponent(props: IProps) {
  const { show, handleClick, setshow } = useShow()
  return (<div>
    { show && <div className='test'>我是div</div>}
    <button onClick={handleClick}>toggle</button>
  </div>) 
}

这种抽象,就是自定义hook,下面介绍几个常用的官方hook

useState

import React, { useState } from 'react';
function Example() {
  // 声明一个叫 “count” 的 state 变量。
  const [count, setCount] = useState(0);
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

在这里,useState 就是一个 Hook (通过在函数组件里调用它来给组件添加一些内部 state。React 会在重复渲染时保留这个 state。useState 会返回一对值:当前状态和一个让你更新它的函数,你可以在事件处理函数中或其他一些地方调用这个函数。你可以简单把它理解成调用这个函数会更新 state 的状态,然后这组件重新渲染。在上面的例子中,我们的计数器是从零开始的,所以初始 state 就是 0。值得注意的是,这里的 state 不一定要是一个对象,可以是任意值。这个初始 state 参数只有在第一次渲染时会被用到。

你可以在一个组件中多次使用 State Hook:

function ExampleWithManyStates() {
  // 声明多个 state 变量!
  const [age, setAge] = useState(42);
  const [name, setName] = useState('chaochao');
  const [friends, setFriends] = useState([{ name: 'lulu' }]);
  // ...
}

注意:
state的变量不能直接修改,这是规则

useEffect
这个 hook 的核心作用就是在组件渲染完毕之后,你想做点别的事情(我们统一把这些别的事情称为副作用)。
比如你想渲染完之后立即进行数据获取、事件订阅或者手动修改过 DOM,这些都是副作用,都可以在 useEffect 中执行。
useEffect 就是一个 Effect Hook,给函数组件增加了操作副作用的能力。它跟 class 组件中的 componentDidMount(组件第一次渲染结束后触发)、componentDidUpdate(组件每次更新结束后触发) 和 componentWillUnmount(组件将要卸载的时候触发) 具有相同的用途,只不过被合并成了一个 API。
即 useEffect 可以根据参数的不同配置,在组件不同的渲染时机被调用。useEffect 接受两个参数:副作用函数,依赖项,类型是数组

// 依赖项是空数组,仅仅挂载和卸载时执行
useEffect(() => {
    ...
},[]);

// 依赖项有值(不论个数),组件第一次渲染结束后,调用一次。后面检测到依赖发生变化的时候,自动调用,每变一次调用一次
useEffect(() => {
    ...
},[依赖1,依赖2]);

// 没有填依赖项,则组件每次渲染结束后,都调用一次,不限次数
useEffect(() => {
    ...
});

你可以在组件中多次使用 useEffect,每个 effect 关注自己的事情即可。

function FriendStatusWithCounter(props) {
  const [count, setCount] = useState(0);
  // 一个组件中可以使用多个useEffect,
  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });

  const [isOnline, setIsOnline] = useState(null);
  useEffect(() => {
    // 这里假设有个 ChatAPI 的服务
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

  function handleStatusChange(status) {
    setIsOnline(status.isOnline);
  }

关于useEffect查一个场景题:现在有一个页面,有三个子组件的div,分别是a、b、c,那如果我现在通过代码改变了他们的顺序,比如b、a、c,子组件中useEffect(() => { … }, [])会触发吗?
答案有两种,触发和不触发。useEffect(() => { … }, []) 只有在组件挂载和卸载时触发。那么根据这个问题有两种情况,a、b、c无key时,就会触发。因为react底层执行判断是否为同一组件的条件是:标签名和key。因为没有key,那么改变后的组件a、b、c都会被判定为是新的实例。如果有key,就不会触发。

useReducer
类似于useState,代码如下:

import React from "react"
import { useState, useReducer } from "react"
import { Button } from 'antd'

const testreducer = (state, action) =>{
  switch (action) {
    case "-":
      return state - 1
    case "+":
      return state + 1
  }
}
const AboutComponent = () => {
  const [count, dispatch] = useReducer(testreducer, 0)
  const handleClick = (type) => {
    dispatch(type)
    console.log('count>>>',count)
  }
  return (
    <div>
      <span>about</span>
      <Button onClick={ () => handleClick('-')}>--</Button>
      <span>{ count }</span>
      <Button onClick={() => handleClick('+')}>++</Button>
    </div>
  )
}

export default AboutComponent

个人感觉,其功能在于抽逻辑代码。

useRef
跟dom元素相关,例如我们需要等待页面渲染完dom后做一些其他的事情,如挂载canvas画布等,就可以这么玩:

import Me from '@/components/Me'
import { useEffect, useRef } from 'react';


export default function HomeComponent() {
  const child = useRef(null)

  useEffect(() => {
    if(child.current) {
      console.log('child>>>>', child)
    }

  }, [child])
  return <div ref={child} className="w-full h-full text-[50px] bg-gradient-to-t from-[#243b55] to-[#141e30]">
    <Me/>
  </div>;
}

useMemo
对函数值的缓存,只有当依赖的值(第二个参数)发生变化时,才会重新计算。避免重复计算,缓存计算结果。代码如下:

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

const TestPage = () => {
  const [number, setNumber] = useState(0);
  const [darkMode, setDarkMode] = useState(false);

  // 使用useMemo缓存计算结果
  // 只有当number变化时,才会重新计算
  const squaredNumber = useMemo(() => {
    console.log('进行了昂贵的计算...');
    // 模拟一个计算开销较大的操作
    let result = 0;
    for (let i = 0; i < 100000000; i++) {
      result = number * number;
    }
    return result;
  }, [number]); // 依赖数组,只有number变化时才重新计算

  // 切换主题的样式
  const themeStyles = {
    backgroundColor: darkMode ? 'black' : 'white',
    color: darkMode ? 'white' : 'black',
    padding: '20px',
    margin: '20px'
  };

  return (
    <div style={themeStyles}>
      <h1>使用useMemo优化计算</h1>
      <input
        type="number"
        value={number}
        onChange={(e) => setNumber(parseInt(e.target.value))}
      />
      <button onClick={() => setDarkMode(!darkMode)}>
        切换主题
      </button>
      <p>数字的平方: {squaredNumber}</p>
    </div>
  );
};

export default TestPage;

当我们点击 “切换主题” 按钮时,虽然组件会重新渲染,但由于 number 没有变化,useMemo 会直接返回缓存的结果,避免了不必要的计算.

每次切换主题都进行计算的版本

import React, { useState, useMemo, useEffect } from 'react';

const TestPage = () => {
  const [number, setNumber] = useState(0);
  const [darkMode, setDarkMode] = useState(false);

  // 直接在组件渲染过程中执行计算
  // 每次组件重新渲染时都会执行,包括切换主题时
  console.log('进行计算...');
  let result = 0;
  // 模拟一个计算开销较大的操作
  for (let i = 0; i < 100000000; i++) {
    result = number * number;
  }
  const squaredNumber = result;

  const themeStyles = {
    backgroundColor: darkMode ? 'black' : 'white',
    color: darkMode ? 'white' : 'black',
    padding: '20px',
    margin: '20px',
    minHeight: '200px'
  };

  return (
    <div style={themeStyles}>
      <h1>不使用useMemo和useEffect的情况</h1>
      <input
        type="number"
        value={number}
        onChange={(e) => setNumber(parseInt(e.target.value) || 0)}
        style={{ marginBottom: '10px', padding: '5px' }}
      />
      <button 
        onClick={() => setDarkMode(!darkMode)}
        style={{ padding: '5px 10px', marginBottom: '10px' }}
      >
        切换主题
      </button>
      <p>数字的平方: {squaredNumber}</p>
      <p>注意: 每次点击切换主题按钮都会触发重新计算</p>
    </div>
  );
};

export default TestPage;

useCallback
对函数方法的缓存,减少不必要的函数创建,优化性能

import React, { useCallback,useRef, forwardRef, useImperativeHandle, useState, useMemo, memo } from "react"
// import { useState, useReducer } from "react"
import { Button } from 'antd'

const Child = memo((props: any) => {
  const { onClick } = props
  console.log('zi组建更新', onClick)
  return <button onClick={onClick}>子组建按钮</button>
})

const AboutComponent = () => {
  const [count, setcount] = useState(0)
  const [value, setvalue] = useState('hhvcg')
  console.log('父组建更新')

  const handleClick = (type) => {
    console.log('执行>>>')
    setcount(count+1)
  }
  const handleClick2 = useCallback((type) => {
    setcount(count+1000)
  }, [])

  return (
    <div>
      <span>{ count }</span>
      <Button onClick={ handleClick }></Button>
      <Child onClick={ handleClick2 }/>
    </div>
  )
}

export default AboutComponent

配合memo使用

总结一下:usecallback、useMemo,useCallback主要用于避免在每次渲染时都重新创建函数,而useMemo用于避免在每次渲染时都进行复杂的计算和重新创建对象。useCallback返回一个函数,当依赖项改变时才会更新;而useMemo返回一个值,用于缓存计算结果,减少重复计算。

useLayoutEffect
同useEffect几乎一摸一样,但稍有些区别。官方建议: 大多数场景下直接使用useEffect,但代码引起页面闪烁就推荐使用useLayoutEffect处理。即:直接操作dom样式相关的使用后者。
useLayoutEffect是在所有dom变更之后同步调用。重点就在于这个同步,大量变动会引起阻塞,建议优先useEffect。

通信

  • react中的通信,同vue有点类似,子组建通过props获取父组建的值,但是因为reat是单向数据流,子组建无法直接修改父组建的值。所以子组建通过调用父组建的方法把值传过去
  • 无关组件之间传值,context,redux。其中context通常用于小型的项目,组件树中的传值,redux相比之则更适用于大型项目的全局状态管理。

useAsyncFn
无需手动管理 loading、error 等状态,简化异步操作的处理逻辑,同时还可以自动处理并发请求(新请求会取消旧请求)

const [loading, test] = useAsyncFn(async () => {
  const p = new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve("1");
    }, 3000);
  });
  p.then(res => {
    console.log("res>>", res);
  });
}, []);

注: 本文大量参考平台内部某同学的文章,请留意。

文毕