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 的底层实现主要基于以下几个关键原理:

  1. 单向链表存储 Hook 状态
  2. 按顺序调用 Hook
  3. 闭包的使用
  4. 在 Fiber 架构下管理 Hook 状态

1. 单向链表存储 Hook 状态

React 使用了一个单向链表来存储每个组件中所有的 Hook。每个函数组件的实例有一个 memoizedState 属性,指向该组件的第一个 Hook。当组件渲染时,React 会从第一个 Hook 开始,一直到最后一个 Hook。

每个组件实例对应一个Fiber存储节点,Fiber 节点记录了组件的状态信息,包括 Hooks 链表。每当组件渲染时,React 会将这个 Fiber 节点作为上下文,用来关联该组件的 Hook 数据。

链表的每个节点存储了一个 Hook 对象,通常包含以下信息:

  • memoizedState:存储最新的状态值
  • next:指向下一个 Hook 的引用
  • queue:需要更新的队列,通常用于 useState 等可更新状态的 Hook

2.Hook 调用索引

React 依赖每个组件中 Hook 的调用顺序来记录状态。在初始化或更新时,每调用一个 Hook,React 都会增加一个索引值,确保 Hook 调用的顺序与链表存储的顺序一致。

React 每次渲染组件时,都会按照顺序遍历 Hook 链表,从而保证每个 Hook 调用位置不变。React 的 currentHook 指针指向当前执行的 Hook,并依次遍历更新每个 Hook 的状态或副作用。这个顺序和链表管理使 React 能够在每次渲染时正确地复用或更新 Hook。

3. 闭包的使用

Hook 使用闭包来持久化每次渲染中的状态。当你在一个渲染周期中使用 setState 更新状态时,React 会创建一个更新对象并将其推入当前 Hook 的 queue 队列中。组件下一次渲染时,React 会遍历更新队列,将所有更新依次应用到 memoizedState 中,以生成最新的状态值。

这种闭包的机制有助于保持每个 Hook 调用的上下文,使得每次渲染可以获取到它需要的状态值或副作用依赖。

4.React Fiber 和调度

React Hook 的实现依赖于 React Fiber 架构,Fiber 架构为每个组件分配了一个独立的 Fiber 节点,支持中断更新和优先级调度。

  • Fiber 的作用
    • 每个 Fiber 节点对应一个组件实例,并存储了该组件的 Hook 状态链表。
    • Fiber 架构允许 React 中断、恢复和调度渲染,优化了 Hook 调用的性能和一致性。
    • 通过 Fiber 节点,React 可以管理每个组件的更新流程,并确保 useEffectuseLayoutEffect 等在适当的时机执行。
二、React的state什么情况下是同步更新的
当使用非合成事件,例如setTimeout、setInterval或者直接在原生事件内部(例如window.addEventListener)直接setState时,此时react会以同步方式更新state
 
三、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方法再次被调用
 
四、ReactDom Diff算法核心思想
1. 标识唯一性
每个子元素的 key 应该在兄弟节点中是唯一的。React 通过 key 来匹配新旧虚拟 DOM 树中的元素节点。具有相同 key 的节点被认为是相同的元素,因此可以复用或更新;而不同的 key 表示不同的元素,React 会卸载旧元素,添加新元素。
2.逐层比较策略
在比较新旧虚拟 DOM 树时,React 对比 key 的顺序和内容,以最小化 DOM 操作。若采用常规的树比较方法,时间复杂度是O(n^3),React基于一些假设,将时间复杂度变为O(n),相应的策略有:
  • 对比key
对于同一层级的一组子节点,通过指定唯一的key进行区分。通过key可以准确地发现新旧集合中的节点是否只存在位置不同的情况,若是,只需要将旧集合中节点的位置进行移动,更新为新集合中节点的位置即可,无需进行节点删除和插入。
基于key的移动算法策略: 如果当前节点在新集合中的位置比老集合中的位置靠前的话,不进行位置移动(例如下图中的B和d):
 
 
 

