REACT 基础

1.react 生命周期

React 16 之前的 React 类组件生命周期包括三个阶段:挂载阶段、更新阶段和卸载阶段。React 16 及以后,引入了新的生命周期方法,并且在函数组件中使用了 React Hook,改变了生命周期的概念。以下是 React 类组件生命周期的主要方法:

挂载阶段

  1. constructor():组件的构造函数,通常用于初始化状态(state)和绑定事件处理方法。此方法仅在组件实例化时调用一次。
  2. static getDerivedStateFromProps(props, state)(React 16.3 新增):用于根据 props 的变化来更新 state,它必须返回一个对象来更新 state,或者返回 null 表示不需要更新 state。
  3. render():负责渲染组件的 UI,返回 React 元素。
  4. componentDidMount():在组件挂载到 DOM 后立即调用,通常用于进行一次性的 DOM 操作,网络请求等初始化工作。

更新阶段

  1. static getDerivedStateFromProps(props, state):同样也存在于更新阶段,用于根据 props 的变化来更新 state。
  2. shouldComponentUpdate(nextProps, nextState):决定是否重新渲染组件,返回 true 表示重新渲染,返回 false 表示阻止重新渲染,默认返回 true。
  3. render():重新渲染组件的 UI。
  4. getSnapshotBeforeUpdate(prevProps, prevState):在 render 之后、更新 DOM 之前被调用,用于获取当前的 DOM 状态或位置信息,通常与componentDidUpdate一起使用。
  5. componentDidUpdate(prevProps, prevState, snapshot):组件更新完成后被调用,通常用于进行 DOM 操作、网络请求等。

卸载阶段

  1. componentWillUnmount():在组件即将被卸载时调用,用于清理资源,取消网络请求、清除定时器等操作。

需要注意的是,React 16 之后,类组件中的生命周期方法仍然可用,但函数组件中使用了 Hook,这是一种更简洁的方式来处理组件的生命周期和副作用。

函数组件和类组件的生命周期方法不是完全一对一的替代关系,需要根据具体情况选择使用哪种组件类型和生命周期方法。

React 16 之后,以下三个生命周期方法被标记为“不建议使用”(deprecated),并在未来的版本中可能会被移除:

  1. componentWillMount():在组件即将被挂载到 DOM 前调用,通常用于在组件挂载前执行一些准备工作。由于它在render()前被调用,因此在此方法中执行setState不会触发额外的渲染。建议使用constructor或者componentDidMount来代替。

  2. componentWillReceiveProps(nextProps):在接收到新的 props 前被调用,用于根据新的 props 更新组件的状态。在 React 16.3 后,它被标记为不建议使用,推荐使用static getDerivedStateFromProps(props, state)来替代。

  3. componentWillUpdate(nextProps, nextState):在组件即将更新前调用,通常用于在更新前执行一些准备工作。不建议使用,推荐使用getSnapshotBeforeUpdate(prevProps, prevState)来代替。

虽然这些生命周期方法目前仍然可以使用,但 React 团队建议尽量避免在新代码中使用它们,以便保持代码的未来兼容性。同时,也推荐使用新的生命周期方法和 React Hook 来取代它们,以获得更清晰、简洁的代码结构。

需要特别注意的是,如果你使用了某个被废弃的生命周期方法,React 会在控制台中给出警告,提醒你尽快更换成推荐的方式。

2.react hooks

React 中的 Hooks 是 React 16.8 引入的一项新特性,它们使函数组件能够拥有类组件的一些功能,例如状态管理、生命周期等。以下是一些常用的 React Hooks:

2.1 useState

useState:用于在函数组件中添加状态。它返回一个包含当前状态和更新状态的数组。

import React, { useState } from "react";

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

  const increment = () => {
    setCount(count + 1);
  };

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

2.2 useEffect

useEffect:用于在函数组件中处理副作用,如订阅事件、发起网络请求、DOM 操作等。

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

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

  useEffect(() => {
    document.title = `Count: ${count}`;
  }, [count]);

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

2.3 useContext

useContext:用于在函数组件中访问 React 的 Context。

import React, { useContext } from "react";

const MyContext = React.createContext();

