「React」大厂装逼八股口喷框架核心原理行测申论

2,609 阅读34分钟

用结构化的答题思路,口喷 React 核心概念与原理,让面试官绝不敢在你面前装逼超过 3 分钟

1. 你真的了解 React 吗

🎯 破题

请面试者谈谈自己对 React 的理解,主动权转移给面试者,开放式问题。

🔢 承题

Q1. 你真的了解 React 吗

💯 答题

React 是一个用于构建 UI 用户界面的 JS 框架。
通过组件化的方式解决了视图层开发复用的问题,本质上是一个组件化的框架。

它的核心设计思路有三点,分别是声明式,组件化和通用性。

  1. 声明式的优势在于直观与方便组合;
  2. 组件化的优势在于视图的拆分和模块复用,更容易做到高内聚低耦合;
  3. 通用性在于一次学习,随处编写,比如在 React Native,React 360 等;
  • 优点:React 的通用性优势,归功于它使用了虚拟 DOM,这使得 React 的适用范围变得更加广阔,无论 Web、Native、VR,甚至是 Shell 应用都能灵活开发。
  • 缺点:作为一个视图层的框架,官方并不提供一揽子的解决方案,而是交给社区去实现,比如路由管理方案 React Router,全局状态管理方案 Redux,这样增加了技术选型的成本,以及对初学者上手难度上造成一定的干扰。

不可否认的是,Facebook 以拥抱社区、拥抱开源的方式,促进了 React 生态的繁荣。

2. 为什么 React 要使用 JSX

🎯 破题

考察面试者技术广度,深挖知识面,对流行框架模板方案是否了解,技术方案调研能力。

🔢 承题

Q2. 为什么 React 要使用 JSX

💯 答题

在回答问题之前,我先解释一下什么是 JSX。JSX 是一个 JavaScript 的语法扩展,结构类似 XML。

JSX 主要用于声明 React 元素,但 React 中并不强制使用 JSX。即使使用了 JSX,也会在打包构建过程中,使用 Babel 插件编译为 React.createElement。所以 JSX 更像是 React.createElement 的语法糖。 所以从这里可以看出,React 团队并不想引入 Javascript 以外的开发体系。而是希望通过合理的关注点分离保持组件开发的的纯粹性。

接下来与 JSX 以外的三种技术方案进行对比。

  1. 模板,React 团队认为模板不应该是开发过程中的关注点,会引入模板语法,模板指令等概念,这是一种不佳的实现方案。
  2. 模板字符串,模板字符串编写的结构会造成多次内部嵌套,使整个结构变得复杂,并且优化代码提示也会变得困难重重。
  3. JXON,同样是因为代码提示困难被放弃

所以 React 最后选用了 JSX,是因为 JSX 与其设计思想贴合,不需要引入过多的新概念,对编辑器的代码提示也极为友好。

3. 如何避免生命周期中的坑

🎯 破题

请面试者梳理 Reect 各生命周期用途以及使用时的注意事项,说说使用经历。

🔢 承题

Q3. 如何避免生命周期中的坑

💯 答题

避免生命周期中的坑需要做好两件事:

  1. 不在不恰当的时候调用不该调用的代码
  2. 在需要调用的时候,不要忘记调用(优化性能,解绑清理事件)

主要有 7 种情况容易造成生命周期的坑:

  1. getDeriverdStateFromProps 容易造成反模式,使得受控组件和非受控组件区分模糊
  2. componentWillMout 已标记弃用,主要原因是在新的异步架构下会导致多次被调用,因此网络请求推荐放到 componentDidMount
  3. componentWillRecieveProps 已标记弃用,被 getDeriverdStateFromProps 取代,主要原因是性能问题
  4. shouldComponentUpdate 通过返回 true 或 false 确定是否需要重新渲染,用于优化性能
  5. componentWillUpdate 已标记弃用,可结合 getSnapshotBeforeUpdate 与 componentDidUpdate 改造使用
  6. 如果在 componentWillUnmout 函数中忘记解除事件绑定,取消定时器等清理操作,容易引发 Bug
  7. 如果未添加错误处理边界,并通过 componentDidCatch 捕获处理,则用户将看到一个无法操作的白屏

4. 类组件与函数组件有什么区别

🎯 破题

考察面试者对 React 组件两种编写模式的了解程度,是否具备在合适业务场景下选择技术栈的能力。

🔢 承题

mini map

💯 答题

