【React不全记录】- React底层知识点汇集

87 阅读12分钟

说在前面的事

React 16.8 版本正式发布了 Hook 机制,所以在设计到React的一些知识点的时候会有 Class ComponentFunction Component 的区别。

对 React 的理解

  • 用于构建界面的JavaScript库
  • 使用虚拟DOM来有效操作DOM
  • 一切皆为组件
  • 遵循自顶向下的单项数据流

生命周期

Class Component 生命周期

React 生命周期指的是从创建到卸载的整个过程,每个过程都有对应的钩子函数会被调用。主要有以下几个阶段:

  • 挂载阶段 - 组件实例被创建和插入 DOM 树的过程
  • 更新阶段 - 组件被重新渲染的过程
  • 卸载阶段 - 组件从 DOM 树被销毁的过程

说在前面的事

React 16.3 版本开始,对生命周期钩子函数进行渐进式的调整。分别废弃和新增了一些钩子函数。

新旧生命周期对比

Old lifecycle

3109554976-19337515b7f385d3_fix732.png

挂载

  • constructor
  • componentWillMount
  • render
  • componentDidMount

更新

  • componentWillReceiveProps
  • shouldComponentUpdate
  • componentWillUpdate
  • render
  • componentDidUpdate

卸载

  • componentWillUnmount
New lifecycle

2890520933-4161ca1f5e38726e_fix732.png

挂载

  • constructor
  • getDerivedStateFromProps
  • render
  • componentDidMount

更新

  • getDerivedStateFromProps
  • shouldComponentUpdate
  • render
  • getSnapshotBeforeUpdate

卸载

  • componentWillUnmount

从以上生命周期的对比,可以看出 React 16.3 开始,

废弃了 - componentWillMountcomponentWillReceivePropscomponentDidUpdate 三个钩子函数。

新增了 - getDerivedStateFromPropsgetSnapshotBeforeUpdate 两个钩子函数

废弃原因

在filber机制下,生命周期分为render和commit阶段。render时是可以被打断的、重新渲染并终止的。

当一个任务执行到一半被打断后,下一次渲染线程抢回主动权时,这个任务被重启的形式是 重复执行一遍整个任务 而非 接着上次执行到的地方继续执行

你会发现废弃的三个钩子函数都是在render之前,所以可能因为render重启而导致这三个钩子函数多次执行。

constructor()

constructor()早组件挂载之前被调用 作用:初始化props,初始化state,绑定this。

  • 若类组件只需要初始化props,则 constructor 可以不写。
  • 若还做了别的事,则 constructor 不能省略。
super的作用:将父类的this对象继承给子类
class Welcome extends React.Component {
    /*
        如果没有额外的代码,这三行可省略
        不写也会默认初始化 props 的数据
    */
    constructor(props) {
        super(props); // 初始化props
    }
}                 

class Welcome extends React.Component {
    constructor(props) {
        super(props);
        this.state = { // 初始化state
            count: 0;
        }
    }
}

static getDerivedStateFromProps(nextProps, state)

getDerivedStateFromProps() 在调用 render方法之前调用,在初始化和后续更新都会被调用。

/*
    参数: 
        第一个参数为即将更新的 `props`, 第二个参数为上一个状态的 `state`。
        可以比较 `props` 和 `state`来加一些限制条件,防止无用的state更新
        
    返回值:
        返回一个对象来更新 `state`, 如果返回 `null` 则不更新任何内容
*/
static getDerivedStateFromPorps (nextProps,prevState){
 if(nextProps.tab!=prevState.tab){
   return {
    tab:nextProps.tab
   };
 }
 return null;
}

render()

render() 方法是class组件中唯一必须实现的方法,用于渲染dom,返回一个虚拟DOM对象

这个虚拟DOM对象只能有一个根元素。如果需要返回两个根元素,需要使用<React.Fragment>包起来。(<React.Fragment>可以简写成<></>。这个包裹元素的标签只是占位符,并不会被渲染到页面上。)

// 一个根元素
render() {
    return {
        <div></div>
    }
}

// 两个根元素
render() {
    return {
        <>
            <div></div>
            <div></div>
        </>
    }
}

componentDidMount()

componentDidMount() 在组件挂载后 (插入DOM树后) 立即调用,此时组件已经出现在页面(操作依赖的DOM最好在此处进行)

componentDidMount() 是发送网络请求、启用事件监听方法的好时机,并且可以在 此钩子函数里直接调用 setState()

【问题】为什么数据获取要在componentDidMount中进行?

【参考】react.js - 为什么废弃react生命周期函数?

shouldComponentUpdate(nextProps, nextState)

