用结构化的答题思路,口喷 React 核心概念与原理,让面试官绝不敢在你面前装逼超过 3 分钟
。
1. 你真的了解 React 吗
🎯 破题
请面试者谈谈自己对 React 的理解,主动权转移给面试者,开放式问题。
🔢 承题
💯 答题
React 是一个用于构建 UI 用户界面的 JS 框架。
通过组件化的方式解决了视图层开发复用的问题,本质上是一个组件化的框架。它的核心设计思路有三点,分别是声明式,组件化和通用性。
- 声明式的优势在于直观与方便组合;
- 组件化的优势在于视图的拆分和模块复用,更容易做到高内聚低耦合;
- 通用性在于一次学习,随处编写,比如在 React Native,React 360 等;
- 优点:React 的通用性优势,归功于它使用了虚拟 DOM,这使得 React 的适用范围变得更加广阔,无论 Web、Native、VR,甚至是 Shell 应用都能灵活开发。
- 缺点:作为一个视图层的框架,官方并不提供一揽子的解决方案,而是交给社区去实现,比如路由管理方案 React Router,全局状态管理方案 Redux,这样增加了技术选型的成本,以及对初学者上手难度上造成一定的干扰。
不可否认的是,Facebook 以拥抱社区、拥抱开源的方式,促进了 React 生态的繁荣。
2. 为什么 React 要使用 JSX
🎯 破题
考察面试者技术广度,深挖知识面,对流行框架模板方案是否了解,技术方案调研能力。
🔢 承题
💯 答题
在回答问题之前,我先解释一下什么是 JSX。JSX 是一个 JavaScript 的语法扩展,结构类似 XML。
JSX 主要用于声明 React 元素,但 React 中并不强制使用 JSX。即使使用了 JSX,也会在打包构建过程中,使用 Babel 插件编译为 React.createElement。所以 JSX 更像是 React.createElement 的语法糖。 所以从这里可以看出,React 团队并不想引入 Javascript 以外的开发体系。而是希望通过合理的关注点分离保持组件开发的的纯粹性。
接下来与 JSX 以外的三种技术方案进行对比。
- 模板,React 团队认为模板不应该是开发过程中的关注点,会引入模板语法,模板指令等概念,这是一种不佳的实现方案。
- 模板字符串,模板字符串编写的结构会造成多次内部嵌套,使整个结构变得复杂,并且优化代码提示也会变得困难重重。
- JXON,同样是因为代码提示困难被放弃
所以 React 最后选用了 JSX,是因为 JSX 与其设计思想贴合,不需要引入过多的新概念,对编辑器的代码提示也极为友好。
3. 如何避免生命周期中的坑
🎯 破题
请面试者梳理 Reect 各生命周期用途以及使用时的注意事项,说说使用经历。
🔢 承题
💯 答题
避免生命周期中的坑需要做好两件事:
- 不在不恰当的时候调用不该调用的代码
- 在需要调用的时候,不要忘记调用(优化性能,解绑清理事件)
主要有 7 种情况容易造成生命周期的坑:
- getDeriverdStateFromProps 容易造成反模式,使得受控组件和非受控组件区分模糊
- componentWillMout 已标记弃用,主要原因是在新的异步架构下会导致多次被调用,因此网络请求推荐放到 componentDidMount
- componentWillRecieveProps 已标记弃用,被 getDeriverdStateFromProps 取代,主要原因是性能问题
- shouldComponentUpdate 通过返回 true 或 false 确定是否需要重新渲染,用于优化性能
- componentWillUpdate 已标记弃用,可结合 getSnapshotBeforeUpdate 与 componentDidUpdate 改造使用
- 如果在 componentWillUnmout 函数中忘记解除事件绑定,取消定时器等清理操作,容易引发 Bug
- 如果未添加错误处理边界,并通过 componentDidCatch 捕获处理,则用户将看到一个无法操作的白屏
4. 类组件与函数组件有什么区别
🎯 破题
考察面试者对 React 组件两种编写模式的了解程度,是否具备在合适业务场景下选择技术栈的能力。
🔢 承题
💯 答题
首先,我们要知道类组件和函数组件的共同点与不同点:
对组件而言,类组件和函数组件在使用与呈现上没有任何不同,性能表现上,在现代浏览器中也不会有明显差异。
但是在开发者的心智模型上却有着巨大的差异。类组件基于面向对象编程 (OOP),主打继承。生命周期这些核心概念。而函数组件基于函数式编程 (FP),主打 immutable、无副作用、引用透明等特点。
- 使用场景上,如果需要使用生命周期,设计模式上,如果需要使用继承,则推荐类组件。但是,自从 React v16.8 以后,因为 React Hooks 的推出,函数组件完全可以替代类组件使用。同时继承并不是组件最佳的设计模式,官方更加推荐 「组合优于继承」 的设计理念。因此类组件的优势渐渐淡出。
- 性能优化上,类组件主要依靠 shouldComponentUpdate 阻断渲染提升渲染性能;而函数组件依靠 React.memo 缓存渲染结果并跳过渲染来提升性能。
- 上手难度上,类组件更容易上手。
- 未来趋势上,由于 React Hooks 的推出,函数组件成为了社区未来主推的方案。 类组件在未来时间切片与并发模式中,由于生命周期带来的复杂度,并不易于优化。而函数组件本身清凉简单,且在 Hooks 的基础上提供了比原先更加细粒度的逻辑组织与复用,更能适应 React 未来的发展。
5. 如何设计 React 组件
🎯 破题
请面试者结合自己的使用经历谈谈 React 组件的设计模式,接手一个新项目要如何进行目录结构划分,考察面试者的前端工程化实践水平。
🔢 承题
💯 答题
React 组件应该从设计分类和工程实践两个方向进行讨论。
从设计分类的角度:
- 展示组件:其逻辑较为简单,一般用于 UI 视图界面的展示表达。主要包括代理组件,样式组件,布局组件。代理组件一个很经典的使用场景就是引入 Antd 组件库时,在每个组件上封装一层,如未来需要更新 Antd 或者定制化开发时,只需要维护代理组件即可。
- 灵巧组件:其主要面向具体业务,功能丰富,复杂度更高,主要包括容器组件和高阶组件。在实际开发中,我们一般将网络请求,和事件处理放在容器组件中,与展示组件隔离开。高阶组件类似于高阶函数,入参为函数,出参依然为函数,一般用于抽取公共业务逻辑或者提供某一项公用能力。主要使用场景包括登录检测和埋点统计,高阶组件可以通过链式调用完成一系列连贯的业务操作,除此之外,高阶函数还可以用于渲染劫持,通过继承原组件然后重写 render() 来达到修改渲染结果的目的,这种方式可用于应用的无侵入二次开发。同时高阶函数也有两个缺陷:1. 静态方法无法被外部直接调用,可以通过复制静态方法的形式实现;2. refs 不能透传,需要使用 React.forwardRef 进行转发。 从工程实践的角度: 通过文件夹划分的方式切分代码。将页面单独放置与 src/pages 中,通常为一些页面的无法复用的业务逻辑代码;将复用性较高的组件抽离出来放于 src/components,其中 src/component/basic 文件夹用于放置展示组件,由于展示组件与特务的关联性较低,可以使用 Storybook 进行 UI 组件的开发和单元测试,从而提升整个工程的质量和效率。
6. setState
是同步还是异步更新
🎯 破题
考察面试者对 React 原理的掌握程度,setState 的概念和用途,在那些场景下是同步操作,哪些场景下是异步操作。
🔢 承题
💯 答题
setState 并非真正异步,只是看上去像是一个异步。在源码中,通过 isBatchingUpdates 来判断 setState 是先存入 state 队列还是直接更新,如果值为 true 则执行异步操作,为 false 则执行更新。
那么什么情况下 isBatchingUpdates 会为 true 呢?在 React 可控制的地方,就为 true,比如在 React 生命周期事件和合成事件中,都会走合并操作,延迟更新的策略。
但在 React 无法控制的地方,比如原生事件 (addEventListener / setTimeout / setInterval) 中只能同步更新。
一般认为,异步设计的目的在于性能优化,减少渲染次数,React 还补充了两点:
- 保持内部一致性。如果将 setState 改为同步更新,那么尽管 state 的更新是同步的,但 props 不是。
- 启用并发更新,完成异步渲染。
7. 如何面向组件跨层级通信
🎯 破题
考察面试者对 React 组件通信方式的掌握程度,结合自己的实际开发经历,谈谈跨组件通讯的主要场景。
🔢 承题
💯 答题
在组件跨层级通信的过程中,主要分类一层与多层的情况。
一层情况下:
- 父组件向子组件通信:仅通过 Props 传递即可,这是最常见的一种方式,符合 React 自顶向下的单向数据流设计哲学。
- 子组件向父组件通信:主要通过回调函数的方式实现,这也是诸如 antd 等组件库的组件 API 设计方式,比如
<Button onClick={...} />
- 兄弟组件之间通信:可由父组件作为中转,结合以上两种方式进行通信。 多层情况下:
- React Context API,一般用于全局化的配置,比如组件库的多语言包,全局主题切换等等。
- 全局对象和全局事件,React 应用中将状态挂载到 Window 对象下,用于存储临时变量,但是并不推荐这种方式。
- 使用状态管理框架 Flux、Redux、Mobx,引入状态管理框架之后,也许开发模式和代码结构会得到约束,但缺点是学习成本较高。
8. 列举一种你了解的 React 状态管理框架
🎯 破题
考察面试者对 React 状态管理框架在复杂业务场景下的使用经验。
🔢 承题
💯 答题
待更新 ...
9. Virtual DOM 的工作原理是什么
🎯 破题
考察面试者对 React 核心概念 Virtual DOM 的理解程度。
🔢 承题
💯 答题
虚拟 DOM 的工作原理是通过 JS 对象模拟真实的 DOM 节点实现的。在 Facebook 构建 React 初期时,考虑到提升代码抽象能力,避免人为的 DOM 操作,降低代码整体风险等因素,从而引入了虚拟 DOM 的概念。
实现原理上:虚拟 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 操作。优点:
改善大规模的 DOM 操作
规避 XSS 风险
较低的成本实现跨平台开发
缺点:
- 内存占用较高,需要模拟整个网页的真实 DOM
- 高性能应用场景下难以优化的情况,比如 Google Earth 应用不会选择 React 虚拟 DOM 的应用场景不仅仅是渲染,还可用于
埋点统计
和数据记录
,只要用户界面的 DOM 发生真实有效的变更,都可以将用户的交互行为上报给服务器,实现监控效果。
10. 与其他框架相比,React 的 Diff 算法有何不同
🎯 破题
考察面试者对 React Diff 算法的理解程度,同时需要面试者技术视野广泛,结合社区其他框架的 Diff 方案做比较。
🔢 承题
💯 答题
首先,Diff 算法是指生成更新补丁的方式,主要应用于虚拟 DOM 树变化后,更新真实 DOM。因此 Diff 算法一定存在这样一个过程:触发更新 -> 生成补丁 -> 应用补丁。
React diff 算法触发更新的时机主要在 setState 变化和 hooks 调用之后,此时,触发虚拟 DOM 树变更遍历,React 采用深度优先遍历算法,但传统的遍历方式,效率较低,复杂度为 O(n^3)。为了提升效率, React 采用计算机科学中的「分治」这一经典的手法,分而治之的巧妙分解问题,将单一节点对比,转化为 3 种类型节点的对比,对应 diff 的三种优化策略:
- 树对比:只对同一层级节点进行比较
- 组件对比:同类型组件才会对比,否则直接放入 patch
- 元素对比:同层级节点中,通过设置 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 算法的原理,在开发中如何优化代码呢?
- 尽量避免跨层级节点移动
- 设置唯一的 key 值进行优化
- 尽量减少组件的层级深度,从而降低树遍历的深度
- 设置 shouldComponent 或者使用 React.PureComponent 减少 diff 次数
11. 如何解释 React 的渲染流程
🎯 破题
此题是一个非常宽泛的开放性题目,考察的不仅仅是面试者的知识储备,还有一定的沟通表达技巧,需要面试者的回答做到「讲话有重点,层次要分明」,做到对 React 渲染流程的归纳总结。
🔢 承题
💯 答题
React 的渲染流程一直没有太大变化,但是协调机制却发生巨大的改变,以 React v16 为分界线,v16 之前采用 Stack Reconciler 栈调和,v16 之后采用的是 Fiber Reconciler 协作式多任务调和。这里提到的协调狭义上来说就是 React 的 diff 算法,广义上来是指的 React 中的 Reconciler 模块,它通常包含了 diff 算法和一些公共的逻辑。
Stack Reconciler 栈调和的核心调度方式是递归。调度的基本处理单位是 Transaction 事务,React 源码中有一个 Transaction 的基类,这个概念是 React 团队从后端开发中借鉴的概念,它能够保证协调过程中对状态操作的一致性和原子性。协调过程中,ReactMount 模块负责挂载, ReactUpdate 模块负责更新,各个模块之间相互分离,通过事务来驱动执行。
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 渲染异常引发的应用级崩溃采取过相应的解决方案。
🔢 承题
💯 答题
React 渲染异常时,在没有做出任何处理的情况下,会出现整个页面白屏的现象。它的主要原因是渲染层出现了 Javascript 错误,导致整个 React 应用崩溃。通常这种错误的原因是在 render 中未对数值进行非空校验,没有控制好空安全。在实际项目中,可以采取两个方向去治理:
- 预防:引入空安全相关的方案,在做技术选型的时候,主要考虑3种方式:第一个是引入第三方库,比如 Facebook 的 idx 或者 Lodash.get;第二个是引入 Babel 插件,使用 ES2020 的语法标准——可选链操作符;第三个是 Typescript,它在 3.7 版本之后可以直接使用可选链操作符。最后我选择了引入 Babel 插件的方案,因为这个方案外部依赖比较少,侵入性小,而且团队内老项目还未上 TS。
- 兜底:如果技术上只解决一个人的问题,价值是不够的的,要解决团队的公共问题,价值才会得到放大,空安全的问题,需要编写一个公共的高阶组件,去处理错误边界情况下的 UI 渲染,封装成 NPM 包供团队内部使用。 从治理的效果上看,预防和治理方案覆盖了团队内 100% 的 React 项目,头三个月兜底组件统计到日均 10 次的报警信息,其中有 10% 是公司的关键业务。经过分析,首先是为关键的 UI 组件添加兜底组件进行拦截,然后上报统计,针对暴露出来的问题,在团队内部做一次培训,对易错点的代码进行指导,加强 Code Review。后续到现在,线上只收到过 1 次报警。
13. 如何分析和调优性能瓶颈
🎯 破题
此题考察面试者是否有优化网站性能的经验,是否会使用相关工具和指标进行性能调优。
🔢 承题
💯 答题
我目前在公司负责的业务是 Saas 产品体验站点的开发,复杂交互的 UI 场景下需要考虑渲染性能和静态资源加载的问题。通过一系列性能优化后,整个用户体验满意度提升了 20%,页面加载速度有了显著提升。
在我加入团队之前,部门基本上没有性能优化的相关指标和数据,我着手接入了 hotjar 第三方用户体验反馈工具,才有了相关的性能指标。然后我对指标观察了一周,思考了目前的业务形态,发现 Saas 产品体验站点业务形态与管理后台较为接近,对加载速度没有 2C 端要求那么苛刻,但对系统稳定性要求比较高。
同时,我们发现静态资源的加载在并发 3000 的状况下加载速度变慢,这显然不能满足企业销售大会千人同时体验该站点的性能要求,于是我们对后端和前端分别进行了优化,后端由原来的单机架构改成拥有 3 个节点的集群架构,前端主要是我改变了 Webpack 构建工作流,将静态资源发布到 CDN。
结果显而易见,发布会当天支撑了上万人的同时在线对我们的 Saas 产品进行实时体验,前端的性能优化降低了服务器带宽的占用,为服务端应用腾出了更多的给空间。最终,我们通过性能优化保障了本次产品发布会场景化体验效果。
Tips:
- 配置超长时间的本地缓存(cache-control) —— 节省带宽,提高性能
- 采用内容摘要作为缓存更新依据 —— 精确的缓存控制
- 静态资源 CDN 部署 —— 优化网络请求
- 更资源发布路径实现非覆盖式发布 —— 平滑升级
14. 如何避免重复渲染
🎯 破题
此题考察面试者是否具备 React 渲染优化的能力,以及相关的避坑经验。
🔢 承题
💯 答题
如何避免重复渲染可以分为三个步骤:选择时机、定位重复渲染的问题、引入解决方案:
- 优化时机需要根据当前业务标准与页面性能数据分析,来决定是否有必要。如果卡顿的情况在业务要求范围内,那确实没必要做;如果有需要,那就进入下一步——定位问题。
- 定位问题首先需要复现问题,通常采用还原用户使用环境的方式进行复现,然后使用 Performance 与 React Profiler 工具进行分析,对照卡顿点与组件重复渲染次数及耗时排查性能问题。
- 通常解决方案是加 PureComponent 或者使用 React.memo 等组件缓存 API,减少重新渲染。但错误的使用方式会使其完全无效,比如在 JSX 的属性中使用箭头函数,或者每次都生成新的对象,那基本就破防了。 针对这样每次都生成新的对象,通常有三个解决方案:
- 缓存,通常使用 reselect 缓存函数执行结果,来避免产生新的对象;
- 不可变数据,使用 Immutable.js 转换数据结构
- 手动控制,自己实现 shouldComponentUpdate 函数,但这一类方案一般不大推荐 通过以上手段基本上就能避免无效渲染带来的性能问题了
15. 如何提升 React 代码的可维护性
🎯 破题
此题考察面试者的工程化能力,能否站在 Team Leader 的角度思考问题。
🔢 承题
💯 答题
如何提升 React 代码的可维护性,究其根本是考虑如何提升 React 项目的可维护性。从工程化的角度出发,可维护性包含了可分析性、可改变性、稳定性、易测试性与可维护性的依从性,接下来我将从这 5 个方面对想关要点进行梳理。
- 可分析性的目标在于能够快速定位线上问题,从预防和兜底两个维度展开工作,预防主要依靠 Lint 工具与团队内部的 Code Review。Lint 工具重在执行代码规划,力图减少不合规的代码;而 Code Review 的重心在于增强团队内部的透明度,做好业务逻辑的潜在风险排查。兜底主要靠发布流水线中加入 sourcemap,能够通过线上报错快速定位问题源码。
- 可改变性的目标在于时代吗易于拓展,业务更加易于迭代。工作主要从设计模式与架构设计展开。设计模式主要是指组件设计模式,通过容器组件与展示组件划分模块边界,隔绝业务逻辑。整体架构设计,采用按照业务模块划分 Store 的方案,人事模块,门户模块,CRM 模块的业务逻辑存于独立的 Store,然后整体 export 到 window 下,成为全局的对象,能实现跨模块的状态变更,Mobx action 成为组件唯一的状态提交方式,这样能使得业务代码始终是纯函数的。
- 稳定性的目标在于避免代码引起不必要的线上问题,在这方便主要通过提升核心业务代码的测试覆盖率来完成。因为业务发展速度快,UI 变化大,所以基于 UI 的测试整体来看成本过高,但是公司核心业务逻辑的前端代码,比如多人实时协同的在线文档 OT 操作转换代码与基础 SDK,就需要覆盖测试。
- 易测试性的目标在于发现代码中的潜在问题。在我负责编写公司的 SDK 过程中,我尽量保证每个模块都是可测试的,在维护组件库的时候,更倾向于编写纯函数和无副作用的代码片段,结合 Typescript 的类型系统和 Jest 的使用,保证了代码通过 CI/CD 发布之前必须通过相应的测试流程。
- 可维护性的依从性目标在于建立团队规范,遵循代码约定,提升代码可读性。这方面的工作是引入工具,减少人为犯错的概率。其中主要有检查 Javascript 语法的 ESLint,检查样式的 StyleLint,检查提交内容的 Commitzation,配置编辑器的 VSCode Editor Config,美化代码风格的 Prettier。总体而言,引入提效工具的效果优于文档和口述约定,为团队全体成员提供一套自动化和稳定易用的开发工作流,才能真正保障团队发挥战斗力并持续产出高质量的代码。
16. React Hook 的使用限制有哪些
🎯 破题
此题考察面试者对 React Hook 基本概念和用法情况,以及是否清楚 React 的工作原理。
🔢 承题
💯 答题
React Hooks 的使用限制主要有两点:
- 不要在循环中、条件或者嵌套函数中调用 Hook;
- 在 React 的函数组件中调用 Hook 这样设计的原因在于,Hooks 的设计初衷是为了改进 React 组件的开发模式。在旧的开发模式下遇到3个问题:
- 组件之间的状态逻辑难以复用,过去的常见解决方案是高阶组件和 render props 以及状态管理框架。
- 复杂的组件变得难以理解,生命周期函数与业务逻辑耦合太深,导致关联部分难以拆分。
- 任何机器都很容易混淆类。常见的有 this 指向的问题,虽然类属性的推出,简化了这一步骤,但毕竟它依然是一个语法草案,需要借助 Babel 实现转译。 以上 3 个问题一定程度上阻碍了 React 的后续发展,为了解决它们,React 团队基于函数组件进行设计,而这三个问题决定了 React 只支持 React 函数组件。 那为什么不要在循环,判断或者嵌套函数中调用 Hook 呢?因为 Hooks 的设计师基于数组实现。在调用时按照顺序加入数组中,如果使用循环,判断或者嵌套数组则很可能导致数组取值错位,执行错误的 Hook。当然,实质上 React 源码里的不是数组,而是链表。
这些使用限制很可能会造成新手一定程度的心智负担,为了避免违反这个规则,可以引入 ESlint 的 Hooks 检查插件进行预防,同时在团队内部需要落实 Code Review。
17. useEffect
和 useLayoutEffect
区别在哪里
🎯 破题
此题考察面试者对 useEffect 和其他常用 hooks 的熟悉程度。
🔢 承题
💯 答题
要谈到他们的区别,我们可以从它们的共同点和不同点两个角度讨论:
- 相同点:这两个 hooks 的函数签名相同,入参和调用方式完全一致,都是用来处理副作用的,包括 DOM 操作与样式更改,订阅事件等等。执行时机都在 React 进行 diff 之后准备更新 DOM 之前。
- 不同点:使用场景上:Effect 覆盖了绝大多数场景,LayoutEffect 则用于处理 DOM 操作并调整样式和避免页面闪烁。Effect 异步处理副作用,而 LayoutEffect 同步处理副作用,能够解决 DOM 闪烁的问题,正因如此,开发者需要避免在 LayoutEffect 中做大量的耗时计算操作而给 UI 渲染造成阻塞。 官网建议如果在不清楚二者使用的时候,可以选择 useEffect,如需要优化再考虑使用 useLayoutEffect。在 SSR 中无法使用 useLayoutEffect,要做判断。
18. 谈谈 React Hook 的设计模式
🎯 破题
此题是一个开放性的话题,在一个前沿的特性面前考察面试者对 hooks 的使用心得。
🔢 承题
💯 答题
React Hooks 目前并未有权威的设计模式,Hooks 的心智模型与 Class 完全不同。我仅仅说一些自己的看法和使用心得:
首先要完全抛弃 Class 时期的生命周期概念,向 effects 思考。比方说在componentDidMount
中订阅,然后在componentWillUnmount
中取消订阅。但是在 React Hooks 中只需要使用useEffect
并将订阅与取消订阅放置在一起,只需要关心外部依赖的数据即可。对于开发者来说,大大降低了心智负担,不用再考虑生命周期的问题。在这样一种认知上,我总结了一些在团队内部的开发心得,并做成了开发规范进行推广:
- 使用
useMemo
取代React.memo()
,因为React.memo()
并不能控制组件内部共享状态的变化,而React.useMemo
更是 hooks 的场景- 使用常量的时候,在写类组件的时候,我们普遍习惯将常量写在类中,这意味着每次渲染都会重新声明变量,这是完全无意义的操作。其次,函数组件内的函数每一次都会被重新创建,如果这个函数使用到组件内部的变量时,建议使用
useCallback
包裹一下这个函数。- 在使用
useEffect
时候,有的同学可能会使用引用类型的数据作为依赖项,通常情况下,引用类型的变量很容易篡改,难以判断开发者的真实意图,所以推荐使用数值类型的变量作为依赖项,当然也可以通过使用JSON.stringify
对引用类型进行序列化,转化成字符串类型,但是比较消耗性能。以上就是开发实践中的一些操作,就 hooks 的设计模式而言,我的经验就是采用外观模式,将业务逻辑封装到各自的自定义 hook 中,比如用户信息的操作,就可吧删除用户、新增用户、编辑用户等操作封装到同一个自定义 hook 中,而视图组件是抽空的不放置任何状态和业务相关的操作,只负责页面的渲染,只需要调用自定义 hook 暴露出来的接口即可,这样也非常方便测试关键业务逻辑。
19. React-Router 实现原理与工作方式分别是什么
🎯 破题
此题是面试官给你面试者一个很好的展示开源学习能力的机会,可以从原理和开源项目工程设计方面来回答
🔢 承题
💯 答题
React Router 库几乎是 React 路由解决方案中的唯一选择,我们可以从两个方面去讨论。
实现原理:
- React 的路由内部实现原理分为 2 种,一种是 Hash 路由,可兼容低版本 IE 浏览器,主要是通过 hashChange 监听哈希变化实现;另一种是切换网址中的 Path,通过 H5 History API 实现,主要用到
pushState
和replaceState
,但这种方式需要服务器配合实现 historyApiFallback,进行资源的重定向,让所有 path 都指向 index.html。- React Router 的实现主要是通过 history 库完成的,是 React Router 团队为了实现跨平台封装的,内部实现了两套路由机制,一套是基于浏览器的 History API,适配 react-router-dom。一套是基于内存的实现版本,通过数组的方式实现 react-router-native。 工作方式:
- 设计模式上,通过 github 仓库我们发现它是一个 Monorepo,使用 lerna 进行多包同一个仓库管理,这样比起建立多个仓库,更有利于多个依赖库之间的协同开发和一起发布。React Router 主要是通过 Context API 实现的,通过顶层的 Router 做生产者,实现路由消息的上下文传递。
- 关键模块上,Router 和 MemoryRouter 作为 Context 容器组件,Route,Readirect,Switch 等作为直接消费者,还有与平台有关的一些 UI 功能组件,比如负责跳转的,Link,NavLink,DeepLink 等。
20. React 中你常用的工具库有哪些
🎯 破题
此题考察面试者的技术广度,以及是否具备站在巨人肩膀上创造的潜力,也从侧面反映出开发者日常的 Github 开源使用状况。
🔢 承题
💯 答题
常用的工具库都融入了前端开发工作流中,所以接下来我以初始化、开发、构建、检查及发布的顺序进行描述。
- 首先是初始化。初始化工程项目一般用官方维护的 create-react-app,这个工具使用起来简单便捷,但 create-react-app 的配置隐藏比较深,修改配置时搭配 react-app-rewired 更为合适。国内的话通常还会用 dva 或者 umi 初始化项目,它们提供了一站式解决方案。dva 更关心数据流领域的问题,而 umi 更关心前端工程化。其次是初始化库,一般会用到 create-react-library,也基本是零配置开始开发,底层用 rollup 进行构建。如果是维护大规模组件的话,通常会使用 StoryBook,它的交互式开发体验可以降低组件库的维护成本。
- 再者是开发,开发通常会有路由、样式、基础组件、功能组件、状态管理等五个方面需要处理。路由方面使用 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。
- 然后是构建,构建主要是 webpack、Rollup 与 esBuild。webpack 久经考验,更适合做大型项目的交付;Rollup 常用于打包小型的库,更干净便捷;esBuild 作为新起之秀,性能十分优异,与传统构建器相比,性能最大可以跑出 100 倍的差距,值得长期关注,尤其是与 webpack 结合使用这点,便于优化 webpack 构建性能。
- 其次是检查。检查主要是代码规范与代码测试编写。代码规范检查一般是 ESLint,再装插件,属于常规操作。编写代码测试会用到 jest、enzyme、react-testing-library、react-hooks-testing-library:jest 是 Facebook 大力推广的测试框架;enzyme 是 Aribnb 大力推广的测试工具库,基本完整包含了大部分测试场景;react-testing-library 与 react-hooks-testing-library 是由社区主推的测试框架,功能上与 enzyme 部分有所重合。
- 最后是发布,我所管理的工程静态资源主要托管在 CDN 上,所以需要在 webpack 中引入上传插件。这里我使用的是 s3-plugin-webpack,主要是识别构建后的静态文件进行上传。开源项目的话,可以使用 jsdeliver 发布静态资源至 CDN.
欢迎关注我 ~
每日精进,专注于探索退休互联网人的新三样: 个人 IP、出海、独立开发