react/基础知识

23 阅读12分钟

一、学习资料

官网;

30分钟精通React Hooks;

9个hooks的使用详解

react-redux

react-dom-router6

react框架面试题

二、快速配置开发环境

webpack/3、配置ts与react环境

三、React基础

react的生命周期

image.png

挂载阶段

组件在这个阶段只会执行一次,以下的api是按时间顺序执行的

constructor

  1. 用户初始化内部状态
  2. 将类方法的 this 绑定到类实例上

getDerivedStateFromProps

  1. 有可能造成组件内部的 state 被意外覆盖,根据 React 官方的建议,应谨慎使用这个方法。

render

  1. 使用jsx渲染dom

componentDidMount

  1. 获取渲染后的dom

更新阶段

当state、props、context这三个值任一一个改变都会触发这个阶段,以下的api是按时间顺序执行的

getDerivedStateFromProps

  1. 有可能造成组件内部的 state 被意外覆盖,根据 React 官方的建议,应谨慎使用这个方法。

shouldComponentUpdate

  1. 当返回值为false的时候,后面的api都不会执行了,也就是不会重新渲染
  2. 常用于性能优化
  3. 最容易出现bug的api

render

  1. 使用jsx渲染dom

getSnapshotBeforeUpdate

  1. 在本次更新真实 DOM 之前,你有一次访问原始 DOM 树的机会,就是这个生命周期方法

componentDidUpdate

  1. 组件完成更新时会调用这个方法

卸载阶段

组件在这个阶段只会执行一次

componentWillUnmount

  1. 当组件即将被从虚拟 DOM 中移除时触发

错误处理阶段

getDerivedStateFromError

componentDidCatch

这两个api当组件错误的时候会执行捕获,层层上递,知道遇到有组件定义这两个api,如果没有,则会报错。

context Api

image.png

hook

react16.8以上的版本才支持

Hooks是一套为函数组件设计的,用于访问 React 内部状态或执行副作用操作,以函数形式存在的 React API,常见的内部状态有 state、context、memo、ref

不编写 class 的情况下使用 state 以及其他的 React 特性。

hook的生命周期

componentDidMountcomponentDidUpdatecomponentWillUnmount三个生命周期合成一个

挂载阶段

useEffect(()=>{},[])  // `不返回函数`则是componentDidMount

useEffect(()=>{}) // `不返回函数`则是componentDidMount

更新阶段

useEffect(()=>{})  // `不返回函数`等于componentDidUpdate
useEffect(()=>{},[state1])  // `不返回函数`等于componentDidUpdate,只有state1更新才会执行,跟vue的watch类似。

卸载阶段

useEffect(()=>{return ()=>{}}) // `返回函数`则是componentWillUnmount这个生命周期

常见的hooks

image.png

注意

  • 第一,只能在 React 的函数组件中调用 Hooks
  • 第二,只能在组件函数的最顶层调用 Hooks。

useState

基本使用
// 定义
const [data, setData] = useState({a:1,b:2})
// 修改
setData(preData => ({...preData, b:3}))
作用
  1. 让视图跟随数据变化
  2. 存储数据
  3. 让useEffect钩子能够监听其数据变化
注意点
  1. 自动批处理,指的是多个useState在react合成一个执行,避免重复render,在react18之后,异步函数才会自动批处理,之前是没有的。
// 同步 ,调用了setCount和setCount1,只触发1次render,因为react内部自动批处理了
function syncState(){
    setCount(1)
    setCount1(2)
}
// 异步 ,调用了setCount和setCount1,会触发两次render
async function asyncState1(){
    await Promise.resolve(()=>{1})
    setCount(1)
    setCount1(2)
}

useEffect

基本使用
useEffect(()=>{},[data])
作用
  1. 监听组件的生命周期
useEffect(()=>{
    // componentDidMount
    return ()=>{
      // componentWillUnmount
    }
},[])

useEffect(()=>{
    // componentDidMount + componentDidUpdate
    return (preData)=>{
      // componentWillUnmount || data变化两次中的第一次preData
    }
},[data])
  1. 监听useState定义的数据变化
注意点

useRef

