react面试核心知识总结

类组件和函数组件的比较

 

一、React Hook

Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。

1.要解决什么问题?

  • 可以在函数组件中使用状态、模拟组件的生命周期
  • 可以复用组件状态及相关的变更逻辑。
2. 使用注意事项

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

这样做的原因是因为: React 靠的是 Hook 调用的顺序来确定state应该和哪个useState对应,因此Hook 的调用顺序在多次渲染之间应保持一致。

3.底层核心原理

React Hook 的实现依赖于 React Fiber 架构,Fiber 架构为每个组件实例分配了一个独立的 Fiber 节点,每个节点都是一个对象,组件所有的hook存储在对象的memoizedState属性中, 该属性指向当前组件实例的第1个Hook 对象

每个hook对象,通常包含以下信息:

memoizedState:存储最新的局部状态,useState的state值| useEffect的deps对象 | useMemo的缓存值和 depsuseRefref 对象。

baseState: 用于 useReducer,存储初始 state

baseQueue: 用于 useReducer,存储更新队列

next:指向下一个 Hook 的引用

queue:需要更新的队列,如果 queue.pending 之前有多个 setState,多个 update 会形成一个环形链表,以确保按顺序执行

这些hook对象组成了一个单向链表,那么memoizedState的初始值是何时产生的呢?

  • hooks初始化

react使用renderWithHooks来调用函数组件,在组件首次渲染 (mount) 时,React并不知道有多少hooks,因此一边执行一边收集组件内所有的Hook,并存储到 memoizedState 链表中,以下面的例子展示一下过程:

function Counter() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState("React");

  useEffect(() => {
    console.log("副作用");
  }, []);

  return <div>{count} - {name}</div>;
}

React 内部执行流程

  1. React 触发组件渲染(执行 Counter())。
  2. 组件中的 useState / useEffect 被调用:
    • useState(0) -> 触发 mountWorkInProgressHook(),创建第一个 Hook。
    • useState("React") ->  触发 mountWorkInProgressHook(),创建第二个 Hook,链接到 next
    • useEffect() ->  触发 mountWorkInProgressHook(),创建第三个 Hook,链接到 next

mountWorkInProgressHook函数每次被执行,都产生一个hook对象,里面保存了当前hook信息,然后将每个hooks以链表形式串联起来,并赋值给workInProgressmemoizedState关键源码如下:

function mountWorkInProgressHook() {
  const hook = {
    memoizedState: null,  // 该 Hook 的状态
    next: null  // 指向下一个 Hook
  };

  if (workInProgressHook === null) {
    // 这是组件的第一个 Hook,存入 Fiber 节点的 memoizedState
    currentlyRenderingFiber.memoizedState = hook;
  } else {
    // 不是第一个 Hook,则链接到前一个 Hook 的 next
    workInProgressHook.next = hook;
  }
  // 更新当前执行的 Hook 指针
  workInProgressHook = hook;
  return hook;
}

最终,React 会创建一个 memoizedState 链表,记录所有 Hook:

fiber.memoizedState = {
  memoizedState: 0,  // useState(0)
  next: {
    memoizedState: "React",  // useState("React")
    next: {
      memoizedState: { /* useEffect数据 */ },
      next: null
    }
  }
};
  • hooks更新

以useState进行说明,首先,我们来看 useState 内部存储的 Hook 结构:

const hook = {
  memoizedState: currentState, // 当前状态值
  queue: {  // 维护 state 更新队列
    pending: null  // 存储等待更新的状态
  },
  next: null  // 指向下一个 Hook(单向链表)
};

假设有一个const [count,setCount] = useState(0)的代码;当 setCount(x) 执行时:

step1: React 内部会创建一个更新对象

const update = {
  action: (prevState) => prevState + 1, // 计算新状态的方法
  next: null // 指向下一个更新(如果有多个 setState)
};

step2: React 将 update 加入 queue.pending,形成循环链表