shouldComponentUpdate() 在组件更新之前调用,可以控制组件是否进行更新, 返回true时组件更新, 返回false则不更新。

注意

  • 不建议在 shouldComponentUpdate() 中进行深层比较或使用 JSON.stringify()。这样非常影响效率,且会损害性能。
  • 不建议在 shouldComponentUpdate() 中进行深层比较或使用 JSON.stringify()。这样非常影响效率,且会损害性能

getSnapshotBeforeUpdate(prevProps, prevState)

在最近一次的渲染输出被提交之前调用。也就是说,在 render 之后,即将对组件进行挂载时调用。

componentDidUpdate(prevProps, prevState, snapshot)

参考:深入详解React生命周期 - 掘金 (juejin.cn)

componentWillUnmount()

组件销毁时调用

注意

  • 如果你在 componentDidMount 里面监听了 window scroll , 那么你就要在 componentWillUnmount里面取消监听
  • 如果你在 componentDidMount 里面创建了 Timer, 那么你就要在 componentWillUnmount 里面取消 Timer
  • 如果你在 componentDidMount里面创建了AJAX请求, 那么你就要在 componentWillUnmount里面取消请求

Function Component 生命周期

Function Component中摒弃了生命周期函数这个概念,改称为Hooks

React Hooks 是 React 16.8 引入的新特性。是一组使函数组件能够拥有转态和其他React特性的API,可以在不编写类组件的情况下使用React的功能。

React Hooks 包括以下API:

  • useState:添加状态
  • useEffect:添加副作用
  • useContext:访问context
  • useReducer:管理复杂的状态
  • useCallback:缓存回调函数,避免不必要的重新渲染
  • useMemo:缓存值,避免不必要的重新计算
  • useRef:存储可变的值

useState

useState 用于在函数组件中调用给组件添加一些内部状态 state。

import React, { useState } from 'react';
const [state, setState] = useState(initialState);

useEffect

useEffect 接收一个包含命令式、且可能有副作用代码的函数。 useEffect函数会在浏览器完成布局和绘制之后,下一次重新渲染之前执行,保证不会阻塞浏览器对屏幕的更新。

import React, { useEffect } from 'react';
useEffect(() => {});

清除effect 通常情况下,组件卸载时需要清除 effect 创建的副作用操作,useEffect Hook 函数可以返回一个清除函数,清除函数会在组件卸载前执行。组件在多次渲染中都会在执行下一个 effect 之前,执行该函数进行清除上一个 effect

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

const Counter = () => {
    const [count, setCount] = useState(0);
    
    useEffect(() => {
        const timer = setInterval(() => {
            setCount(count + 1)
        }, 1000);
        
        // 返回一个清除函数,在组件卸载和洗衣歌effect执行前执行
        return () => {
            clearInterval(timer);
        }
    }, [])
}

export default Counter

useEffect可以接收第二个参数。它是 effect 所依赖的值数组,这样就只有当数组值发生变化才会重新创建订阅。但需要注意的是:

  • 确保数组中包含了所有外部作用域中会发生变化且在 effect 中使用的变量
  • 传递一个空数组作为第二个参数可以使 effect 只会在初始渲染完成后执行一次

useContext

Context 提供了一个无需为每层组件手动添加 props ,就能在组件树间进行数据传递的方法,useContext 用于函数组件中订阅上层 context 的变更,可以获取上层 context 传递的 value prop 值

useContext 接收一个 context 对象(React.createContext的返回值)并返回 context 的当前值,当前的 context 值由上层组件中距离当前组件最近的 <MyContext.Provider>value prop 决定

import React, { useState, useContext } from 'react'

const themes = {
    dark: {
        background: '#000000'
    },
    light: {
        background: '#ffffff'
    }
}
// 为当前的theme创建一个context
const themeContext = React.useContext();

export default function parentComp() {
    const [theme, setTheme] = useState(themes.dark);
    
    const changeTheme = () => {
        setTheme(currentTheme => {
            currentTheme === theme.dark ? theme.light : theme.dark
        })
    }
    
    return (
        // 使用 Provider 将当前 props.value 传递给内部组件
        <ThemeContext.Provider value={{theme, changeTheme}} >
            <ChildComp />
        </themeContext.Provider>
    )
}

function ChildComp() {
    // 通过 useContext 获取当前 context 值
    const { theme, changeTheme } = useContext(themeContext);
    return (
        <button style={{background: theme.background}} onClick={changeTheme}>
            change theme
        </button>
    )
}

useReducer

useReducer 作为 useState 的代替方案。在 state 逻辑较复杂且包含多个子值,或下一个 state 依赖 之前的 state 等。

const [state, dispatch] = useReducer(reducer, initialArg, init);
import React, { useReducer } from 'react'

