react 高频面试题总结(一)
React Hook 的使用限制有哪些?
React Hooks 的限制主要有两条:
不要在循环、条件或嵌套函数中调用 Hook;
在 React 的函数组件中调用 Hook。
那为什么会有这样的限制呢?Hooks 的设计初衷是为了改进 React 组件的开发模式。在旧有的开发模式下遇到了三个问题。
组件之间难以复用状态逻辑。过去常见的解决方案是高阶组件、render props 及状态管理框架。
复杂的组件变得难以理解。生命周期函数与业务逻辑耦合太深,导致关联部分难以拆分。
人和机器都很容易混淆类。常见的有 this 的问题,但在 React 团队中还有类难以优化的问题,希望在编译优化层面做出一些改进。
这三个问题在一定程度上阻碍了 React 的后续发展,所以为了解决这三个问题,Hooks 基于函数组件开始设计。然而第三个问题决定了 Hooks 只支持函数组件。
那为什么不要在循环、条件或嵌套函数中调用 Hook 呢?因为 Hooks 的设计是基于数组实现。在调用时按顺序加入数组中,如果使用循环、条件或嵌套函数很有可能导致数组取值错位,执行错误的 Hook。当然,实质上 React 的源码里不是数组,是链表。
这些限制会在编码上造成一定程度的心智负担,新手可能会写错,为了避免这样的情况,可以引入 ESLint 的 Hooks 检查插件进行预防。
react16 版本的 reconciliation 阶段和 commit 阶段是什么
reconciliation 阶段包含的主要工作是对 current tree 和 new tree 做 diff 计算,找出变化部分。进行遍历、对比等是可以中断,歇一会儿接着再来。
commit 阶段是对上一阶段获取到的变化部分应用到真实的 DOM 树中,是一系列的 DOM 操作。不仅要维护更复杂的 DOM 状态,而且中断后再继续,会对用户体验造成影响。在普遍的应用场景下,此阶段的耗时比 diff 计算等耗时相对短。
state 是怎么注入到组件的,从 reducer 到组件经历了什么样的过程
通过 connect 和 mapStateToProps 将 state 注入到组件中:
上面代码中,active 就是注入到 Link 组件中的状态。 mapStateToProps(state,ownProps)中带有两个参数,含义是∶
state-store 管理的全局状态对象,所有都组件状态数据都存储在该对象中。
ownProps 组件通过 props 传入的参数。
reducer 到组件经历的过程:
reducer 对 action 对象处理,更新组件状态,并将新的状态值返回 store。
通过 connect(mapStateToProps,mapDispatchToProps)(Component)对组件 Component 进行升级,此时将状态值从 store 取出并作为 props 参数传递到组件。
高阶组件实现源码∶
Redux 请求中间件如何处理并发
使用 redux-Saga redux-saga 是一个管理 redux 应用异步操作的中间件,用于代替 redux-thunk 的。它通过创建 Sagas 将所有异步操作逻辑存放在一个地方进行集中处理,以此将 react 中的同步操作与异步操作区分开来,以便于后期的管理与维护。 redux-saga 如何处理并发:
takeEvery
可以让多个 saga 任务并行被 fork 执行。
takeLatest
takeLatest 不允许多个 saga 任务并行地执行。一旦接收到新的发起的 action,它就会取消前面所有 fork 过的任务(如果这些任务还在执行的话)。在处理 AJAX 请求的时候,如果只希望获取最后那个请求的响应, takeLatest 就会非常有用。
**
React 与 Vue 的 diff 算法有何不同?
diff 算法是指生成更新补丁的方式,主要应用于虚拟 DOM 树变化后,更新真实 DOM。所以 diff 算法一定存在这样一个过程:触发更新 → 生成补丁 → 应用补丁。
React 的 diff 算法,触发更新的时机主要在 state 变化与 hooks 调用之后。此时触发虚拟 DOM 树变更遍历,采用了深度优先遍历算法。但传统的遍历方式,效率较低。为了优化效率,使用了分治的方式。将单一节点比对转化为了 3 种类型节点的比对,分别是树、组件及元素,以此提升效率。
树比对:由于网页视图中较少有跨层级节点移动,两株虚拟 DOM 树只对同一层次的节点进行比较。
组件比对:如果组件是同一类型,则进行树比对,如果不是,则直接放入到补丁中。
元素比对:主要发生在同层级中,通过标记节点操作生成补丁,节点操作对应真实的 DOM 剪裁操作。
以上是经典的 React diff 算法内容。自 React 16 起,引入了 Fiber 架构。为了使整个更新过程可随时暂停恢复,节点与树分别采用了 FiberNode 与 FiberTree 进行重构。fiberNode 使用了双链表的结构,可以直接找到兄弟节点与子节点。整个更新过程由 current 与 workInProgress 两株树双缓冲完成。workInProgress 更新完成后,再通过修改 current 相关指针指向新节点。
Vue 的整体 diff 策略与 React 对齐,虽然缺乏时间切片能力,但这并不意味着 Vue 的性能更差,因为在 Vue 3 初期引入过,后期因为收益不高移除掉了。除了高帧率动画,在 Vue 中其他的场景几乎都可以使用防抖和节流去提高响应性能。
React 事件机制
React 并不是将 click 事件绑定到了 div 的真实 DOM 上,而是在 document 处监听了所有的事件,当事件发生并且冒泡到 document 处的时候,React 将事件内容封装并交由真正的处理函数运行。这样的方式不仅仅减少了内存的消耗,还能在组件挂在销毁时统一订阅和移除事件。
除此之外,冒泡到 document 上的事件也不是原生的浏览器事件,而是由 react 自己实现的合成事件(SyntheticEvent)。因此如果不想要是事件冒泡的话应该调用 event.preventDefault()方法,而不是调用 event.stopProppagation()方法。 JSX 上写的事件并没有绑定在对应的真实 DOM 上,而是通过事件代理的方式,将所有的事件都统一绑定在了 document
上。这样的方式不仅减少了内存消耗,还能在组件挂载销毁时统一订阅和移除事件。
另外冒泡到 document
上的事件也不是原生浏览器事件,而是 React 自己实现的合成事件(SyntheticEvent)。因此我们如果不想要事件冒泡的话,调用 event.stopPropagation
是无效的,而应该调用 event.preventDefault
。
实现合成事件的目的如下:
合成事件首先抹平了浏览器之间的兼容问题,另外这是一个跨浏览器原生事件包装器,赋予了跨浏览器开发的能力;
对于原生浏览器事件来说,浏览器会给监听器创建一个事件对象。如果你有很多的事件监听,那么就需要分配很多的事件对象,造成高额的内存分配问题。但是对于合成事件来说,有一个事件池专门来管理它们的创建和销毁,当事件需要被使用时,就会从池子中复用对象,事件回调结束后,就会销毁事件对象上的属性,从而便于下次复用事件对象。
请说岀 React 从 EMAScript5 编程规范到 EMAScript6 编程规范过程中的几点改变。
主要改变如下。(1)创建组件的方法不同。EMAScript5 版本中,定义组件用 React.createClass。EMAScript6 版本中,定义组件要定义组件类,并继承 Component 类。(2)定义默认属性的方法不同。EMAScript5 版本中,用 getDefaultProps 定义默认属性。EMAScript6 版本中,为组件定义 defaultProps 静态属性,来定义默认属性。(3)定义初始化状态的方法不同。EMAScript5 版本中,用 getInitialState 定义初始化状态。EMAScript6 版本中,在构造函数中,通过 this. state 定义初始化状态。注意:构造函数的第一个参数是属性数据,一定要用 super 继承。(4)定义属性约束的方法不同。EMAScript5 版本中,用 propTypes 定义属性的约束。EMAScript6 版本中,为组件定义 propsTypes 静态属性,来对属性进行约束。(5)使用混合对象、混合类的方法不同。EMAScript5 版本中,通过 mixins 继承混合对象的方法。EMAScript6 版本中,定义混合类,让混合类继承 Component 类,然后让组件类继承混合类,实现对混合类方法的继承。(6)绑定事件的方法不同。EMAScript5 版本中,绑定的事件回调函数作用域是组件实例化对象。EMAScript6 版本中,绑定的事件回调函数作用域是 null。(7)父组件传递方法的作用域不同。EMAScript5 版本中,作用域是父组件。 EMAScript6 版本中,变成了 null。(8)组件方法作用域的修改方法不同。EMAScript5 版本中,无法改变作用域。EMAScript6 版本中,作用域是可以改变的。
为何 React 事件要自己绑定 this
在 React 源码中,当具体到某一事件处理函数将要调用时,将调用 invokeGuardedCallback 方法。
事件处理函数是直接调用的,并没有指定调用的组件,所以不进行手动绑定的情况下直接获取到的 this 是不准确的,所以我们需要手动将当前组件绑定到 this 上
shouldComponentUpdate 有什么用?为什么它很重要?
组件状态数据或者属性数据发生更新的时候,组件会进入存在期,视图会渲染更新。在生命周期方法 should ComponentUpdate 中,允许选择退出某些组件(和它们的子组件)的和解过程。和解的最终目标是根据新的状态,以最有效的方式更新用户界面。如果我们知道用户界面的某一部分不会改变,那么没有理由让 React 弄清楚它是否应该更新渲染。通过在 shouldComponentUpdate 方法中返回 false, React 将让当前组件及其所有子组件保持与当前组件状态相同。
React 最新的⽣命周期是怎样的?
React 16 之后有三个⽣命周期被废弃(但并未删除)
componentWillMount
componentWillReceiveProps
componentWillUpdate
官⽅计划在 17 版本完全删除这三个函数,只保留 UNSAVE_前缀的三个函数,⽬的是为了向下兼容,但是对于开发者⽽⾔应该尽量避免使⽤他们,⽽是使⽤新增的⽣命周期函数替代它们。
⽬前 React16.8+的⽣命周期分为三个阶段,分别是挂载阶段、更新阶段、卸载阶段。
挂载阶段:
constructor:构造函数,最先被执⾏,我们通常在构造函数⾥初始化 state 对象或者给⾃定义⽅法绑定 this;
getDerivedStateFromProps:static getDerivedStateFromProps(nextProps, prevState),这是个静态⽅法,当我们接收到新的属性想去修改我们 state, 可以使⽤getDerivedStateFromProps
render:render 函数是纯函数,只返回需要渲染的东⻄,不应该包含其它的业务逻辑,可以返回原⽣的 DOM、React 组件、Fragment、Portals、字符串和数字、 Boolean 和 null 等内容;
componentDidMount:组件装载之后调⽤,此时我们可以获取到 DOM 节点并操作,⽐如对 canvas,svg 的操作,服务器请求,订阅都可以写在这个⾥⾯,但是记得在 componentWillUnmount 中取消订阅;
更新阶段:
getDerivedStateFromProps: 此⽅法在更新个挂载阶段都可能会调⽤;
shouldComponentUpdate:shouldComponentUpdate(nextProps, nextState),有两个参数 nextProps 和 nextState,表示新的属性和变化之后的 state,返回⼀个布尔值,true 表示会触发重新渲染,false 表示不会触发重新渲染,默认返回 true,我们通常利⽤此⽣命周期来优化 React 程序性能;
render:更新阶段也会触发此⽣命周期;
getSnapshotBeforeUpdate:getSnapshotBeforeUpdate(prevProps, prevState),这个⽅法在 render 之后,componentDidUpdate 之前调⽤,有两个参数 prevProps 和 prevState,表示之前的属性和之前的 state,这个函数有⼀个返回值,会作为第三个参数传给 componentDidUpdate,如果你不想要返回值,可以返回 null,此⽣命周期必须与 componentDidUpdate 搭配使⽤;
componentDidUpdate:componentDidUpdate(prevProps, prevState, snapshot),该⽅法在 getSnapshotBeforeUpdate⽅法之后被调⽤,有三个参数 prevProps,prevState,snapshot,表示之前的 props,之前的 state,和 snapshot。第三个参数是 getSnapshotBeforeUpdate 返回的,如果触发某些回调函数时需要⽤到 DOM 元素的状态,则将对⽐或计算的过程迁移⾄getSnapshotBeforeUpdate,然后在 componentDidUpdate 中统⼀触发回调或更新状态。
卸载阶段:
-componentWillUnmount:当我们的组件被卸载或者销毁了就会调⽤,我们可以在这个函数⾥去清除⼀些定时器,取消⽹络请求,清理⽆效的 DOM 元素等垃圾清理⼯作。
总结:
componentWillMount:在渲染之前执行,用于根组件中的 App 级配置;
componentDidMount:在第一次渲染之后执行,可以在这里做 AJAX 请求,DOM 的操作或状态更新以及设置事件监听器;
componentWillReceiveProps:在初始化 render 的时候不会执行,它会在组件接受到新的状态(Props)时被触发,一般用于父组件状态更新时子组件的重新渲染
shouldComponentUpdate:确定是否更新组件。默认情况下,它返回 true。如果确定在 state 或 props 更新后组件不需要在重新渲染,则可以返回 false,这是一个提高性能的方法;
componentWillUpdate:在 shouldComponentUpdate 返回 true 确定要更新组件之前件之前执行;
componentDidUpdate:它主要用于更新 DOM 以响应 props 或 state 更改;
componentWillUnmount:它用于取消任何的网络请求,或删除与组件关联的所有事件监听器。
React Hooks 在平时开发中需要注意的问题和原因
(1)不要在循环,条件或嵌套函数中调用 Hook,必须始终在 React 函数的顶层使用 Hook
这是因为 React 需要利用调用顺序来正确更新相应的状态,以及调用相应的钩子函数。一旦在循环或条件分支语句中调用 Hook,就容易导致调用顺序的不一致性,从而产生难以预料到的后果。
(2)使用 useState 时候,使用 push,pop,splice 等直接更改数组对象的坑
使用 push 直接更改数组无法获取到新值,应该采用析构方式,但是在 class 里面不会有这个问题。代码示例:
(3)useState 设置状态的时候,只有第一次生效,后期需要更新状态,必须通过 useEffect
TableDeail 是一个公共组件,在调用它的父组件里面,我们通过 set 改变 columns 的值,以为传递给 TableDeail 的 columns 是最新的值,所以 tabColumn 每次也是最新的值,但是实际 tabColumn 是最开始的值,不会随着 columns 的更新而更新:
(4)善用 useCallback
父组件传递给子组件事件句柄时,如果我们没有任何参数变动可能会选用 useMemo。但是每一次父组件渲染子组件即使没变化也会跟着渲染一次。
(5)不要滥用 useContext
可以使用基于 useContext 封装的状态管理工具。
什么是 React 的 refs?为什么它们很重要
refs 允许你直接访问 DOM 元素或组件实例。为了使用它们,可以向组件添加个 ref 属性。如果该属性的值是一个回调函数,它将接受底层的 DOM 元素或组件的已挂载实例作为其第一个参数。可以在组件中存储它。
如果该属性值是一个字符串, React 将会在组件实例化对象的 refs 属性中,存储一个同名属性,该属性是对这个 DOM 元素的引用。可以通过原生的 DOM API 操作它。
使用 React 有何优点
只需查看
render
函数就会很容易知道一个组件是如何被渲染的JSX 的引入,使得组件的代码更加可读,也更容易看懂组件的布局,或者组件之间是如何互相引用的
支持服务端渲染,这可以改进 SEO 和性能
易于测试
React 只关注 View 层,所以可以和其它任何框架(如 Backbone.js, Angular.js)一起使用
对 React-Fiber 的理解,它解决了什么问题?
React V15 在渲染时,会递归比对 VirtualDOM 树,找出需要变动的节点,然后同步更新它们, 一气呵成。这个过程期间, React 会占据浏览器资源,这会导致用户触发的事件得不到响应,并且会导致掉帧,导致用户感觉到卡顿。
为了给用户制造一种应用很快的“假象”,不能让一个任务长期霸占着资源。 可以将浏览器的渲染、布局、绘制、资源加载(例如 HTML 解析)、事件响应、脚本执行视作操作系统的“进程”,需要通过某些调度策略合理地分配 CPU 资源,从而提高浏览器的用户响应速率, 同时兼顾任务执行效率。
所以 React 通过 Fiber 架构,让这个执行过程变成可被中断。“适时”地让出 CPU 执行权,除了可以让浏览器及时地响应用户的交互,还有其他好处:
分批延时对 DOM 进行操作,避免一次性操作大量 DOM 节点,可以得到更好的用户体验;
给浏览器一点喘息的机会,它会对代码进行编译优化(JIT)及进行热代码优化,或者对 reflow 进行修正。
核心思想: Fiber 也称协程或者纤程。它和线程并不一样,协程本身是没有并发或者并行能力的(需要配合线程),它只是一种控制流程的让出机制。让出 CPU 的执行权,让 CPU 能在这段时间执行其他的操作。渲染的过程可以被中断,可以将控制权交回浏览器,让位给高优先级的任务,浏览器空闲后再恢复渲染。
constructor
class 语法糖默认会帮你定义一个 constructor,所以当你不需要使用 constructor 的时候,是可以不用自己定义的
当你自己定义一个 constructor 的时候,就一定要写 super(),否则拿不到 this
当你在 constructor 里面想要使用 props 的值,就需要传入 props 这个参数给 super,调用 super(props),否则只需要写 super()
useEffect 与 useLayoutEffect 的区别
(1)共同点
运用效果: useEffect 与 useLayoutEffect 两者都是用于处理副作用,这些副作用包括改变 DOM、设置订阅、操作定时器等。在函数组件内部操作副作用是不被允许的,所以需要使用这两个函数去处理。
使用方式: useEffect 与 useLayoutEffect 两者底层的函数签名是完全一致的,都是调用的 mountEffectImpl 方法,在使用上也没什么差异,基本可以直接替换。
(2)不同点
使用场景: useEffect 在 React 的渲染过程中是被异步调用的,用于绝大多数场景;而 useLayoutEffect 会在所有的 DOM 变更之后同步调用,主要用于处理 DOM 操作、调整样式、避免页面闪烁等问题。也正因为是同步处理,所以需要避免在 useLayoutEffect 做计算量较大的耗时任务从而造成阻塞。
使用效果: useEffect 是按照顺序执行代码的,改变屏幕像素之后执行(先渲染,后改变 DOM),当改变屏幕内容时可能会产生闪烁;useLayoutEffect 是改变屏幕像素之前就执行了(会推迟页面显示的事件,先改变 DOM 后渲染),不会产生闪烁。useLayoutEffect 总是比 useEffect 先执行。
在未来的趋势上,两个 API 是会长期共存的,暂时没有删减合并的计划,需要开发者根据场景去自行选择。React 团队的建议非常实用,如果实在分不清,先用 useEffect,一般问题不大;如果页面有异常,再直接替换为 useLayoutEffect 即可。
在使用 React Router 时,如何获取当前页面的路由或浏览器中地址栏中的地址?
在当前组件的 props 中,包含 location 属性对象,包含当前页面路由地址信息,在 match 中存储当前路由的参数等数据信息。可以直接通过 this .props 使用它们。
React 中的 setState 和 replaceState 的区别是什么?
(1)setState() setState()用于设置状态对象,其语法如下:
nextState,将要设置的新状态,该状态会和当前的 state 合并
callback,可选参数,回调函数。该函数会在 setState 设置成功,且组件重新渲染后调用。
合并 nextState 和当前 state,并重新渲染组件。setState 是 React 事件处理函数中和请求回调函数中触发 UI 更新的主要方法。
(2)replaceState() replaceState()方法与 setState()类似,但是方法只会保留 nextState 中状态,原 state 不在 nextState 中的状态都会被删除。其语法如下:
nextState,将要设置的新状态,该状态会替换当前的 state。
callback,可选参数,回调函数。该函数会在 replaceState 设置成功,且组件重新渲染后调用。
总结: setState 是修改其中的部分状态,相当于 Object.assign,只是覆盖,不会减少原来的状态。而 replaceState 是完全替换原来的状态,相当于赋值,将原来的 state 替换为另一个对象,如果新状态属性减少,那么 state 中就没有这个状态了。
评论