function MyComponent() {
  const value = useContext(MyContext);

  return <div>{value}</div>;
}

2.4 useReducer

useReducer:用于在函数组件中管理复杂的状态逻辑,类似于 Redux 的reducer

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();
  }
}

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

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

2.5 useMemo

useMemo:用于在渲染过程中执行一些开销较大的计算,并缓存计算结果。

import React, { useMemo } from "react";

function ExpensiveComponent({ a, b }) {
  const result = useMemo(() => {
    return a * b;
  }, [a, b]);

  return <div>{result}</div>;
}

2.5 useCallback

useCallback:类似于useMemo,用于缓存函数,避免在每次渲染时都重新创建函数。

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

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

  const handleClick = useCallback(() => {
    setCount(count + 1);
  }, [count]);

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

2.6 useRef

useRef 可以用于在函数组件中创建一个可变的对象,通常用于引用 DOM 元素或者保存持久化的值。

另外,useRef 的值在组件重新渲染时保持不变。

以下是一个简单的示例:

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

function TextInput() {
  const inputRef = useRef();

  useEffect(() => {
    inputRef.current.focus();
  }, []);

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

在上面的例子中,useRef() 创建了一个 ref 对象并将其赋值给 inputRef 变量。

然后,将 inputRef 赋值给 <input> 元素的 ref 属性。

useEffect 会在组件挂载后执行,它会调用 inputRef.current.focus(),使得 <input> 元素获得焦点。

useRef 除了用于引用 DOM 元素外,还可以用于保存组件中的持久化值,这些值在组件重新渲染时保持不变。例如:

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

function Example() {
  const countRef = useRef(0);

  useEffect(() => {
    countRef.current = count;
  }, [count]);

  const handleClick = () => {
    setCount(count + 1);
  };

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

在这个例子中,countRef 会在组件的生命周期中保持不变,它的值在组件重新渲染时不会重置。

countRef.current 可以访问到最新的 count 值。

2.7 useLayoutEffect

useLayoutEffect:类似于 useEffect,但在 DOM 更新前同步触发,通常用于执行与 DOM 相关的操作。

useLayoutEffect 是 React 提供的一个 Hook,它类似于 useEffect,但是会在 DOM 变更之后同步执行,

而不是在浏览器绘制之后执行。

useEffect 不同,useLayoutEffect 会在 DOM 更新前同步触发,因此在浏览器绘制之前执行。

这使得它在执行 DOM 相关的操作时非常有用,例如获取 DOM 元素的尺寸或位置等。

以下是一个简单的示例:

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

function MeasureBox() {
  const [height, setHeight] = useState(0);
  const [width, setWidth] = useState(0);

  const measureBox = () => {
    const { offsetHeight, offsetWidth } = boxRef.current;
    setHeight(offsetHeight);
    setWidth(offsetWidth);
  };

  const boxRef = React.createRef();

  useLayoutEffect(() => {
    measureBox();
    window.addEventListener("resize", measureBox);
    return () => {
      window.removeEventListener("resize", measureBox);
    };
  }, []);

  return (
    <div>
      <div
        ref={boxRef}
        style={{
          width: "200px",
          height: "200px",
          backgroundColor: "lightblue",
        }}
      >
        Box
      </div>
      <p>Height: {height}px</p>
      <p>Width: {width}px</p>
    </div>
  );
}

在上面的例子中,useLayoutEffect 中的 measureBox 函数会在 DOM 更新前同步执行, 这样我们可以获取到 boxRef.current 的高度和宽度,然后更新相应的状态。

此外,我们还监听了窗口的 resize 事件,以确保在窗口大小变化时能够重新计算元素的尺寸。

需要注意的是,由于 useLayoutEffect 会在 DOM 更新前同步执行,因此在性能上可能会比 useEffect 更慢。 因此,除非你确实需要在 DOM 更新前同步执行操作,否则应该优先考虑使用 useEffect

2.8 useImperativeHandle

useImperativeHandle可以用于在父组件中控制子组件的暴露接口。

通常在使用第三方 UI 组件库或者需要与 DOM 元素进行交互时会用到。

以下是一个简单的示例:

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

// 子组件
const ChildComponent = forwardRef((props, ref) => {
  const inputRef = useRef();

  useImperativeHandle(ref, () => ({
    focusInput: () => {
      inputRef.current.focus();
    },
    getValue: () => inputRef.current.value,
  }));

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

// 父组件
function ParentComponent() {
  const childRef = useRef();

  const handleFocusInput = () => {
    childRef.current.focusInput();
  };

  const handleGetValue = () => {
    const value = childRef.current.getValue();
    alert(`Input value is: ${value}`);
  };

  return (
    <div>
      <ChildComponent ref={childRef} />
      <button onClick={handleFocusInput}>Focus Input</button>
      <button onClick={handleGetValue}>Get Input Value</button>
    </div>
  );
}

export default ParentComponent;

在这个例子中,我们创建了一个子组件 ChildComponent,它接受一个 ref 作为参数。

在子组件内部,我们使用 useRef 创建了一个 inputRef,然后使用 useImperativeHandle 来定义需要暴露给父组件的接口。

useImperativeHandle 中,我们返回了一个对象,其中包含了 focusInputgetValue 两个方法。

这两个方法会在父组件中通过 childRef.current 来调用。

在父组件中,我们使用 useRef 创建了一个 childRef,然后将其传递给 ChildComponentref 属性。

这样,父组件就能够通过 childRef.current 来调用 focusInputgetValue 方法。

通过 useImperativeHandle,我们可以灵活地控制子组件的接口,只暴露需要给父组件使用的部分,提高了组件的封装性和可控性。

2.9 useLayoutEffect 与 useEffect 区别

useLayoutEffect 在 React 组件生命周期中,会在 DOM 更新之前同步触发。

具体来说,useLayoutEffect 会在浏览器执行绘制之前立即执行,这使得它在执行 DOM 相关的操作时非常有用,例如获取 DOM 元素的尺寸或位置等。

然而,由于 useLayoutEffect 在浏览器绘制之前同步执行,如果其中包含大量计算或阻塞操作,可能会导致性能问题,因此在使用时要慎重考虑。

需要注意的是,尽管 useLayoutEffect 在 DOM 更新前同步执行,但它的执行时机并不一定在所有组件的 render 阶段之后,因此可能会导致性能问题。因此,一般情况下,应该优先考虑使用 useEffect,只有在确实需要在 DOM 更新前同步执行操作时才使用 useLayoutEffect

useEffect 在 React 组件生命周期中是在浏览器执行绘制之后异步执行的。

具体来说,useEffect 中的回调函数会在组件的 DOM 渲染完成后才会执行。 这意味着在 useEffect 中可以安全地执行与 DOM 相关的操作,例如订阅事件、发起网络请求、操作 DOM 元素等。

需要注意的是,由于 useEffect 是异步执行的,它可能会导致在组件渲染期间产生一些视觉上的闪烁或者瞬时的不一致。因此,在编写 useEffect 时,要确保不会引起不必要的视觉干扰,可以考虑在组件初次渲染时显示加载状态,待 useEffect 执行完成后再显示真正的内容。

3.react 的合成事件

React 中的合成事件(Synthetic Events)是一种事件系统,它提供了跨浏览器一致性,并且与原生 DOM 事件相似,但具有一些额外的性能优势和跨浏览器的便利性。合成事件是 React 用来处理事件的一种方式,它在 React 的事件系统上建立了一个抽象层,以便更容易处理不同浏览器和平台的事件差异。

以下是一些关于 React 合成事件的重要信息:

  1. 跨浏览器一致性: 合成事件是 React 自己实现的,它们被设计成在不同浏览器中表现一致。这意味着你不必担心不同浏览器之间的事件差异,React 会为你处理这些问题。

  2. 性能优化: 合成事件也允许 React 进行性能优化。React 可以在合成事件上设置事件委托,以减少事件监听器的数量,提高应用性能。

  3. 事件委托: React 使用事件委托的方式来处理事件。它将事件监听器添加到根元素上,并根据事件的冒泡阶段来调用适当的组件的事件处理函数。这使得在具有大量元素的页面上处理事件变得更加高效。

  4. 与原生事件相似: 虽然合成事件不是原生 DOM 事件,但它们的 API 与原生事件非常相似。你可以像处理原生事件一样使用event.targetevent.preventDefault()event.stopPropagation()等方法和属性。

  5. 事件池: 合成事件是基于事件池的,这意味着在事件处理函数执行完成后,合成事件对象将被重用,以减少内存消耗。

React 中使用合成事件的基本示例:

import React from "react";

class MyComponent extends React.Component {
  handleClick = (event) => {
    // 访问事件属性,如event.target
    const clickedElement = event.target;

    // 阻止默认行为
    event.preventDefault();

    // 阻止事件冒泡
    event.stopPropagation();
  };

  render() {
    return <button onClick={this.handleClick}>点击我</button>;
  }
}

export default MyComponent;

总之,React 的合成事件提供了一种方便且跨浏览器一致的方式来处理事件,同时也允许 React 进行性能优化。 你可以像处理原生事件一样使用合成事件,并且不必担心不同浏览器之间的事件差异。

4.Fiber 节点和虚拟 DOM(Virtual DOM)差异

理解 React 中的 Fiber 节点和虚拟 DOM(Virtual DOM)是非常重要的,因为它们是 React 内部实现的核心组成部分,有助于提高性能和渲染优化。

Fiber 节点:

  1. Fiber 架构: Fiber 是 React 16 引入的新的协调算法。它的目标是实现可中断和增量式的渲染,以提高 React 应用的响应性和性能。

  2. Fiber 节点的作用: 在 Fiber 架构中,每个 React 元素(组件)都对应一个 Fiber 节点。这些节点构成了 Fiber 树,与 React 组件树相对应。Fiber 节点包含了组件的信息,如类型、props、状态等,以及与渲染相关的信息。

  3. 协调和调度: Fiber 节点的主要作用是协调组件的更新并进行调度。React 可以根据 Fiber 树的结构来确定何时更新组件,以及如何安排更新的优先级。这允许 React 在渲染中断和恢复时能够更好地控制渲染流程。

虚拟 DOM(Virtual DOM):

  1. 虚拟 DOM 的概念: 虚拟 DOM 是一个抽象的 JavaScript 对象树,它是 React 的一种性能优化机制。它与实际 DOM 结构一一对应,但比实际 DOM 更轻量。

  2. 虚拟 DOM 的用途: 当组件状态发生变化时,React 首先在虚拟 DOM 上进行操作,而不是直接更新实际 DOM。React 会将新旧虚拟 DOM 树进行比较,找出差异,并生成一系列更新指令。

  3. 减少实际 DOM 操作: 虚拟 DOM 的目标是减少直接操作实际 DOM 所带来的性能开销。React 通过最小化实际 DOM 操作的次数来提高性能,只更新必要的部分,从而减少页面重绘的开销。

  4. 虚拟 DOM 的更新: 当 React 生成了一系列更新指令后,它将这些指令应用于实际 DOM,以反映组件状态的变化。这个过程通常比直接操作实际 DOM 更高效。

综合来说,Fiber 节点是 React 内部的数据结构,用于管理组件的更新和渲染控制,而虚拟 DOM 是 React 的性能优化机制,通过抽象的方式减少实际 DOM 操作的开销。这两者协同工作,使得 React 能够提供高性能、响应式的用户界面。

Fiber 节点和虚拟 DOM(Virtual DOM)是 React 中两个不同的概念,它们有一些关键的差异:

  1. 性质和用途:

    • Fiber 节点是 React 内部用于协调和管理组件更新的数据结构,它的主要作用是实现可中断、增量式的渲染以提高性能和响应性。
    • 虚拟 DOM 是 React 的性能优化机制,是一个轻量级的 JavaScript 对象树,用于减少实际 DOM 操作的开销,通过比较新旧虚拟 DOM 来确定需要更新的部分。
  2. 关联对象:

    • Fiber 节点与 React 组件一一对应,每个组件都有一个对应的 Fiber 节点,用于保存组件的状态和与渲染相关的信息。
    • 虚拟 DOM 是一个抽象的 JavaScript 对象树,与实际 DOM 结构一一对应,但比实际 DOM 更轻量,用于描述组件的渲染结构。
  3. 层次结构:

    • Fiber 节点构成了 Fiber 树,它与 React 组件树一样具有层次结构。Fiber 树的结构用于协调和调度组件的更新。
    • 虚拟 DOM 不是树状结构,而是一个简单的 JavaScript 对象树,用于表示组件的渲染结构。
  4. 目标和功能:

    • Fiber 节点的目标是实现渲染的可中断性,使 React 能够更好地响应用户交互、调度异步更新,并优化渲染顺序。
    • 虚拟 DOM 的目标是减少实际 DOM 操作的次数,通过比较虚拟 DOM 树来确定需要更新的部分,以提高性能。
  5. 实现细节:

    • Fiber 节点是 React 的内部数据结构,不直接暴露给开发者。React 使用 Fiber 节点来实现协调算法。
    • 虚拟 DOM 是 React 的概念,而且通常是不可见的。开发者通常不需要直接操作虚拟 DOM,React 会在内部处理虚拟 DOM 的创建和更新。

总之,Fiber 节点和虚拟 DOM 是 React 内部实现的两个不同概念,它们分别用于协调组件更新和优化渲染。

Fiber 节点是一种数据结构,虚拟 DOM 是一种性能优化机制。它们一起帮助 React 提供高性能的用户界面渲染。

5.react18 新特性

5.1 兼容性

React 18 已经放弃了对 ie11 的支持,2022 年 6 月 15 日 停止支持 ie,如需兼容,需要回退到 React 17 版本。

5.2 自动批量更新(automatic batching)

在 React 18 之前,React 只会在事件回调中使用批处理,而在 Promise、setTimeout、原生事件等场景下,是不能使用批处理的。

// react18之前,react会触发两次更新
setTimeout(() => {
  setCount((c) => c + 1);
  setFlag((f) => !f);
}, 1000);

// react18之后,react会自动批量更新
setTimeout(() => {
  setCount((c) => c + 1);
  setFlag((f) => !f);
}, 1000);

如果不想批量更新,可以使用 flushSync(不推荐)

import { flushSync } from "react-dom";

function handleClick() {
  flushSync(() => {
    setCounter((c) => c + 1);
  });

  flushSync(() => {
    setFlag((f) => !f);
  });
}

5.3 concurrent mode(并发模式)

React18 的渲染是可以被终端、继续、终止的

5.4 useTransition

可以用来降低渲染优先级。分别用来包裹计算量大的 function 和 value,降低优先级,减少重复渲染次数。

举个例子:搜索引擎的关键词联想。一般来说,对于用户在输入框中输入都希望是实时更新的,如果此时联想词比较多同时也要实时更新的话,这就可能会导致用户的输入会卡顿。这样一来用户的体验会变差,这并不是我们想要的结果。

我们将这个场景的状态更新提取出来:一个是用户输入的更新;一个是联想词的更新。这个两个更新紧急程度显然前者大于后者。

以前我们可以使用防抖的操作来过滤不必要的更新,但防抖有一个弊端,当我们长时间的持续输入(时间间隔小于防抖设置的时间),页面就会长时间都不到响应。

而 startTransition 可以指定 UI 的渲染优先级,哪些需要实时更新,哪些需要延迟更新。

即使用户长时间输入最迟 5s 也会更新一次,官方还提供了 hook 版本的 useTransition,接受传入一个毫秒的参数用来修改最迟更新时间,返回一个过渡期的 pending 状态和 startTransition 函数。

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

export default function Demo() {
  const [value, setValue] = useState("");
  const [searchQuery, setSearchQuery] = useState([]);
  const [loading, startTransition] = useTransition(2000);

  const handleChange = (e) => {
    setValue(e.target.value);
    // 延迟更新
    startTransition(() => {
      setSearchQuery(Array(20000).fill(e.target.value));
    });
  };

  return (
    <div className="App">
      <input value={value} onChange={handleChange} />
      {loading ? (
        <p>loading...</p>
      ) : (
        searchQuery.map((item, index) => <p key={index}>{item}</p>)
      )}
    </div>
  );
}
Contributors: masecho