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注入到组件中:
import { connect } from 'react-redux' import { setVisibilityFilter } from '@/reducers/Todo/actions' import Link from '@/containers/Todo/components/Link'const mapStateToProps = (state, ownProps) => ({ active: ownProps.filter === state.visibilityFilter })const mapDispatchToProps = (dispatch, ownProps) => ({ setFilter: () => { dispatch(setVisibilityFilter(ownProps.filter)) } })export default connect( mapStateToProps, mapDispatchToProps )(Link) 复制代码

上面代码中,active就是注入到Link组件中的状态。 mapStateToProps(state,ownProps)中带有两个参数,含义是∶
  • state-store管理的全局状态对象,所有都组件状态数据都存储在该对象中。
  • ownProps 组件通过props传入的参数。
reducer 到组件经历的过程:
  • reducer对action对象处理,更新组件状态,并将新的状态值返回store。
  • 通过connect(mapStateToProps,mapDispatchToProps)(Component)对组件 Component进行升级,此时将状态值从store取出并作为props参数传递到组件。
高阶组件实现源码∶
import React from 'react' import PropTypes from 'prop-types'// 高阶组件 contect export const connect = (mapStateToProps, mapDispatchToProps) => (WrappedComponent) => { class Connect extends React.Component { // 通过对context调用获取store static contextTypes = { store: PropTypes.object }constructor() { super() this.state = { allProps: {} } }// 第一遍需初始化所有组件初始状态 componentWillMount() { const store = this.context.store this._updateProps() store.subscribe(() => this._updateProps()); // 加入_updateProps()至store里的监听事件列表 }// 执行action后更新props,使组件可以更新至最新状态(类似于setState) _updateProps() { const store = this.context.store; let stateProps = mapStateToProps ? mapStateToProps(store.getState(), this.props) : {} // 防止 mapStateToProps 没有传入 let dispatchProps = mapDispatchToProps ? mapDispatchToProps(store.dispatch, this.props) : { dispatch: store.dispatch } // 防止 mapDispatchToProps 没有传入 this.setState({ allProps: { ...stateProps, ...dispatchProps, ...this.props } }) }render() { return } } return Connect } 复制代码

Redux 请求中间件如何处理并发
使用redux-Saga redux-saga是一个管理redux应用异步操作的中间件,用于代替 redux-thunk 的。它通过创建 Sagas 将所有异步操作逻辑存放在一个地方进行集中处理,以此将react中的同步操作与异步操作区分开来,以便于后期的管理与维护。 redux-saga如何处理并发:
  • takeEvery
可以让多个 saga 任务并行被 fork 执行。
import { fork, take } from "redux-saga/effects"const takeEvery = (pattern, saga, ...args) => fork(function*() { while (true) { const action = yield take(pattern) yield fork(saga, ...args.concat(action)) } }) 复制代码

  • takeLatest
takeLatest 不允许多个 saga 任务并行地执行。一旦接收到新的发起的 action,它就会取消前面所有 fork 过的任务(如果这些任务还在执行的话)。
在处理 AJAX 请求的时候,如果只希望获取最后那个请求的响应, takeLatest 就会非常有用。
import { cancel, fork, take } from "redux-saga/effects"const takeLatest = (pattern, saga, ...args) => fork(function*() { let lastTask while (true) { const action = yield take(pattern) if (lastTask) { yield cancel(lastTask) // 如果任务已经结束,则 cancel 为空操作 } lastTask = yield fork(saga, ...args.concat(action)) } }) 复制代码

**
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方法。
function invokeGuardedCallback(name, func, a) { try { func(a); } catch (x) { if (caughtError === null) { caughtError = x; } } }

事件处理函数是直接调用的,并没有指定调用的组件,所以不进行手动绑定的情况下直接获取到的 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里面不会有这个问题。代码示例:
function Indicatorfilter() { let [num,setNums] = useState([0,1,2,3]) const test = () => { // 这里坑是直接采用push去更新num // setNums(num)是无法更新num的 // 必须使用num = [...num ,1] num.push(1) // num = [...num ,1] setNums(num) } return (测试{num.map((item,index) => ({item} ))}) }class Indicatorfilter extends React.Component{ constructor(props:any){ super(props) this.state = { nums:[1,2,3] } this.test = this.test.bind(this) }test(){ // class采用同样的方式是没有问题的 this.state.nums.push(1) this.setState({ nums: this.state.nums }) }render(){ let {nums} = this.state return(测试{nums.map((item:any,index:number) => ({item} ))}) } } 复制代码

(3)useState设置状态的时候,只有第一次生效,后期需要更新状态,必须通过useEffect
TableDeail是一个公共组件,在调用它的父组件里面,我们通过set改变columns的值,以为传递给TableDeail 的 columns是最新的值,所以tabColumn每次也是最新的值,但是实际tabColumn是最开始的值,不会随着columns的更新而更新:
const TableDeail = ({columns,}:TableData) => { const [tabColumn, setTabColumn] = useState(columns) }// 正确的做法是通过useEffect改变这个值 const TableDeail = ({columns,}:TableData) => { const [tabColumn, setTabColumn] = useState(columns) useEffect(() =>{setTabColumn(columns)},[columns]) }复制代码

(4)善用useCallback
父组件传递给子组件事件句柄时,如果我们没有任何参数变动可能会选用useMemo。但是每一次父组件渲染子组件即使没变化也会跟着渲染一次。
(5)不要滥用useContext
可以使用基于 useContext 封装的状态管理工具。
什么是 React的refs?为什么它们很重要
refs允许你直接访问DOM元素或组件实例。为了使用它们,可以向组件添加个ref属性。
如果该属性的值是一个回调函数,它将接受底层的DOM元素或组件的已挂载实例作为其第一个参数。可以在组件中存储它。
export class App extends Component { showResult() { console.log(this.input.value); } render() { return ( (this.input = input)} /> ); } }

如果该属性值是一个字符串, React将会在组件实例化对象的refs属性中,存储一个同名属性,该属性是对这个DOM元素的引用。可以通过原生的 DOM API操作它。
export class App extends Component { showResult() { console.log(this.refs.username.value); } render() { return ( ); } }

使用 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
答案是:在 constructor 函数里面,需要用到props的值的时候,就需要调用 super(props)

  1. class语法糖默认会帮你定义一个constructor,所以当你不需要使用constructor的时候,是可以不用自己定义的
  2. 当你自己定义一个constructor的时候,就一定要写super(),否则拿不到this
  3. 当你在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()用于设置状态对象,其语法如下:
setState(object nextState[, function callback]) 复制代码

  • nextState,将要设置的新状态,该状态会和当前的state合并
  • callback,可选参数,回调函数。该函数会在setState设置成功,且组件重新渲染后调用。
合并nextState和当前state,并重新渲染组件。setState是React事件处理函数中和请求回调函数中触发UI更新的主要方法。
(2)replaceState() replaceState()方法与setState()类似,但是方法只会保留nextState中状态,原state不在nextState中的状态都会被删除。其语法如下:
replaceState(object nextState[, function callback]) 复制代码

  • nextState,将要设置的新状态,该状态会替换当前的state。
  • callback,可选参数,回调函数。该函数会在replaceState设置成功,且组件重新渲染后调用。
【react高频面试题总结(一)】总结: setState 是修改其中的部分状态,相当于 Object.assign,只是覆盖,不会减少原来的状态。而replaceState 是完全替换原来的状态,相当于赋值,将原来的 state 替换为另一个对象,如果新状态属性减少,那么 state 中就没有这个状态了。

    推荐阅读