首先,我们要知道类组件和函数组件的共同点与不同点:

  • 对组件而言,类组件和函数组件在使用与呈现上没有任何不同,性能表现上,在现代浏览器中也不会有明显差异。

  • 但是在开发者的心智模型上却有着巨大的差异。类组件基于面向对象编程 (OOP),主打继承。生命周期这些核心概念。而函数组件基于函数式编程 (FP),主打 immutable、无副作用、引用透明等特点。

  1. 使用场景上,如果需要使用生命周期,设计模式上,如果需要使用继承,则推荐类组件。但是,自从 React v16.8 以后,因为 React Hooks 的推出,函数组件完全可以替代类组件使用。同时继承并不是组件最佳的设计模式,官方更加推荐 「组合优于继承」 的设计理念。因此类组件的优势渐渐淡出。
  2. 性能优化上,类组件主要依靠 shouldComponentUpdate 阻断渲染提升渲染性能;而函数组件依靠 React.memo 缓存渲染结果并跳过渲染来提升性能。
  3. 上手难度上,类组件更容易上手。
  4. 未来趋势上,由于 React Hooks 的推出,函数组件成为了社区未来主推的方案。 类组件在未来时间切片与并发模式中,由于生命周期带来的复杂度,并不易于优化。而函数组件本身清凉简单,且在 Hooks 的基础上提供了比原先更加细粒度的逻辑组织与复用,更能适应 React 未来的发展。

5. 如何设计 React 组件

🎯 破题

请面试者结合自己的使用经历谈谈 React 组件的设计模式,接手一个新项目要如何进行目录结构划分,考察面试者的前端工程化实践水平。

🔢 承题

mind map

💯 答题

React 组件应该从设计分类和工程实践两个方向进行讨论。

从设计分类的角度:

  1. 展示组件:其逻辑较为简单,一般用于 UI 视图界面的展示表达。主要包括代理组件,样式组件,布局组件。代理组件一个很经典的使用场景就是引入 Antd 组件库时,在每个组件上封装一层,如未来需要更新 Antd 或者定制化开发时,只需要维护代理组件即可。
  2. 灵巧组件:其主要面向具体业务,功能丰富,复杂度更高,主要包括容器组件和高阶组件。在实际开发中,我们一般将网络请求,和事件处理放在容器组件中,与展示组件隔离开。高阶组件类似于高阶函数,入参为函数,出参依然为函数,一般用于抽取公共业务逻辑或者提供某一项公用能力。主要使用场景包括登录检测和埋点统计,高阶组件可以通过链式调用完成一系列连贯的业务操作,除此之外,高阶函数还可以用于渲染劫持,通过继承原组件然后重写 render() 来达到修改渲染结果的目的,这种方式可用于应用的无侵入二次开发。同时高阶函数也有两个缺陷:1. 静态方法无法被外部直接调用,可以通过复制静态方法的形式实现;2. refs 不能透传,需要使用 React.forwardRef 进行转发。 从工程实践的角度: 通过文件夹划分的方式切分代码。将页面单独放置与 src/pages 中,通常为一些页面的无法复用的业务逻辑代码;将复用性较高的组件抽离出来放于 src/components,其中 src/component/basic 文件夹用于放置展示组件,由于展示组件与特务的关联性较低,可以使用 Storybook 进行 UI 组件的开发和单元测试,从而提升整个工程的质量和效率。

6. setState 是同步还是异步更新

🎯 破题

考察面试者对 React 原理的掌握程度,setState 的概念和用途,在那些场景下是同步操作,哪些场景下是异步操作。

🔢 承题

mind map

💯 答题

setState 并非真正异步,只是看上去像是一个异步。在源码中,通过 isBatchingUpdates 来判断 setState 是先存入 state 队列还是直接更新,如果值为 true 则执行异步操作,为 false 则执行更新。

那么什么情况下 isBatchingUpdates 会为 true 呢?在 React 可控制的地方,就为 true,比如在 React 生命周期事件和合成事件中,都会走合并操作,延迟更新的策略。

但在 React 无法控制的地方,比如原生事件 (addEventListener / setTimeout / setInterval) 中只能同步更新。

一般认为,异步设计的目的在于性能优化,减少渲染次数,React 还补充了两点:

  1. 保持内部一致性。如果将 setState 改为同步更新,那么尽管 state 的更新是同步的,但 props 不是。
  2. 启用并发更新,完成异步渲染。

7. 如何面向组件跨层级通信

🎯 破题

考察面试者对 React 组件通信方式的掌握程度,结合自己的实际开发经历,谈谈跨组件通讯的主要场景。