基本使用
const refEle = useRef()
作用
  1. 存储不需要数据驱动视图改变的数据,比如sdk实例、dom元素、普通数据
注意点
  1. 当数据的变化不需要作用于视图,则需要考虑用useRef。这是为了避免useEffect监听数据导致出现额外的函数执行。

useMemo[缓存值]

作用
  1. 常用于计算大量的数据时,缓存使用。跟vue的computed类似。
// 记忆化(Memoization),对于计算量大的函数,通过缓存它的返回值来节省计算时间,提升程序执行速度。
// 当a||b发生变化的时候,它才会执行
const memoized = useMemo(() => createByHeavyComputing(a, b), [a, b]);

useCallback[缓存函数]

作用

跟useMemo相同的作用,但是它返回的是一个函数。

const memoizedFunc = useCallback(() => {/*省略*/}, [a, b]);

等价于
// 当a||b发生变化的时候,它才会执行
const memoizedFunc = useMemo(() => () => {/*省略*/}, [a, b]);

// 只有当a||b的值发生变化的时候,它们的函数引用地址||值才会发生变化

useImperativeHandle

作用
  1. 父组件能够直接使用子组件的方法
    // 子组件
    import React, { useState, useImperativeHandle, forwardRef } from 'react'
    const IM = forwardRef((props, ref) => {
      const [msgList, setMsgList] = useState([])

      function onAddMsgItem() {
        setMsgList(preData => [...preData, 'a'])
      }
      useImperativeHandle(ref, () => ({
        onAddMsgItem,
      }));
      return (
        <div className={cs.live_im}>
          {
            msgList.map((item, index) => {
              return <div key={index}/>
            })
          }
        </div>
      )
    })
    export default IM

    // 父组件
    import React, { useState, useRef } from 'react'
    import IM from './componenst/IM'
    const Father = ()=>{
    const imEle = useRef();
      return (
        <div className={cs.wrapper}>
          <IM ref={imEle}></IM>
          <div onClick={() => imEle.current.onAddMsgItem()}>开始</div>
        </div>
      )
    }

useLayoutEffect

作用
  1. 与useEffect作用类似,但是里面的方法是同步的,它的执行顺序比useEffec优先
  2. 确保所有数据的更新在渲染页面之前已经完成,避免页面闪烁(重绘)
特点
  1. useEffect里面的方法的执行是异步的,useLayoutEffect是同步的。
  2. useLayoutEffect里面的执行会阻碍dom元素的渲染,但是它能确保dom的渲染是在其方法执行之后。不像useEffect会导致页面闪烁(重绘)。
import * as React from "react";
import "@/component/TheMain.less";
export function TheMain() {
  const [val, setVal] = React.useState('0')
  const [val1, setVal1] = React.useState('10')

  React.useLayoutEffect(() => {
    setVal('1')
    setVal1('2')
    
  }, []);
  // React.useEffect(() => {
  //   setVal('1')
  //   setVal1('2')
  // }, []);
  return <div className="TheMain">{val} {val1} </div>;
}

通过上面的代码:

注释useEffect,使用useLayoutEffect,页面直接渲染 1 2

注释useLayoutEffect,使用useEffect,页面渲染 0 10 再变成 1 2

useContext

作用
  1. 孙子组件直接读取爷爷组件定义的响应式数据,避免组件层层透传的麻烦。
// 大体思路:
// 爷爷组件声明需要透传的数据obj,
// 使用导出声明与使用React.createContext(obj),并被它包裹。
// 孙子组件导入ObjContext并使用React.useContext(ObjContext)获取数据obj。

// 爷爷组件
import * as React from "react";
import * as ReactDOM from "react-dom";
import { App } from "./App";
const obj = {
  value: 111111,
};
export const ObjContext = React.createContext(obj);

ReactDOM.render(
  <ObjContext.Provider value={obj}>
    <App />
  </ObjContext.Provider>,
  document.getElementById("root")
);

// 孙子组件
import * as React from "react";
import "@/component/TheMain.less";
import { ObjContext } from "@/main";
export function TheMain() {
  const obj = React.useContext(ObjContext)
  return <div className="TheMain">{obj.value}</div>;
}

自定义hooks

