前端开发的十万个为什么(三)
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>
);
为什么 React 官方不建议使用 index 作为 key?有特殊情况吗?
官方原因:
- 当列表顺序改变时,元素的索引也会变化,框架无法复用原本的节点,需要执行完整的 unmount 和 mount,导致性能下降。
- 更重要的是,在某些极端情况下,使用索引作为
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 或者公司内部的项目,考虑使用公共的离线包,来加速资源的获取。例如通过内网来拉取
react
和react-dom
库
- SSR:使用合适的框架实现 SSR,减少客户端加载的内容
Service Worker 和 Web Worker 有啥关系?
Web Worker 是一种独立于浏览器的 JS 线程的后台 JS 线程,可以额外地处理一些计算复杂性的任务,再将计算结果返回给主线程。这种独立的计算能力,能够避免主线程在处理复杂任务时的渲染卡顿。一般可以使用 postMessage
去实现和主线程的通信。
Web Worker 的主要使用场景:数据加解密、数据预请求、预渲染(canvas)、大型数据高时间复杂度任务、图片预加载。
Service Worker 是基于 Web Worker 开发的一种技术,它独立于网页并可以处理网络请求、缓存数据等任务。和 Web Worker 的区别在于:
- 主要用途是在后台进行网络请求,缓存资源并且提供离线访问和推送通知的功能
- Service Worker 运行在浏览器的上下文之外,Web Worker 运行在浏览器的上下文之中
- Service Worker 在网页打开时注册,在关闭时也仍然能够保持后台运行
Vite(Rollup)和 Webpack 相比有什么缺陷?
- Vite 采用按需编译的方式,因此本地编译速度较快,但是 Vite 将源码的编译行为交给了浏览器,在预览项目的时候由于需要实时的编译工作可能会产生延迟。
- Rollup 缺失了 Webpack 对于拆包的灵活性,例如
optimization.splitChunks
等功能。 - 在插件和工具时可能会面临选择较少的问题。
针对基于 JavaScript 的编译工具,字节开源了基于 Rust 的 Rspack,用于实现前端项目的快速构建。
useMemo 和 useCallback 有什么区别?
从使用方法上,两者区别显而易见,useMemo
缓存的是值,useCallback
缓存的是函数。
个人使用下来,在开发的时候大部分场景是比较好区分的:
- 给一个元素的事件添加一个回调函数,并且这个回调函数跟当前组件内部绝大部分的状态都无关,就用
useCallback
去缓存这个事件的回调函数。 - 给一个元素一个特定的值,并且这个值跟组件内部绝大部分状态都无关,就用
useMemo
去缓存这个值。
useMemo
我个人用下来跟 Vue 中的 watch
和 computed
很接近,都是通过控制状态的更新时机来减少组件重渲染的次数。
Vue 中自动跟踪依赖进行重渲染,为什么 React 不行?
React 的设计哲学是单向数据流,希望状态有一个集中管理和修改的地方,强调将组件设计为纯函数,通过状态的变更来实现重选染,以实现更加可控的状态管理。
Vue 则更加推崇双向数据绑定,这意味着同一个状态可以由父子或者多个组件一起维护和更新,这保证了状态管理的灵活性。
为什么 React 18 中的 useEffect 在开发模式下会运行两次?
React 官方很“好心”地在 React 18 中默认开启了严格模式,在严格模式下,Mount 组件的时候 useEffect
的回调会执行两次,模拟组件卸载和再次挂载的场景,来确保你的组件不会引入任何未知的副作用。
为什么 React 的组件不默认开启 memo?
React.memo(MyComp)
可以减少子组件的 re-render,原理上是会将组件的新旧 props
进行浅比较来确认是否需要 re-render。
可以推测没有默认启用的理由:
- 浅比较 props 是有额外的开销的,React 官方认为在默认情况下浅比较 props 比重渲染会带来更大的开销。
memo
一般来说需要配合着useMemo
和useCallback
来实现控制props
的更新,来实现避免重渲染。如果你的组件中没有用到useMemo
和useCallback
来控制状态的缓存,那么默认开启memo
就没有任何意义。- React 的官方哲学是从简,而默认不开启
memo
符合直觉上的从上至下全部 re-render 的行为。
据说React Compier 准备帮你实现全自动 useMemo、useCallback: 译:我今天尝试了 React Compiler,你猜怎么着…… 😉 – 云谦的博客 (sorrycc.com)