🔢 承题

mind map

💯 答题

在组件跨层级通信的过程中,主要分类一层与多层的情况。

一层情况下:

  1. 父组件向子组件通信:仅通过 Props 传递即可,这是最常见的一种方式,符合 React 自顶向下的单向数据流设计哲学。
  2. 子组件向父组件通信:主要通过回调函数的方式实现,这也是诸如 antd 等组件库的组件 API 设计方式,比如 <Button onClick={...} />
  3. 兄弟组件之间通信:可由父组件作为中转,结合以上两种方式进行通信。 多层情况下:
  4. React Context API,一般用于全局化的配置,比如组件库的多语言包,全局主题切换等等。
  5. 全局对象和全局事件,React 应用中将状态挂载到 Window 对象下,用于存储临时变量,但是并不推荐这种方式。
  6. 使用状态管理框架 Flux、Redux、Mobx,引入状态管理框架之后,也许开发模式和代码结构会得到约束,但缺点是学习成本较高。

8. 列举一种你了解的 React 状态管理框架

🎯 破题

考察面试者对 React 状态管理框架在复杂业务场景下的使用经验。

🔢 承题

mind map

💯 答题

待更新 ...

9. Virtual DOM 的工作原理是什么

🎯 破题

考察面试者对 React 核心概念 Virtual DOM 的理解程度。

🔢 承题

mind map

💯 答题

虚拟 DOM 的工作原理是通过 JS 对象模拟真实的 DOM 节点实现的。在 Facebook 构建 React 初期时,考虑到提升代码抽象能力,避免人为的 DOM 操作,降低代码整体风险等因素,从而引入了虚拟 DOM 的概念。

  1. 实现原理上:虚拟 DOM 是一个纯对象 (Plain Object) ,我们通常在 render 函数中声明的 JSX 会被 Babel 转译成 React.createElement(type, props, chidren), 该方法返回一个对象,这个对象 { type, props, children } 就是虚拟 DOM 节点的 JS 描述。 整个应用通过各个虚拟 DOM 的 chidren 属性建立层级关系并相互连接,组成一棵树状结构(虚拟 DOM 树),当状态变更时, React 会将变更前后的两颗 DOM 树进行差异比较,这个过程称为 diff,生成的差异结果叫做 patch,ReactDOM 会将生成的 patch 进行渲染,从而完成真实的 DOM 操作。

  2. 优点:

  • 改善大规模的 DOM 操作

  • 规避 XSS 风险

  • 较低的成本实现跨平台开发

  1. 缺点:

  • 内存占用较高,需要模拟整个网页的真实 DOM
  • 高性能应用场景下难以优化的情况,比如 Google Earth 应用不会选择 React 虚拟 DOM 的应用场景不仅仅是渲染,还可用于埋点统计数据记录,只要用户界面的 DOM 发生真实有效的变更,都可以将用户的交互行为上报给服务器,实现监控效果。

10. 与其他框架相比,React 的 Diff 算法有何不同

🎯 破题

考察面试者对 React Diff 算法的理解程度,同时需要面试者技术视野广泛,结合社区其他框架的 Diff 方案做比较。

🔢 承题

mind map

💯 答题

首先,Diff 算法是指生成更新补丁的方式,主要应用于虚拟 DOM 树变化后,更新真实 DOM。因此 Diff 算法一定存在这样一个过程:触发更新 -> 生成补丁 -> 应用补丁。

React diff 算法触发更新的时机主要在 setState 变化和 hooks 调用之后,此时,触发虚拟 DOM 树变更遍历,React 采用深度优先遍历算法,但传统的遍历方式,效率较低,复杂度为 O(n^3)。为了提升效率, React 采用计算机科学中的「分治」这一经典的手法,分而治之的巧妙分解问题,将单一节点对比,转化为 3 种类型节点的对比,对应 diff 的三种优化策略:

  1. 树对比:只对同一层级节点进行比较
  2. 组件对比:同类型组件才会对比,否则直接放入 patch
  3. 元素对比:同层级节点中,通过设置 key 标记优化对比效率

自从 React v16 引入 Fiber 架构之后,为了使整个更新过程可以随时暂停恢复,节点与树分别采用了 FiberNode 和 FiberTree 进行重构,其中 FiberNode 使用了双链表结构,可以直接找到兄弟节点和子节点。整个更新过程由 current 和 workProgress 两株树双缓冲完成。