diff过程如下:

    使用变量(下面假设为lastIndex)来记录在新集合访问过的节点中,并跟其在老dom树中的位置做比较,如果当前位置>旧的位置,则需要移动的节点,就应该移动到该位置,初始值为0。

    循环新集合

  • 节点B:  lastIndex=0,   oldIndex=1; lastIndex < oldindex,因此B节点不动,lastIndex= Math.max(oldIndex, lastIndex) = 1
  • 节点A:lastIndex=1, oldIndex=0;lastIndex > oldIndex,因此A节点进行移动操作,lastIndex= Math.max(oldIndex, lastIndex) = 1
  • 节点D:lastIndex=1,   oldIndex=3;lastIndex < oldindex,因此D节点不动,lastIndex= Math.max(oldIndex, lastIndex) = 3
  • 节点C:lastIndex=3,   oldIndex=2;lastIndex > oldindex ,因此C节点进行移动操作。循环结束
  • 对比元素类型
两个不同类型的元素,生成的树肯定是不同的,当某一层级两个元素的类型不同,就直接生成该节点及子树,没有必要再递归比较了。
如下图:当component D换成了component G 后,即使两者的结构非常类似,也会将D删除再重新创建G
不考虑跨层级的移动情况

节点间只存在添加、删除操作。如下图所示,当想把A移动到D下面时,先删除A,然后再到D节点中创建A

 

 

五、渲染原理

 

渲染原理

 

六、fiber架构

出现背景

React16之前组件更新是一个递归的过程,深度优先遍历整个组件树,所有工作必须在单一的渲染周期中完成,这可能导致卡顿、丢帧等性能问题,React 16 引入的一种全新的协调(reconciliation)引擎,它使得 React 可以更好地中断和恢复渲染任务,帮助 React 在 UI 更新时更流畅地响应用户操作。Fiber 提供了一种更细粒度的工作分割方式,使得 React 可以在必要时暂停渲染任务,去执行更高优先级的任务,并在空闲时继续未完成的工作。

Fiber 的关键思想

       Fiber 架构的核心思想是将渲染工作分割成多个可中断的任务单元,并且通过以下两个关键概念实现优先级控制和调度:

  • 可中断的任务:在传统的同步渲染中,React 会一次性完成整个渲染过程,导致长时间的主线程占用。而 Fiber 的设计允许渲染任务被分割成一个个小单元,这样当有更高优先级的任务(如用户输入)需要处理时,React 可以暂停当前任务,先去执行高优先级任务,然后在合适的时机恢复之前未完成的渲染任务。

  • 优先级和任务调度:Fiber 引入了优先级的概念,为每个更新任务设定优先级。例如,用户输入等高优先级任务会优先被调度执行,而不影响低优先级的任务(如数据加载)。Fiber 能够根据任务的优先级合理安排渲染顺序,以确保高优先级任务更快得到响应。

Fiber 的核心机制

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

(1)Fiber 节点

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

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

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

(3)任务的中断与恢复

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

(4)任务优先级调度

  • Fiber 支持多种优先级策略,如同步优先级(例如用户点击)、异步优先级(例如数据加载)和离线优先级(例如后台数据更新)。
  • 每个任务都会被分配一个优先级,React 会根据优先级来调度任务,确保高优先级任务在低优先级任务之前完成。

原理

七、react如何声明渲染优先级

八、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 插件。它可以帮助你检查依赖项是否完整和正确

九、React17、18有哪些新特性

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

  • jsx中不再需要import React from 'react'; 为了实现该特性,需要支持新转换的工具链(如 Babel 7.9+ 或 TypeScript 4.1+)
  • React 17 优化了开发环境中的错误信息,使堆栈追踪更清晰、更有上下文
  • React 事件系统被重写,避免事件绑定到 document,而是直接绑定到 React 渲染树的根节点。
  • 允许在同一应用中同时使用多个 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  我是格鲁特  阅读(98)  评论(0编辑  收藏  举报