const queue = hook.queue; 
if (queue.pending === null) {
  update.next = update; // 只有一个更新时,形成环
} else {
  update.next = queue.pending.next;
  queue.pending.next = update;
}
queue.pending = update;

step3: React 调用 scheduleUpdateOnFiber(workInProgress) ,该函数负责标记当前组件 Fiber 节点需要更新,并触发 React 的调度更新流程

step4: renderWithHooks 计算新的 state 并创建新的 Fiber 树

4. useEffect和useLayout的区别 

二、React的state更新机制
 
场景 更新方式
React 事件处理函数(俗称合成事件) 异步
生命周期函数(useEffect也是) 异步
原生 JavaScript 事件处理函数 同步
setTimeoutsetInterval 回调函数 同步
Promise 的 then 回调函数(需要特别注意) 同步

一些注意事项:

  • 当你的状态更新依赖于之前的状态时,应该使用函数式更新(react会把最新的状态传进来):
function FunctionalUpdateExample() {
  const [count, setCount] = useState(0);
  const handleClick = () => {
   //可能产生bug:由于 setState 的异步行为,直接使用 count + 1 可能会导致状态更新不正确
    setCount(count + 1);
   //正确
    setCount((prevCount) => prevCount + 1);
  };
  console.log('render', count);
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={handleClick}>Increment</button>
    </div>
  );
}
  • 多次更新同一个State会自动合并为一个
const [count, setCount] = useState(0);

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

在上述代码中,React 会合并多个 setCount 调用,确保只进行一次渲染,而不是三次。

react18之前,仅复合事件和生命周期支持,js原生事件方式、异步(setTimeout/setInterval、promise)都不会自动合并. react18支持了所有的方式

  •  集中处理state的更新
 短时间内批量更新多个不同的 state,React 会集中在一次渲染中一起处理,而不会触发多次重新渲染。
import React, { useState } from "react";

