前端开发的十万个为什么(三)


react-helmet 库导出的 helmet 组件有什么用?

react-helmet 提供了对 <head> 标签内元素的动态控制,对 SEO 友好。

例如:

import { Helmet } from 'react-helmet';

const Home = () => (
  <div>
    <Helmet>
      <title>首页</title>
      <meta name="description" content="这是我们的首页" />
    </Helmet>
    <h1>欢迎来到首页</h1>
  </div>
);

Link To the File

为什么 React 官方不建议使用 index 作为 key?有特殊情况吗?

官方原因:

  1. 当列表顺序改变时,元素的索引也会变化,框架无法复用原本的节点,需要执行完整的 unmount 和 mount,导致性能下降。
  2. 更重要的是,在某些极端情况下,使用索引作为 key 会导致渲染错误。特别是当列表中的元素可以被修改、添加或删除时。这可能会导致索引和元素之间的关联关系混淆,从而产生不符合期望的结果。

有一种特殊情况:完全受控组件(内容可控的列表)中可能会出现用 index 作为 key 的性能更好。 https://codesandbox.io/s/key-1st6g 用 index 作为 key 比 id 快一倍。 原因:当用 id 作为 key 时,每次翻页都需要对所有子元素执行完整的 unmount 步骤,完全重新初始化一遍节点。而用 index 作为 key,React 只需要更新子组件的状态,进行重渲染。