社区中的其他优秀框架 Diff 算法的区别:

  • Preact 整体设计思路相似,但是底层元素采用了真实 DOM 对比操作,未采用 Fiber 设计。
  • Vue 2.0 和 3.0 整体设计思路与 React 一致,依然未引入 Fiber 架构。 横向比较
  • React 拥有完整的 Diff 算法策略,且拥有随时中断更新的时间切片能力,在大批量节点更新的极端情况下,拥有更友好的交互体验。
  • Preact 可以在一些对性能要求不高,仅需要渲染框架的简单场景下应用。
  • Vue 的整体 diff 策略与 React 对齐,虽然缺乏时间切片的能力,但并不意味着 Vue 的性能更差,在 Vue 3 初期引入过,后期因为收益不高被移除掉了,除了高帧率动画,在 Vue 中其他场景几乎都可以使用防抖和节流去提高响应能力。

Tips: 在面试过程中,切忌踩一捧一,容易引发面试官的反感。

🎇 延伸

那么基于 React Diff 算法的原理,在开发中如何优化代码呢?

  1. 尽量避免跨层级节点移动
  2. 设置唯一的 key 值进行优化
  3. 尽量减少组件的层级深度,从而降低树遍历的深度
  4. 设置 shouldComponent 或者使用 React.PureComponent 减少 diff 次数

11. 如何解释 React 的渲染流程

🎯 破题

此题是一个非常宽泛的开放性题目,考察的不仅仅是面试者的知识储备,还有一定的沟通表达技巧,需要面试者的回答做到「讲话有重点,层次要分明」,做到对 React 渲染流程的归纳总结。

🔢 承题

mind map

💯 答题

React 的渲染流程一直没有太大变化,但是协调机制却发生巨大的改变,以 React v16 为分界线,v16 之前采用 Stack Reconciler 栈调和,v16 之后采用的是 Fiber Reconciler 协作式多任务调和。这里提到的协调狭义上来说就是 React 的 diff 算法,广义上来是指的 React 中的 Reconciler 模块,它通常包含了 diff 算法和一些公共的逻辑。

  1. Stack Reconciler 栈调和的核心调度方式是递归。调度的基本处理单位是 Transaction 事务,React 源码中有一个 Transaction 的基类,这个概念是 React 团队从后端开发中借鉴的概念,它能够保证协调过程中对状态操作的一致性和原子性。协调过程中,ReactMount 模块负责挂载, ReactUpdate 模块负责更新,各个模块之间相互分离,通过事务来驱动执行。

  2. Fiber Reconciler 的调度方式有两个特点,

  • 协作式多任务模式,在这个模式下,线程会定时放弃自己的运行权利,交还给主线程,通过 requestIdleCallback 实现。
  • 策略优先级,调度任务通过标记 tag 的方式分优先级执行,比如动画,或者标记为 high 的任务优先执行。
  • Fiber Reconciler 的基本单位是 Fiber,基于过去的 React Element 提供了二次封装,提供了指向父、子、兄弟节点的引用,为 diff 工作的双链表实现提供了基础。在新的架构下,整个应用生命周期被划分为 Render 和 Commit 两个阶段。Render 阶段的执行特点是可中断、可停止、无副作用,主要是通过构造 workInProgress 树计算出 diff。以 current 树为基础,将每一个 Fiber 作为一个基本单位,自下而上逐个节点检查并构造 workInProgress 树。这个过程不再是递归,而是基于循环来完成的。
  • 在执行上通过 requestIdleCallback 来调度执行每一组任务,每组中的每个计算任务又被称为 work,每个 work 完成后确认是否有更高优先级的 work 需要插入,如果有就让出位置,没有就继续执行。优先级通常是标记为动画或者 high 的会优先处理。每完成一组后,将调度权交回给主线程,直到下次 requestIdleCallback 调用,再继续构建 workInProgress 树。 在 commit 阶段需要处理 effect 列表,这里的 effect 列表包含了根据 diff 更新 DOM 树、回调生命周期、响应 ref 等等。 但要注意的是,这个阶段是同步执行的,不可中断暂停,因此不要再 componentDidMout、componentDidUpdate、componentWillUnmout 中去执行重度消耗计算能力的任务。

综上所述,如果只是一般的应用场景,比如管理后台、H5 展示页等,两者性能差距不会太明显,但是在动画、画布以及手势等场景下、 Stack Reconciler 的设计会占用主线程,造成卡顿,而 Fiber Reconciler 的设计恰恰能带来高性能的表现,这也是是 react-three-fiber 等动画库相继推出的大背景。

12. React 渲染异常会造成什么后果