export default function App() {
  const [count, setCount] = useState(0);
  const [text, setText] = useState("Hello");

  const handleClick = () => {
    setCount(count + 1);
    setText("World");
    console.log("Before render:", count, text); // 可能还是旧值
  };

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

react18之前,仅复合事件和生命周期支持,js原生事件方式、异步(setTimeout/setInterval、promise)都不会自动合并. react18支持了所有的方式

三、React的钩子函数依赖项注意事项

使用浅比较判断各个依赖项是否发生了变化,对于基本类型(如 stringnumberboolean),只比较值本身;对于引用类型(如对象、数组、函数),只比较引用是否相同(即内存地址),而不比较内容是否发生了变化。

注意的点:

  • 所有依赖项都必须声明,如果在依赖项数组中漏掉了某些值,可能会导致闭包问题、结果还是上次的,从而引发意料之外的行为
  • 使用常量依赖没有意义,因为常量不会改变
  • 如果依赖项数组中有复杂的对象或函数,可能导致不必要的重计算或重新创建(应该使用useRef来存储复杂变量)
    const obj = { key: 'value' };
    const memoizedValue = useMemo(() => doSomething(obj), [obj]); // 组件函数每次被执行时,obj每次都是新引用,导致重计算
    
    // 改进
    const stableObj = useRef({ key: 'value' });
    const memoizedValue = useMemo(() => doSomething(stableObj.current), []);
    启用 eslint-plugin-react-hooks 插件。它可以帮助你检查依赖项是否完整和正确
四、fiber架构

React16之前组件更新是一个递归的过程,深度优先遍历整个组件树,所有工作必须在单一的渲染周期中完成,这可能导致卡顿、丢帧等性能问题,React Fiber 是 React 的一个重写的核心算法,用来处理渲染过程中的调度和更新。它是 React 16 版本引入的重要改进,旨在提升 React 在大型应用中的性能,尤其是在复杂 UI 更新、动画和高频更新场景。

fiber算法渲染任务分割成一个个小单元,并指定优先级,当有更高优先级的任务(如用户输入)需要处理时,React 可以暂停当前任务,先去执行高优先级任务,然后在合适的时机恢复之前未完成的渲染任务。

为了实现这种细粒度的任务控制,Fiber 架构引入了以下几个关键机制:

(1)Fiber 节点

  • 每个 React 组件实例对应一个 Fiber 节点。Fiber 节点是一个 JavaScript 对象,用于描述组件的类型、状态、子节点等信息
  • 在组件更新过程中,每个组件对应的 Fiber 节点可以存储其状态和更新内容,以便在重新渲染时可以直接访问并更新。

(2)双缓存树结构(Current 和 WorkInProgress)

  • React Fiber 使用双缓存树来管理渲染工作。它分为 current tree 和 workInProgress tree,即当前树工作树
  • current tree 表示当前屏幕上展示的 UI 状态,而 workInProgress tree 是新的更新任务的工作空间。
  • 在渲染的过程中,React 会在 workInProgress tree 上构建新的 UI 状态,更新完成后会将其替换成 current tree,从而完成一次更新。

(3)任务的中断与恢复

  • Fiber 使用 JavaScript 的 requestIdleCallback 或 scheduler 来实现任务中断,使得更新任务可以分批执行。
  • 这种机制确保在渲染任务被打断后,React 可以从中断的地方恢复,继续执行未完成的任务。

(4)任务优先级调度

    • 每个任务都会被分配一个优先级,React 会根据优先级来调度任务,确保高优先级任务在低优先级任务之前完成。
    • Fiber 支持多种优先级策略,内部的优先级策略大体如下:

Immediate(同步)优先级:比如事件处理,setState 发生在事件回调中,立即执行。

User Blocking 优先级:比如交互性较强的输入(如文本输入),需要尽快响应,但可以打断。

Normal(默认)优先级:大部分更新,比如组件挂载、数据请求后更新等。

Low 优先级:非关键的后台任务,比如渲染不在视口中的组件。

Idle 优先级:最低级,比如不紧急的预渲染、数据预加载等。

    • 在react18以前,没有提供相关的api给用户,从18版本开始,开发者可以手动控制优先级

1. Concurrent Mode : React 会自动根据更新的类型来分配优先级。你只需在渲染根组件时启用 Concurrent Mode 即可:

import ReactDOM from 'react-dom';
import App from './App'; 
ReactDOM.createRoot(document.getElementById('root')).render(<App />);

注意:React 18 之后默认开启了 Concurrent Mode,使用 ReactDOM.createRoot 即可

2.使用 useTransition 和 startTransition(降低任务的优先级)

useTransition 和 startTransition 是 React 18 引入的 API,允许我们手动设置更新的优先级。startTransition 和 useTransition 通常用于较低优先级的任务,例如加载数据等不影响用户操作的任务。

useTransition

useTransition 是一个 React Hook,返回一个布尔值 isPending 和一个 startTransition 函数。

import { useState, useTransition } from 'react';

function Example() {
  const [isPending, startTransition] = useTransition();
  const [value, setValue] = useState(0);

  const handleClick = () => {
    startTransition(() => {
      setValue((v) => v + 1);
    });
  };

  return (
    <div>
      <button onClick={handleClick}>Increment</button>
      {isPending ? <p>Updating...</p> : <p>Value: {value}</p>}
    </div>
  );
}

在上述例子中,handleClick 函数中的 setValue 被包裹在 startTransition 中,表示这是一个低优先级的任务。即使用户频繁点击按钮,React 也会优先渲染其他高优先级的任务,而把这个更新安排在后面。

startTransition

import { startTransition } from 'react';

function handleClick() {
  startTransition(() => {
    // 放入低优先级更新
    setValue((v) => v + 1);
  });
}

 

原理

五、React性能优化技术
核型思想就是让组件避免不必要的重复渲染,举例来说,当父组件render时,即使子组件没有发生任何变化,还是会重渲染。具体有以下优化措施:

1. 父组件传递给子组件的属性值是对象、函数时,不要使用内联模式,不然父组件每次render,即使属性值没发生变化,都会导致子组件渲染。

2. 函数组件的优化

  • 组件内部如果涉及到复杂耗时的数据计算,可以使用useMemo来缓存计算结果
  • 若内部有子组件,并且子组件需要接收函数做为属性值的话,此时父组件中的回调函数需要使用useCallback函数,不然会导致子组件的渲染优化判断机制失败。
  • 使用React.memo(fnComponent)这个高阶组件来做记忆函数,当props不变时,直接复用上一次的渲染结果。对props的比较是浅比较,可以自己实现比较逻辑,文档说明

3. class组件非必要情况下,不要使用state;可以在shouldComponentUpdate里判断props或state是否发生了改变,或者继承自PureComponent

4. 使用React.lazy来延迟加载非必需的组件,典型场景是路由系统中
const Foo = React.lazy(() => import('../componets/Foo));

React.lazy不能单独使用,需要配合React.suspense,suspence是用来包裹异步组件,添加loading效果等。

<React.Suspense fallback={<div>loading...</div>}>
    <Foo/>
</React.Suspense>
5. 使用循环来生成element元素时,应使用key,以便react diff算法能识别出同级元素是否只是位置发生了变化。
6. 减少不必要的dom嵌套,在react中多个同级元素需要一个父元素,为了满足这个规则,会构造无用的dom。应该使用<React.Fragment></React.Fragment>
7. 使用css来隐藏节点,而不是真的移除或添加DOM节点, 组件重新初始化一次的代价很高
8  如果有多个子或孙组件需要使用同一个state,此时应使用redux,而不是将公共的state移到祖先组件中,否则组件层次太深的话,在祖先组件setState会导致无数个子孙组件的render方法再次被调用
  

六、渲染原理

七、React17、18有哪些新特性

react17没功能上的更新,主要为后续版本升级做了一些铺垫.

    • jsx中不再需要import React from 'react'; 为了实现该特性,需要支持新转换的工具链(如 Babel 7.9+ 或 TypeScript 4.1+)
    • React 17 优化了开发环境中的错误信息,使堆栈追踪更清晰、更有上下文
    • React 事件系统被重写,避免事件绑定到 document,而是直接绑定到 React 渲染树的根节点。

在 React 16 及更早版本中,React 使用事件委托(Event Delegation),即:

所有事件不会直接绑定到 DOM 元素,

而是统一绑定到 document 上,然后在事件冒泡阶段处理它们

document.addEventListener('click', function(event) {
  // React 事件监听器在 document 上处理
});
              在 React 17 及以后,事件绑定不再挂载到document上,而是挂载到 React 渲染树的根节点。
// React 17+ 的事件绑定示例
const root = document.getElementById("root"); // React 根节点
root.addEventListener("click", function(event) {
  // 事件监听器绑定到 root,而不是 document
});
    • 允许在同一应用中同时使用多个 React 版本

react18的主要在并发渲染方面做了一些更新:

  • Concurrent Rendering(并发渲染): React 18 默认启用了并发渲染,它不会阻塞主线程,从而使应用更具响应性。
  • 自动批处理(Automatic Batching): React 18 会将多个状态更新自动批处理,从而减少渲染次数
    function handleClick() {
      setCount((c) => c + 1);
      setFlag((f) => !f);
      // React 18 中只会触发一次渲染,而不是两次。
    }
  • 开发者声明低优先级任务(useTransition)、延迟更新值(useDeferredValue)

  • React 18 引入了新的根 API(createRoot 和 hydrateRoot): createRoot 是为了支持并发渲染,hydrateRoot 则用于支持 SSR 的改进
  • ssr的改进:1. 增加了流式渲染,支持以流的方式将 HTML 发送到客户端,提升首屏加载性能 2. <Suspense> 支持服务端渲染(SSR)
posted @ 2022-03-30 09:54  我是格鲁特  阅读(134)  评论(0)    收藏  举报