那为什么 React 官方还是强烈推荐用 id 作为 key,而不是 index?

  • DOM 操作的耗时远小于接口调用获取数据的耗时,例如翻页情况下,一共就那么多 DOM,用 ID 还是 index 影响不大。
  • 用 ID 可以更加准确地标记组件的唯一性,例如你将某一行设置为了编辑态,然后进行了翻页。如果用 index 作为 key,这个时候原来那一行的选中状态就到了新的一行,而用 id 则还能标记原来那一行。(这里想说明的是在层级较深时很容易变成非受控

什么是受控组件?什么是非受控组件?

受控组件: 没有内部状态或内部状态完全由 props 决定的组件 非受控组件: 存在不受 props 控制的内部状态的组件

/* 受控的 input 写法 */
<input value={text} onChange={handleChange} />

/* 非受控 input 写法 */
<input defaultValue={initialText} onChange={handleChange} />

setState 是同步的还是异步的?为什么面试会问这个问题

React18 之前,setState 可以分为同步和异步:

  • 组件生命周期或者 React 的合成事件中,setState 是异步的
  • setTimeout、原生 DOM 事件、Promise 的回调中,setState 是同步的

React18 之后,所有状态的更新均为异步,React 会自动进行 batch。

为什么会问: React 会对状态的更新进行批处理。所以可能你上一步更新了状态,下一步访问这个状态的时候发现并没有拿到这个最新的值。

企业级首屏优化一般有哪些手段?

在实现首屏优化前,首先需要进行的是链路分析,也就是在当前方案的前提下对首屏渲染的耗时进行分段分析。例如:

  • 页面白屏时间
    • 创建 WebView
    • 获取 HTML
    • 内联 JS 执行
    • JS 资源的下载和执行
  • 页面初始化时间
    • 主要设置信息接口
    • 鉴权接口
    • 文案信息
  • 其余会在用户操作前执行的流程
    • 接口调用

拿到当前项目的这些信息之后,再去思考该怎么对这个链路进行剪枝。例如:

  • 接口调用提前:「首屏的接口调用真的一定要在最后阶段进行吗?」「框架是否提供了 prefetch 的功能,让我们可以提前去调用接口?」
  • 打包体积优化:对前端的包进行体积优化,通常 JS 的包大小是影响最大的。一些核心思路:
    • 在 HTTP 2 的前提下,尽可能让包大小相近,并且测试打多少个包,每个包大概多大能保证加载速度最快
    • 假设 webpack 是项目的打包构建工具,思考使用动态的 import 去调用组件,减少首屏包的大小
    • 假设是 ToB 或者公司内部的项目,考虑使用公共的离线包,来加速资源的获取。例如通过内网来拉取 reactreact-dom
  • SSR:使用合适的框架实现 SSR,减少客户端加载的内容

Service Worker 和 Web Worker 有啥关系?

Web Worker 是一种独立于浏览器的 JS 线程的后台 JS 线程,可以额外地处理一些计算复杂性的任务,再将计算结果返回给主线程。这种独立的计算能力,能够避免主线程在处理复杂任务时的渲染卡顿。一般可以使用 postMessage 去实现和主线程的通信。

Web Worker 的主要使用场景:数据加解密、数据预请求、预渲染(canvas)、大型数据高时间复杂度任务、图片预加载。

Service Worker 是基于 Web Worker 开发的一种技术,它独立于网页并可以处理网络请求、缓存数据等任务。和 Web Worker 的区别在于:

  1. 主要用途是在后台进行网络请求,缓存资源并且提供离线访问和推送通知的功能
  2. Service Worker 运行在浏览器的上下文之外,Web Worker 运行在浏览器的上下文之中
  3. Service Worker 在网页打开时注册,在关闭时也仍然能够保持后台运行

Vite(Rollup)和 Webpack 相比有什么缺陷?

  1. Vite 采用按需编译的方式,因此本地编译速度较快,但是 Vite 将源码的编译行为交给了浏览器,在预览项目的时候由于需要实时的编译工作可能会产生延迟。
  2. Rollup 缺失了 Webpack 对于拆包的灵活性,例如 optimization.splitChunks 等功能。
  3. 在插件和工具时可能会面临选择较少的问题。

针对基于 JavaScript 的编译工具,字节开源了基于 Rust 的 Rspack,用于实现前端项目的快速构建。

useMemo 和 useCallback 有什么区别?

从使用方法上,两者区别显而易见,useMemo 缓存的是useCallback 缓存的是函数

个人使用下来,在开发的时候大部分场景是比较好区分的:

  • 给一个元素的事件添加一个回调函数,并且这个回调函数跟当前组件内部绝大部分的状态都无关,就用 useCallback 去缓存这个事件的回调函数。
  • 给一个元素一个特定的值,并且这个值跟组件内部绝大部分状态都无关,就用 useMemo 去缓存这个值。

useMemo 我个人用下来跟 Vue 中的 watchcomputed 很接近,都是通过控制状态的更新时机来减少组件重渲染的次数。

Vue 中自动跟踪依赖进行重渲染,为什么 React 不行?

React 的设计哲学是单向数据流,希望状态有一个集中管理和修改的地方,强调将组件设计为纯函数,通过状态的变更来实现重选染,以实现更加可控的状态管理。

Vue 则更加推崇双向数据绑定,这意味着同一个状态可以由父子或者多个组件一起维护和更新,这保证了状态管理的灵活性。

为什么 React 18 中的 useEffect 在开发模式下会运行两次?

React 官方很“好心”地在 React 18 中默认开启了严格模式,在严格模式下,Mount 组件的时候 useEffect 的回调会执行两次,模拟组件卸载和再次挂载的场景,来确保你的组件不会引入任何未知的副作用。

为什么 React 的组件不默认开启 memo?

memo – React 中文文档

React.memo(MyComp) 可以减少子组件的 re-render,原理上是会将组件的新旧 props 进行浅比较来确认是否需要 re-render。

可以推测没有默认启用的理由:

  1. 浅比较 props 是有额外的开销的,React 官方认为在默认情况下浅比较 props 比重渲染会带来更大的开销。
  2. memo 一般来说需要配合着 useMemouseCallback 来实现控制 props 的更新,来实现避免重渲染。如果你的组件中没有用到 useMemouseCallback 来控制状态的缓存,那么默认开启 memo 就没有任何意义。
  3. React 的官方哲学是从简,而默认不开启 memo 符合直觉上的从上至下全部 re-render 的行为。

据说React Compier 准备帮你实现全自动 useMemo、useCallback: 译:我今天尝试了 React Compiler,你猜怎么着…… 😉 – 云谦的博客 (sorrycc.com)