🎯 破题

此题考察面试者在实际项目中,是否掌握对 React 错误边界的细节处理,是否能对 React 渲染异常引发的应用级崩溃采取过相应的解决方案。

🔢 承题

mind map

💯 答题

React 渲染异常时,在没有做出任何处理的情况下,会出现整个页面白屏的现象。它的主要原因是渲染层出现了 Javascript 错误,导致整个 React 应用崩溃。通常这种错误的原因是在 render 中未对数值进行非空校验,没有控制好空安全。在实际项目中,可以采取两个方向去治理:

  1. 预防:引入空安全相关的方案,在做技术选型的时候,主要考虑3种方式:第一个是引入第三方库,比如 Facebook 的 idx 或者 Lodash.get;第二个是引入 Babel 插件,使用 ES2020 的语法标准——可选链操作符;第三个是 Typescript,它在 3.7 版本之后可以直接使用可选链操作符。最后我选择了引入 Babel 插件的方案,因为这个方案外部依赖比较少,侵入性小,而且团队内老项目还未上 TS。
  2. 兜底:如果技术上只解决一个人的问题,价值是不够的的,要解决团队的公共问题,价值才会得到放大,空安全的问题,需要编写一个公共的高阶组件,去处理错误边界情况下的 UI 渲染,封装成 NPM 包供团队内部使用。 从治理的效果上看,预防和治理方案覆盖了团队内 100% 的 React 项目,头三个月兜底组件统计到日均 10 次的报警信息,其中有 10% 是公司的关键业务。经过分析,首先是为关键的 UI 组件添加兜底组件进行拦截,然后上报统计,针对暴露出来的问题,在团队内部做一次培训,对易错点的代码进行指导,加强 Code Review。后续到现在,线上只收到过 1 次报警。

13. 如何分析和调优性能瓶颈

🎯 破题

此题考察面试者是否有优化网站性能的经验,是否会使用相关工具和指标进行性能调优。

🔢 承题

13

💯 答题

我目前在公司负责的业务是 Saas 产品体验站点的开发,复杂交互的 UI 场景下需要考虑渲染性能和静态资源加载的问题。通过一系列性能优化后,整个用户体验满意度提升了 20%,页面加载速度有了显著提升。

在我加入团队之前,部门基本上没有性能优化的相关指标和数据,我着手接入了 hotjar 第三方用户体验反馈工具,才有了相关的性能指标。然后我对指标观察了一周,思考了目前的业务形态,发现 Saas 产品体验站点业务形态与管理后台较为接近,对加载速度没有 2C 端要求那么苛刻,但对系统稳定性要求比较高。

同时,我们发现静态资源的加载在并发 3000 的状况下加载速度变慢,这显然不能满足企业销售大会千人同时体验该站点的性能要求,于是我们对后端和前端分别进行了优化,后端由原来的单机架构改成拥有 3 个节点的集群架构,前端主要是我改变了 Webpack 构建工作流,将静态资源发布到 CDN。

结果显而易见,发布会当天支撑了上万人的同时在线对我们的 Saas 产品进行实时体验,前端的性能优化降低了服务器带宽的占用,为服务端应用腾出了更多的给空间。最终,我们通过性能优化保障了本次产品发布会场景化体验效果。

Tips:

  1. 配置超长时间的本地缓存(cache-control) —— 节省带宽,提高性能
  2. 采用内容摘要作为缓存更新依据 —— 精确的缓存控制
  3. 静态资源 CDN 部署 —— 优化网络请求
  4. 更资源发布路径实现非覆盖式发布 —— 平滑升级

14. 如何避免重复渲染

🎯 破题

此题考察面试者是否具备 React 渲染优化的能力,以及相关的避坑经验。

🔢 承题

14

💯 答题

如何避免重复渲染可以分为三个步骤:选择时机、定位重复渲染的问题、引入解决方案:

  1. 优化时机需要根据当前业务标准与页面性能数据分析,来决定是否有必要。如果卡顿的情况在业务要求范围内,那确实没必要做;如果有需要,那就进入下一步——定位问题。
  2. 定位问题首先需要复现问题,通常采用还原用户使用环境的方式进行复现,然后使用 Performance 与 React Profiler 工具进行分析,对照卡顿点与组件重复渲染次数及耗时排查性能问题。
  3. 通常解决方案是加 PureComponent 或者使用 React.memo 等组件缓存 API,减少重新渲染。但错误的使用方式会使其完全无效,比如在 JSX 的属性中使用箭头函数,或者每次都生成新的对象,那基本就破防了。 针对这样每次都生成新的对象,通常有三个解决方案:
  4. 缓存,通常使用 reselect 缓存函数执行结果,来避免产生新的对象;
  5. 不可变数据,使用 Immutable.js 转换数据结构
  6. 手动控制,自己实现 shouldComponentUpdate 函数,但这一类方案一般不大推荐 通过以上手段基本上就能避免无效渲染带来的性能问题了