作用
  1. 当一个函数方法,频繁使用+使用到了hooks的相关方法。
import { useContext, useCallback } from 'react'
import { useNavigate } from 'react-router-dom'
import { Context, accountLogin } from '../store'

const useLogin = () => {
  const navigate = useNavigate()
  const { state, dispatch } = useContext(Context)
  const { loginInfo } = state

  const checkLogin = useCallback(() => {
    console.log('checkLogin', loginInfo)
    if (!loginInfo?.uid || !loginInfo?.sid) {
      navigate('/login')
      return false
    }
    return true
  }, [navigate, loginInfo])

  return { checkLogin }
}

export default useLogin

能触发 React 渲染(函数组件)的方法

propsstatecontext。只要这三种数据之一发生了变化,React 就会对当前组件触发协调过程,最终根据Diffing算法更改页面。

Diffing算法

image.png

协调

使用diff算法,找出变更的元素,渲染ui的过程被称为协调

setState

父组件setState,子组件也会重新渲染

React 的处理 render 的基本思维模式是每次一有变动就会去重新渲染整个应用
以最小的代价去更新 DOM,但是当dom树很大,
顶层的一个state微小变动也会出现vn比较性能的损耗

合成事件

合成事件是原生 DOM 事件的一种包装,它与原生事件的接口相同,根据 W3c 规范,React 内部规范化(Normalize)了这些接口在不同浏览器之间的行为,开发者不用再担心事件处理的浏览器兼容性问题。

合成事件与原生dom事件的区别

注册事件方式不同

// 原生的三种注册方式

<button id="btn" onclick="handleClick()">按钮</button>

document.getElementById('btn').onclick = handleClick;

document.getElementById('btn').addEventListener('click', handleClick);

// react注册方式
<button onClick={handleClick}>按钮</button>

特定事件的行为不同

React 合成事件规范化了一些在各个浏览器间行为不一致,甚至是在不同元素上行为不一致的事件,
其中有代表性的是 onChange 。

实际注册的目标 DOM 元素不同

//已知通过 evt.nativeEvent 属性,可以得到这个合成事件所包装的原生事件。
evt.target === evt.natveEvnet.target
evt.currentTarget != evt.natveEvnet.currentTarget

原因是因为React 使用了事件代理模式。React 在创建根( createRoot )的时候,
会在容器上监听所有自己支持的原生 DOM 事件。
当原生事件被触发时,React 会根据事件的类型和目标元素,找到对应的 FiberNode 和事件处理函数,
创建相应的合成事件并调用事件处理函数。

组件通信

数据流只能是单向的。

父传子

props

父传孙(透穿),兄弟组件互相通信

context

性能优化

纯展示的组件

没有用到state、props、context的组件可以使用React.memo进行封装,它在父组件的state更新时,不会重新渲染。

const PureComponent = React.memo(()=>{
  useEffect(()=>{
    console.log('纯组件,不更改')
  })
  return (
    <div>这是纯组件,不会改变的哦</div>
  )
})

缓存数据

useMemo

常用于计算大量的数据时,缓存使用。跟vue的computed类似。

// 记忆化(Memoization),对于计算量大的函数,通过缓存它的返回值来节省计算时间,提升程序执行速度。
const memoized = useMemo(() => createByHeavyComputing(a, b), [a, b]);

useCallback,跟useMemo相同的作用,但是它返回的是一个函数。

const memoizedFunc = useCallback(() => {/*省略*/}, [a, b]);

等价于
const memoizedFunc = useMemo(() => () => {/*省略*/}, [a, b]);

异步组件加载

如果需要加载一个异步组件,切记需要由Suspense包裹

import React, { Suspense, lazy } from 'react'
const Home = lazy(() => import('../views/Home/index'))
function ReactHoom(){
   <Suspense fallback="loading"> 
       <Home/>
   </Suspense>
}

代码复用

自定义hooks

  1. 当一个函数方法,频繁使用+使用到了hooks的相关方法。
import { useContext, useCallback } from 'react'
import { useNavigate } from 'react-router-dom'
import { Context, accountLogin } from '../store'

