Skip to content

React相关面经整理

Posted on:2024年1月20日 at 22:33

架构相关

Fiber

产生原因

传统的虚拟dom在递归更新时无法中断,如果组件树层级较深时会占用大部分线程时间,使得页面卡顿。

Fiber的引入改变了这一情况。Fiber可以理解为是React自定义的一个带有链接关系的DOM树,每个Fiber都代表了一个工作单元,React可以在处理任何Fiber之前判断是否有足够的时间完成该工作,并在必要时中断和恢复工作。

其实也可以理解为增强的虚拟dom。

是什么?

React内部实现的一套状态更新机制。支持任务不同 优先级,可中断与恢复,并且恢复后可以复用之前的 中间状态。

fiber怎么找到断点继续开始

// packages/react-reconciler/src/ReactFiberWorkLoop.js

JSX到页面dom的过程

JSX 经过 babel 转换后会变成 React.createElement(...) 形式的 js 代码,返回的是一个 React Element实例,也就是虚拟dom。

JSX 是一种描述当前组件内容的数据结构,他不包含组件 schedule 、 reconcile 、render所需的相关信息。

在组件 mount 时,Reconciler根据 JSX描述的组件内容生成组件对应的 Fiber节点。

在组件 update 时,Reconciler将 JSX与 Fiber节点保存的数据对比,生成组件对应的 Fiber节点,并根据对比结果为 Fiber节点打上 标记。

最后进入 Render 阶段,将fiber转化为真实dom并显示在页面上。

React diff

Diff的执行位于render阶段的 update。

对于 update的组件,他会将当前组件与该组件在上次更新时对应的Fiber节点比较(也就是俗称的Diff算法),将比较的结果生成新Fiber节点。


一个 DOM节点在某一时刻最多会有4个节点和他相关。

  1. current Fiber。如果该 DOM节点已在页面中,current Fiber代表该 DOM节点对应的 Fiber节点。
  2. workInProgress Fiber。如果该 DOM节点将在本次更新中渲染到页面中,workInProgress Fiber代表该 DOM节点对应的 Fiber节点。
  3. DOM节点本身。
  4. JSX对象。即 ClassComponent的 render方法的返回结果,或 FunctionComponent的调用结果。JSX对象中包含描述 DOM节点的信息。

Diff算法的本质是对比1和4,生成2。

SSR

SSR的整个过程,Next.js做了什么

为什么SSR的首屏时间会更短

之所以说ssr快,主要体现在http请求数量上,ssr只需要请求一个html文件就能展现出页面,虽然在服务器上会调取接口,但服务器之间的通信要远比客户端快,甚至是同一台服务器上的本地接口调取。

spa慢主要慢在需要请求大量的js资源,一般的首页,请求10几个js都算少到,要知道js的加载是同步的,页面逻辑可能藏在最后几个js里面,请求-等待-下载-然后是解析js-再调接口-展现页面。

React各版本区别

函数式组件和类式组件

函数式组件与类组件有何不同? - 知乎 (zhihu.com)

v16

重写底层逻辑,引入fiber架构

引入Hooks (16.7)

v17

全新的jsx转换器

副作用清理时机

useEffect 副作用清理函数由同步变为异步执行,如果要卸载组件,则清理会在屏幕更新后运行

启发式更新算法更新

React17新特性:启发式更新算法

v18

Concurrent Mode 并发模式

Concurrent Mode(并发模式)

setState 自动批处理

在 React 18 之前,我们只在 React 事件处理函数 中进行批处理更新。默认情况下,在 promise、setTimeout、原生事件处理函数中、或 任何其它事件内的更新都不会进行批处理。

在 React 18 之后,任何情况都会自动执行批处理,多次更新始终合并为一次。

更新 render API

v18 使用 ReactDOM.createRoot()代替 ReactDOM.render(),创建一个新的根元素进行渲染,使用该 API,会自动启用并发模式。

Suspense 支持 SSR

可以使用 <Suspense> 来将程序分解成较小的独立单元,允许服务端分组件返回页面,有选择的让重要的部分首先加载与水合。

useTransition

Hooks

useState

执行过程

setState后发生了什么?

update时,useReducer与 useState调用的则是同一个函数updateReducer。

function updateReducer<S, I, A>(
  reducer: (S, A) => S,
  initialArg: I,
  init?: (I) => S
): [S, Dispatch<A>] {
  // 获取当前hook
  const hook = updateWorkInProgressHook();
  const queue = hook.queue;

  queue.lastRenderedReducer = reducer;

  // ...同update与updateQueue类似的更新逻辑

  const dispatch: Dispatch<A> = (queue.dispatch: any);
  return [hook.memoizedState, dispatch];
}

整个流程可以概括为一句话:

找到对应的 hook,根据 update计算该 hook的新 state并返回。

什么时候需要传入一个函数作为参数?

通常用于优化性能,也称为惰性初始化。

const initialState = Number(window.localStorage.getItem("count"));
const [count, setCount] = React.useState(initialState);

每次组件更新时都会重新执行一遍第一行的代码,当localstorage数据偏大时有不小的性能开销。

const initialState = () => Number(window.localStorage.getItem("count"));
const [count, setCount] = React.useState(initialState);

当 initialState 以函数形式传入时,它只会在函数组件初始化的时候执行一次,规避不必要的性能问题。

异步更新与批处理更新时需要注意

在setState时需要传入一个函数,获取之前的state值保证每次获取到的是最新

let [num, setNum] = useState(0)
const add = () => {
  let = num = 0;
  setNum(num + 1);
  setNum(num + 1);
  setNum(num + 1);
  console.log(num); // 1
}

传入函数作为参数,确保每次访问的值是更新后的值。

let [num, setNum] = useState(0);
const add = () => {
  setNum(num => num + 1);
  setNum(num => num + 1);
  setNum(num => num + 1);
  console.log(num); // 3
};

异步操作state值时情况类似,也需要传入函数操作。

useCallback

useMemo

useRef

如何解决闭包?

性能优化

组件

受控/非受控

通信方式

Redux

如何理解redux的三大原则,为什么必须这样?

  1. 单一数据源

    使得状态之间更好管理,更容易调试,以及一些撤销/重做的功能更容易。同时解决了Flux中存在的多的Store带来的Store互相依赖问题。

  2. State 是只读的

    更方便定位问题、追踪数据,避免竞态问题的出现。

  3. 使用纯函数来修改state

    借鉴了函数式编程,纯函数的好处是没有副作用,相同的输入,永远都会有相同的输出。这样做的好处是这个函数可以不依赖外部环境,更加解耦,更容易调试以及更容易的写单元测试。

useContext与useReducer

  1. userReducer获取 state和 dispatch
  2. userContext将 state和 dispatch共享到子组件

与Redux的区别

仍然强耦合于UI,生命周期局限在函数组件内部,并没有做到分离。一般用于只有少部分组件需要深层传递数据时的简易方案使用。