15. 如何提升 React 代码的可维护性

🎯 破题

此题考察面试者的工程化能力,能否站在 Team Leader 的角度思考问题。

🔢 承题

15

💯 答题

如何提升 React 代码的可维护性,究其根本是考虑如何提升 React 项目的可维护性。从工程化的角度出发,可维护性包含了可分析性、可改变性、稳定性、易测试性与可维护性的依从性,接下来我将从这 5 个方面对想关要点进行梳理。

  1. 可分析性的目标在于能够快速定位线上问题,从预防和兜底两个维度展开工作,预防主要依靠 Lint 工具与团队内部的 Code Review。Lint 工具重在执行代码规划,力图减少不合规的代码;而 Code Review 的重心在于增强团队内部的透明度,做好业务逻辑的潜在风险排查。兜底主要靠发布流水线中加入 sourcemap,能够通过线上报错快速定位问题源码。
  2. 可改变性的目标在于时代吗易于拓展,业务更加易于迭代。工作主要从设计模式与架构设计展开。设计模式主要是指组件设计模式,通过容器组件与展示组件划分模块边界,隔绝业务逻辑。整体架构设计,采用按照业务模块划分 Store 的方案,人事模块,门户模块,CRM 模块的业务逻辑存于独立的 Store,然后整体 export 到 window 下,成为全局的对象,能实现跨模块的状态变更,Mobx action 成为组件唯一的状态提交方式,这样能使得业务代码始终是纯函数的。
  3. 稳定性的目标在于避免代码引起不必要的线上问题,在这方便主要通过提升核心业务代码的测试覆盖率来完成。因为业务发展速度快,UI 变化大,所以基于 UI 的测试整体来看成本过高,但是公司核心业务逻辑的前端代码,比如多人实时协同的在线文档 OT 操作转换代码与基础 SDK,就需要覆盖测试。
  4. 易测试性的目标在于发现代码中的潜在问题。在我负责编写公司的 SDK 过程中,我尽量保证每个模块都是可测试的,在维护组件库的时候,更倾向于编写纯函数和无副作用的代码片段,结合 Typescript 的类型系统和 Jest 的使用,保证了代码通过 CI/CD 发布之前必须通过相应的测试流程。
  5. 可维护性的依从性目标在于建立团队规范,遵循代码约定,提升代码可读性。这方面的工作是引入工具,减少人为犯错的概率。其中主要有检查 Javascript 语法的 ESLint,检查样式的 StyleLint,检查提交内容的 Commitzation,配置编辑器的 VSCode Editor Config,美化代码风格的 Prettier。总体而言,引入提效工具的效果优于文档和口述约定,为团队全体成员提供一套自动化和稳定易用的开发工作流,才能真正保障团队发挥战斗力并持续产出高质量的代码。

16. React Hook 的使用限制有哪些

🎯 破题

此题考察面试者对 React Hook 基本概念和用法情况,以及是否清楚 React 的工作原理。

🔢 承题

16

💯 答题

React Hooks 的使用限制主要有两点:

  1. 不要在循环中、条件或者嵌套函数中调用 Hook;
  2. 在 React 的函数组件中调用 Hook 这样设计的原因在于,Hooks 的设计初衷是为了改进 React 组件的开发模式。在旧的开发模式下遇到3个问题:
  3. 组件之间的状态逻辑难以复用,过去的常见解决方案是高阶组件和 render props 以及状态管理框架。
  4. 复杂的组件变得难以理解,生命周期函数与业务逻辑耦合太深,导致关联部分难以拆分。
  5. 任何机器都很容易混淆类。常见的有 this 指向的问题,虽然类属性的推出,简化了这一步骤,但毕竟它依然是一个语法草案,需要借助 Babel 实现转译。 以上 3 个问题一定程度上阻碍了 React 的后续发展,为了解决它们,React 团队基于函数组件进行设计,而这三个问题决定了 React 只支持 React 函数组件。 那为什么不要在循环,判断或者嵌套函数中调用 Hook 呢?因为 Hooks 的设计师基于数组实现。在调用时按照顺序加入数组中,如果使用循环,判断或者嵌套数组则很可能导致数组取值错位,执行错误的 Hook。当然,实质上 React 源码里的不是数组,而是链表。
    这些使用限制很可能会造成新手一定程度的心智负担,为了避免违反这个规则,可以引入 ESlint 的 Hooks 检查插件进行预防,同时在团队内部需要落实 Code Review。