const useLogin = () => {
  const navigate = useNavigate()
  const { state, dispatch } = useContext(Context)
  const { loginInfo } = state

  const checkLogin = useCallback(() => {
    console.log('checkLogin', loginInfo)
    if (!loginInfo?.uid || !loginInfo?.sid) {
      navigate('/login')
      return false
    }
    return true
  }, [navigate, loginInfo])

  return { checkLogin }
}

export default useLogin

组件组合

<father>
    <son></son>
<father>

高阶组件

一个函数接受一个组件作为参数,经过一系列加工后,最后返回一个新的组件。

const EnhancedComponent = withSomeFeature(WrappedComponent);
//    -----------------   --------------- ----------------
//          |             ----    |               |
//          |              |      |               |
//          V              V      V               V
//       增强组件       (约定前缀) 高阶组件         原组件



const EnhancedComponent = withSomeFeature(args)(WrappedComponent);
//    -----------------   --------------- ----  ----------------
//          |                    |         |            |
//          |                    V         V            |
//          |                 高阶函数     参数           |
//          |             ---------------------         |
//          |                       |                   |
//          V                       V                   V
//       增强组件                 高阶组件               原组件

四、常用的三方工具库

1、react的全局状态管理

react-redux与redux-toolkit

react-redux是一个js状态管理库。 redux-toolkit则将react-redux与react这两个库进行绑定。

使用这些库的原因有两个:

  • 响应式管理全局数据
  • 内部自动实现了许多性能优化
  • redux-toolkit简化了react-redux的很多操作

相关资料

redux-toolkit官方文档

依赖安装

npm install @reduxjs/toolkit react-redux

工作流程

在store文件夹中,通过@reduxjs/toolkit提供的两个方法
`createSlice`:初始化状态树中模块state的初始化数据与修改state的方法
`createSlice.actions`: 导出修改state的方法,方便react组件使用
`configureStore`: 导出状态树

在根组件中,通过react-redux提供的方法
`Provider`:将状态树注入

在业务组件中,通过react-redux提供的方法
`useSelector`: 获取状态树的模块state值
`useDispatch`: 修改状态树的模块state值

基本使用,详情参考官方文档

pnpm i @reduxjs/toolkit  react-redux
initStore

src/store/index

import { createSlice, configureStore } from '@reduxjs/toolkit'


// 初始化store树中stateModule的初始数据state与修改state的方法reducers
export const initStateModule = createSlice({
    name: 'state',
    initialState: {
        loading: false,
        useInfo: {
            username: 'snyc'
        },
    },
    reducers: {
        syncActionSetUseInfo(state, action) {
            state.useInfo = action.payload
        },
        setLoading(state, action) {
            state.loading = action.payload
        },
    },
})

// 导出同步方法,方便react组件使用
export const {
    syncActionSetUseInfo,
    setLoading,
} = initStateModule.actions

// 导出异步方法
export const asyncActionSetUserInfo = (params = {}): any => {
    return async (dispatch, getState) => {
        dispatch(setLoading(true))
        console.log('params', params)
        try {
            await new Promise((resolve, reject) => {
                setTimeout(() => {
                    resolve(1)
                }, 3000)
            })
            dispatch(syncActionSetUseInfo(params))
        } catch (error) {
            console.error(error)
        }
        dispatch(setLoading(false))
    }
}
// 导出store
export default configureStore({
    reducer: {
        state: initStateModule.reducer
    }
})

将store注入根组件
import * as React from "react";
import * as ReactDOM from "react-dom";
import { App } from "./App";

// store相关
import { Provider } from "react-redux";
import { store } from "./store/index";

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById("root")
);

在组件中操作store
import * as React from 'react'
// 引入store相关
import { useSelector, useDispatch } from "react-redux";
import { syncActionSetUseInfo, asyncActionSetUserInfo } from 'src/pages/mobile/store'
interface Props {
}
interface State {
}
export default (props: Props) => {

  // 声明store相关
  const state = useSelector((states: any) => states.state) as any;
  const dispatch = useDispatch();
  const handleChangeSync = () => {
    dispatch(syncActionSetUseInfo({
      username: "hhhh"
    }))
  }
  const handleChangeASync = () => {
    dispatch(asyncActionSetUserInfo({
      username: "aaaa"
    }))

  }
  React.useEffect(() => {
    console.log('state', state)
  }, [state])
  return <div className={cs.wrapper}>
    <div onClick={handleChangeSync}>handleChangeSync</div>
    <div onClick={handleChangeASync}>handleChangeASync</div>
  </div>
}