const initialState = { count: 0 };

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

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

  return (
    <>
      <p>Count: {state.count}</p>
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
    </>
  );
}

useRef

  • 绑定DOM元素
  • 绑定可变值

useCallback

useCallback 结合 React.memo() 是性能优化常见的方式。可以避免由于父组件状态变更,导致不必要的子组件进行重新渲染。

import React, { useState, useCallback } from 'react';
import Button from './Button';

export default function App() {
    const [count1, setCount1] = useState(0);
    const [count2, setCount2] = useState(0);

    const handleClickButton1 = () => {
      setCount1(count1 + 1);
    };

    const handleClickButton2 = useCallback(() => {
      setCount2(count2 + 1);
    }, [count2]);

    return (
    <div>
        <Button onClickButton={handleClickButton1}>Button1</Button>
        <Button onClickButton={handleClickButton2}>Button2</Button>
    </div>
    );
}
// Button.jsx
import React from 'react';

const Button = ({ onClickButton, children }) => {
    return (
        <>
              <button onClick={onClickButton}>{children}</button>
              <span>{Math.random()}</span>
        </>
    );
};

export default React.memo(Button);
  • 当点击Button1时,只会更新Button1组件的内容
  • 当点击Button2时,两个按钮组件都会被更新

这里或许会注意到 Button 组件的 React.memo 这个方法,此方法内会对 props 做一个浅层比较,如果如果 props 没有发生改变,则不会重新渲染此组件。

对于 Button 组件都需要一个onClickButton的属性,这个属性直接定义为一个方法,每当父组件重新渲染时,这里就会声明出一个新的方法。所以,尽管子组件使用了 React.memo() 还是会重新渲染

对于 Button2 而言,使用 useCallback 进行包装,并且传入了 count2 变量。这就意味着,会根据 count2 是否改变,来决定这里的函数是否被重新声明。

useMemo

useCallback 有异曲同工之妙,针对传入子组件的值进行缓存优化

// ...
const [count, setCount] = useState(0);

const userInfo = useMemo(() => {
  return {
    // ...
    name: "Jace",
    age: count
  };
}, [count]);

return <UserCard userInfo={userInfo}>

上面的代码,只有当 count发生改变之后,才会返回新的对象。

但是 useMemo 的应用不止于此,应用范围要比 useCallback 要广泛得多。可以把一些耗时的计算逻辑放到 useMemo 中,只有当依赖值发生改变时,才会重新去执行这个计算逻辑。

当把返回值改成返回函数时,变通的实现了 useCallback

useCallback 与 useMemo 的对比

  • 缓存
    • useCallback缓存的是函数
    • useMemo缓存的是函数的返回值
  • 组件优化
    • useCallback是优化子组件的
    • useMemo既可以优化当前组件又可以优化子组件。
      • 优化当前组件主要依靠通过缓存一些复杂的计算逻辑

useLayoutEffect - todo

哪个生命周期发送异步请求

  • componentDidMount。

参考这个 react.js - 为什么废弃react生命周期函数?

JSX 与 JS

JSX 全称 Javascript XML, 是react定义的一种JS的扩展语法。

浏览器不能识别JSX语法,需要通过babel转义成JS。