17. useEffectuseLayoutEffect 区别在哪里

🎯 破题

此题考察面试者对 useEffect 和其他常用 hooks 的熟悉程度。

🔢 承题

17

💯 答题

要谈到他们的区别,我们可以从它们的共同点和不同点两个角度讨论:

  1. 相同点:这两个 hooks 的函数签名相同,入参和调用方式完全一致,都是用来处理副作用的,包括 DOM 操作与样式更改,订阅事件等等。执行时机都在 React 进行 diff 之后准备更新 DOM 之前。
  2. 不同点:使用场景上:Effect 覆盖了绝大多数场景,LayoutEffect 则用于处理 DOM 操作并调整样式和避免页面闪烁。Effect 异步处理副作用,而 LayoutEffect 同步处理副作用,能够解决 DOM 闪烁的问题,正因如此,开发者需要避免在 LayoutEffect 中做大量的耗时计算操作而给 UI 渲染造成阻塞。 官网建议如果在不清楚二者使用的时候,可以选择 useEffect,如需要优化再考虑使用 useLayoutEffect。在 SSR 中无法使用 useLayoutEffect,要做判断。

18. 谈谈 React Hook 的设计模式

🎯 破题

此题是一个开放性的话题,在一个前沿的特性面前考察面试者对 hooks 的使用心得。

🔢 承题

18

💯 答题

React Hooks 目前并未有权威的设计模式,Hooks 的心智模型与 Class 完全不同。我仅仅说一些自己的看法和使用心得:
首先要完全抛弃 Class 时期的生命周期概念,向 effects 思考。比方说在 componentDidMount 中订阅,然后在 componentWillUnmount 中取消订阅。但是在 React Hooks 中只需要使用 useEffect 并将订阅与取消订阅放置在一起,只需要关心外部依赖的数据即可。对于开发者来说,大大降低了心智负担,不用再考虑生命周期的问题。

在这样一种认知上,我总结了一些在团队内部的开发心得,并做成了开发规范进行推广:

  1. 使用 useMemo 取代 React.memo(),因为 React.memo() 并不能控制组件内部共享状态的变化,而 React.useMemo 更是 hooks 的场景
  2. 使用常量的时候,在写类组件的时候,我们普遍习惯将常量写在类中,这意味着每次渲染都会重新声明变量,这是完全无意义的操作。其次,函数组件内的函数每一次都会被重新创建,如果这个函数使用到组件内部的变量时,建议使用 useCallback 包裹一下这个函数。
  3. 在使用 useEffect 时候,有的同学可能会使用引用类型的数据作为依赖项,通常情况下,引用类型的变量很容易篡改,难以判断开发者的真实意图,所以推荐使用数值类型的变量作为依赖项,当然也可以通过使用 JSON.stringify 对引用类型进行序列化,转化成字符串类型,但是比较消耗性能。

以上就是开发实践中的一些操作,就 hooks 的设计模式而言,我的经验就是采用外观模式,将业务逻辑封装到各自的自定义 hook 中,比如用户信息的操作,就可吧删除用户、新增用户、编辑用户等操作封装到同一个自定义 hook 中,而视图组件是抽空的不放置任何状态和业务相关的操作,只负责页面的渲染,只需要调用自定义 hook 暴露出来的接口即可,这样也非常方便测试关键业务逻辑。

19. React-Router 实现原理与工作方式分别是什么

🎯 破题

此题是面试官给你面试者一个很好的展示开源学习能力的机会,可以从原理和开源项目工程设计方面来回答

🔢 承题

19

💯 答题