2、react路由

react-router-dom

相关资料

官方资料react-dom-router6

基本使用,详情参考官方文档

路由的配置与渲染

/router/index.jsx

import { createBrowserRouter, RouterProvider, BrowserRouter, Routes, Route } from "react-router-dom";

export const router = createBrowserRouter([
  {
    path: "/",
    element: <Root />, // 当路由为“/ 渲染Root组件
    errorElement: <ErrorPage />, // 当路由从为“/”跳往其他没有配置的路由时,渲染ErrorPage组件
    children: [
      {
        index: true,    // 默认父路由渲染此子路由
        element: <Index /> 
      },
      {
        path: "/home",
        loader: asyncData,
        element: <Home />, // 当路由为“/home" 渲染Home组件
        children: [
          {
            path: "contacts/:contactId",
            loader: asyncDataViaParmas,
            element: <Contact />, // 路由为"/“的子路由为”contacts/:contactId“时,渲染 Contact 注意其父路由的组件 需要配置 <Outlet>来渲染子路由
          },
        ],
      },

      {
        path: "/login",
        element: <Login />,
      },
    ],
  },
]);

// RouterProvider,用来渲染已经配置完成的路由,通过组件的形式进行导出。
export const RouterRender = () => {
  return (
    <React.StrictMode>
      <RouterProvider router={router} />
    </React.StrictMode>
  );
};

======等价于下面这个=====

<BrowserRouter basename="/">
    <Routes>
        <Route
          key={'home'}
          path={'home'}
          element={<Home />} 
          replace />
            <Route
              key={'contacts/:contactId'}
              path={'contacts/:contactId'}
              element={<Contact/>}
            ></Route>
        </Route>
    </Routes>
</BrowserRouter>

/main.jsx

页面上的路由跳转

Link

import {
    Link,
  } from "react-router-dom";
// Link 进行路由跳转
<Link to="login">go to Login</Link>

NavLink

侧边栏导航的跳转,特点是ClassName为回调函数,可以通过它的参数获取跳转到下一个路由的状态显示

import {
    NavLink,
  } from "react-router-dom";
// Link 进行路由跳转

<NavLink
  key={index}
  to={item.to}
  className={({ isActive, isPending }) =>
    isActive ? "active" : isPending ? "pending" : ""
  }
>
  {item.to}
</NavLink>
子路由的渲染

Outlet

import {
    Outlet,
  } from "react-router-dom";
  
export default function Home() {
  return (
    <div>
        <Link to="home/children">home/children</Link>
        <Outlet></Outlet>
    </div>
  );
}
进入路由之前,加载异步数据
  1. 使用useLoaderData与在路由配置中配置loader
  2. 获取数据的函数可以通过参数获取进入的url上的参数params
// 可以通过params获取想要跳转的路由参数
export async function asyncData({params}) {
  const contacts = await new Promise((res, rej) => {
    setTimeout(() => {
      res([1, 2, 2, 3, 4, params.contactId]);
    }, 3000);
  });
  return { contacts };
}
export const router = createBrowserRouter([
  {
    path: "/",
    loader: asyncData, // 进入路由前需要加载的异步数据
    element: <Home />
  }
]);


import {
    useLoaderData,
  } from "react-router-dom";
export default function Home() {
  const { contacts } = useLoaderData() as any;
  return (
    <div>
        {
          contacts.toString()
        }
    </div>
  );
}
重定向

从A 跳往 B, B 重定向到 C, 此时路由回退, 回退到 A 而不是 B。

redirect

import { redirect } from "react-router-dom";
redirect('/login')
页面跳转的状态判断
import {
  useNavigation,
} from "react-router-dom";
const navigation = useNavigation();

navigation.state // loading 代表页面正在跳转中

3、keepAlive

保留组件活性。

react-activation

相关资料

React Activation官方文档