基本规则:

  • 遇到 < 开头的代码, 以标签的语法解析: html同名标签转换为html同名元素, 其它标签需要特别解析
  • 遇到以 { 开头的代码,以JS语法解析: 标签中的js表达式必须用{ }包含
  • 样式的类名指定不要用class,要用className,内联样式,要用style={{key:value}}的形式去写
  • 标签首字母,若是小写字母开头,则将标签转为html中同名元素,若html中无该标签对应的同名元素,则报错;若大写字母开头,react就去渲染对应的组件,若组件没有定义,则报错。

在React中,元素和组件有什么区别

  • 元素是页面中DOM元素的对象表示方式
  • 组件是一个函数或者一个类

state和props的区别

  • state 是组件自己管理的数据,控制自己的状态,可变
    • 没有 state 的叫无状态组件,有 state 的叫有状态组件
  • props 是外部传入的数据,不可变。

props变动,是否会引起 state hook 中数据的变动

React组件的 props 更新,会重新渲染组件,但是不会引起 state 数据的变动。state 的值只能由 setState 控制。

若想在 props 更新时触发对应的 state 更新,则需要在 useEffect 中监听 props,并在函数中对 state 进行操作

const App = props => {
    const [count, setCount] = useState(0);
    
    useEffect(() => {
        setCount(0)
    }, [props])
}

React组件的通信方式

这个问题也可以换一种方式提问:数据如何在React组件中流动?

参考:构建一个react项目 - react的通信方式 - 掘金 (juejin.cn)

React18 的新变化

  • 并发渲染机制
    • 根据用户的设备性能和网速对渲染过程进行适当的调整, 保证 React 应用在长时间的渲染过程中依旧保持可交互性,避免页面出现卡顿或无响应的情况,从而提升用户体验。
  • 新的创建方式
    • 先通过 ReactDOM.createRoot() 创建一个 root 节点,然后该 root 节点来调用 render() 方法
    • 以前的是直接通过 ReactDOM.render()方法创建
    // 新的创建方式
    const root = ReactDOM.createRoot(rootElement); 
    root.render(<App />);
    
    // 旧的创建方式
    ReactDOM.render(rootElement, <App />);
    
  • 自动批处理优化
    • 批处理:React 将多个状态更新分组到一个重新渲染中以获得更好的性能。
    • 在 V18 以前,只能在事件处理函数中实现批处理。在 v18 中,所有更新都将自动批处理。

并发模式如何执行的

React 中的并发,并不是指同一时刻同时在做多件事情。因为 js 本身就是单线程的(同一时间只能执行一件事情),而且还要跟 UI 渲染竞争主线程。

若一个很耗时的任务占据了线程,那么后续的执行内容都会被阻塞。

为了避免这种情况,React 就利用 fiber 结构和时间切片的机制,将一个大任务分解成多个小任务,然后按照任务的优先级和线程的占用情况,对任务进行调度。

  • 对于每个更新,为其分配一个优先级 lane,用于区分其紧急程度。
  • 通过 Fiber 结构将不紧急的更新拆分成多段更新,并通过宏任务的方式 将其合理分配到浏览器的帧当中。这样就能使得紧急任务能够插入进来。
  • 高优先级的更新会打断低优先级的更新,等高优先级更新完成后,再开始 低优先级更新。

受控组件和非受控组件

  • 受控组件:受 React 控制的组件。当组件状态发生变化时,都会通知 React 进行处理,比如可以使用 useState 存储
  • 非受控组件:如 input/textarea/select/checkbox等组件,本身就能控制数据和状态的变更,数据存储在 DOM 中而不是组件内。可以使用 ref从DOM中获取元素数据
const App = () => {
    const [inputVal, setInputVal] = useState(0);
    
    const eleRef = useRef(null);
    
    const handleValue = () => {
        console.log(eleRef.current?.value)
    }
    return (
        <>
            // 受控组件
            <input value={inputVal} onChange={(e) => {
                setInputVal(e.target.value)
            }}/>
            
            // 非受控组件
            <input defaultValue='0' ref={eleRef}/>
            <button onClick={handleValue}>点击提交</button>
        </>
    )
}

React事件机制

  • React 所有事件都挂载在 document 对象上 (减少内存开销就是因为所有的事件都绑定在 document 上,其他节点没有绑定事件)
  • 当真实 DOM 元素触发事件,会冒泡到 document 对象后,再处理 React 事件
  • 会先执行原生事件,然后处理 React 事件
  • 最后真正执行 document 上挂载的事件

React中的Key

而元素key属性的作用是用于判断元素是新创建的还是被移动的元素,从而减少不必要的元素渲染

虚拟DOM和Diff算法的理解

参考:blog.csdn.net/m0_52409770… vue3js.cn/interview/R…

其中有一点要特别提醒的

1. 数据更改 -> 虚拟DOM -> 更新界面
2. 数据更改 -> 更新界面 

/*
    为什么多一步转化为 虚拟DOM 这个操作,看起来多了一步的消耗。
    其实在转化为 虚拟DOM 时用了 Diff算法 【虚拟DOM算法 = 虚拟DOM + Diff算法】
*/

使用虚拟DOM算法的耗损计算:总耗损 = 虚拟DOM增删改 + 真实DOM差异增删改(与Diff算法效率有关) + 回流与重绘(较少节点)
直接操作真实DOM的耗损计算:总耗损 = 真实DOM完全增删改 + 回流与重绘(可能较多节点)

Diff三个层级的比较:

  • Tree层级
  • Component层级
  • Element层级
- Tree diff
1. 进行树结构的层级比较,对同一个父节点下的所有子节点进行比较
2. 看节点是什么类型
    2.1. 组件类型 -> 进行Component diff
    2.2. 元素类型 -> 进行Element diff

- Component diff
1. 若组件类型相同,则继续进行 Tree diff
2. 若组件类型不同,则替换整个组件的内容

- Element diff
1. 节点是原生标签,则看标签名名称是都相同
    1.1. 相同,递归进行 Tree diff
    1.2. 不同,进行替换

Diff算法中Key值问题

参考:juejin.cn/post/702653…

关于React的性能优化

参考:React 性能优化的手段