React Router 库几乎是 React 路由解决方案中的唯一选择,我们可以从两个方面去讨论。
实现原理:

  1. React 的路由内部实现原理分为 2 种,一种是 Hash 路由,可兼容低版本 IE 浏览器,主要是通过 hashChange 监听哈希变化实现;另一种是切换网址中的 Path,通过 H5 History API 实现,主要用到 pushStatereplaceState,但这种方式需要服务器配合实现 historyApiFallback,进行资源的重定向,让所有 path 都指向 index.html。
  2. React Router 的实现主要是通过 history 库完成的,是 React Router 团队为了实现跨平台封装的,内部实现了两套路由机制,一套是基于浏览器的 History API,适配 react-router-dom。一套是基于内存的实现版本,通过数组的方式实现 react-router-native。 工作方式:
  1. 设计模式上,通过 github 仓库我们发现它是一个 Monorepo,使用 lerna 进行多包同一个仓库管理,这样比起建立多个仓库,更有利于多个依赖库之间的协同开发和一起发布。React Router 主要是通过 Context API 实现的,通过顶层的 Router 做生产者,实现路由消息的上下文传递。
  2. 关键模块上,Router 和 MemoryRouter 作为 Context 容器组件,Route,Readirect,Switch 等作为直接消费者,还有与平台有关的一些 UI 功能组件,比如负责跳转的,Link,NavLink,DeepLink 等。

20. React 中你常用的工具库有哪些

🎯 破题

此题考察面试者的技术广度,以及是否具备站在巨人肩膀上创造的潜力,也从侧面反映出开发者日常的 Github 开源使用状况。

🔢 承题

20

💯 答题

常用的工具库都融入了前端开发工作流中,所以接下来我以初始化、开发、构建、检查及发布的顺序进行描述。

  1. 首先是初始化。初始化工程项目一般用官方维护的 create-react-app,这个工具使用起来简单便捷,但 create-react-app 的配置隐藏比较深,修改配置时搭配 react-app-rewired 更为合适。国内的话通常还会用 dva 或者 umi 初始化项目,它们提供了一站式解决方案。dva 更关心数据流领域的问题,而 umi 更关心前端工程化。其次是初始化库,一般会用到 create-react-library,也基本是零配置开始开发,底层用 rollup 进行构建。如果是维护大规模组件的话,通常会使用 StoryBook,它的交互式开发体验可以降低组件库的维护成本。
  2. 再者是开发,开发通常会有路由、样式、基础组件、功能组件、状态管理等五个方面需要处理。路由方面使用 React Router 解决,它底层封装了 HTML5 的 history API 实现前端路由,也支持内存路由。样式方面主要有两个解决方案,分别是 CSS 模块化和 CSS in JS。CSS 模块化主要由 css-loader 完成,而 CSS in JS 比较流行的方案有 emotion 和  styled-components。emotion 提供 props 接口消灭内联样式;styled-components 通过模板字符串提供基础的样式组件。基础组件库方面,一般管理后台使用 Antd,因为用户基数庞大,稳定性好;面向 C 端的话,主要靠团队内部封装组件。功能组件就比较杂了,比如用于实现拖拽的有 react-dnd 和 react-draggable,react-dnd 相对于 react-draggable,在拖放能力的抽象与封装上做得更好,下层差异屏蔽更完善,更适合做跨平台适配;PDF 预览用过 react-pdf-viewer;视频播放用过 Video-React;长列表用过 react-window 与 react-virtualized,两者的作者是同一个人,react-window 相对于 react-virtualized 体积更小,也被作者推荐。最后是状态管理,主要是 Redux 与 Mobx,这两者的区别就很大了,Redux 主要基于全局单一状态的思路,Mobx 主要是基于响应式的思路,更像 Vue。
  3. 然后是构建,构建主要是 webpack、Rollup 与 esBuild。webpack 久经考验,更适合做大型项目的交付;Rollup 常用于打包小型的库,更干净便捷;esBuild 作为新起之秀,性能十分优异,与传统构建器相比,性能最大可以跑出 100 倍的差距,值得长期关注,尤其是与 webpack 结合使用这点,便于优化 webpack 构建性能。
  4. 其次是检查。检查主要是代码规范与代码测试编写。代码规范检查一般是 ESLint,再装插件,属于常规操作。编写代码测试会用到 jest、enzyme、react-testing-library、react-hooks-testing-library:jest 是 Facebook 大力推广的测试框架;enzyme 是 Aribnb 大力推广的测试工具库,基本完整包含了大部分测试场景;react-testing-library 与 react-hooks-testing-library 是由社区主推的测试框架,功能上与 enzyme 部分有所重合。
  5. 最后是发布,我所管理的工程静态资源主要托管在 CDN 上,所以需要在 webpack 中引入上传插件。这里我使用的是 s3-plugin-webpack,主要是识别构建后的静态文件进行上传。开源项目的话,可以使用 jsdeliver 发布静态资源至 CDN.

欢迎关注我 ~

每日精进,专注于探索退休互联网人的新三样: 个人 IP、出海、独立开发

image.png