前端 react 面试题指南
概述下 React 中的事件处理逻辑
抹平浏览器差异,实现更好的跨平台。
避免垃圾回收,React 引入事件池,在事件池中获取或释放事件对象,避免频繁地去创建和销毁。
方便事件统一管理和事务机制。
为了解决跨浏览器兼容性问题,
React
会将浏览器原生事件(Browser Native Event
)封装为合成事件(SyntheticEvent
)传入设置的事件处理器中。这里的合成事件提供了与原生事件相同的接口,不过它们屏蔽了底层浏览器的细节差异,保证了行为的一致性。另外有意思的是,React
并没有直接将事件附着到子元素上,而是以单一事件监听器的方式将所有的事件发送到顶层进行处理。这样React
在更新DOM
的时候就不需要考虑如何去处理附着在DOM
上的事件监听器,最终达到优化性能的目的
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 中的 key 是什么?为什么它们很重要?
key 可以帮助 React 跟踪循环创建列表中的虚拟 DOM 元素,了解哪些元素已更改、添加或删除。每个绑定 key 的虚拟 DOM 元素,在兄弟元素之间都是独一无二的。在 React 的和解过程中,比较新的虛拟 DOM 树与上一个虛拟 DOM 树之间的差异,并映射到页面中。key 使 React 处理列表中虛拟 DOM 时更加高效,因为 React 可以使用虛拟 DOM 上的 key 属性,快速了解元素是新的、需要删除的,还是修改过的。如果没有 key,Rεat 就不知道列表中虚拟 DOM 元素与页面中的哪个元素相对应。所以在创建列表的时候,不要忽略 key。
描述事件在 React 中的处理方式。
为了解决跨浏览器兼容性问题, React 中的事件处理程序将传递 SyntheticEvent 的实例,它是跨浏览器事件的包装器。这些 SyntheticEvent 与你习惯的原生事件具有相同的接口,它们在所有浏览器中都兼容。React 实际上并没有将事件附加到子节点本身。而是通过事件委托模式,使用单个事件监听器监听顶层的所有事件。这对于性能是有好处的。这也意味着在更新 DOM 时, React 不需要担心跟踪事件监听器。
调用 setState 之后发生了什么
在代码中调用 setState 函数之后,React 会将传入的参数与之前的状态进行合并,然后触发所谓的调和过程(Reconciliation)。经过调和过程,React 会以相对高效的方式根据新的状态构建 React 元素树并且着手重新渲染整个 UI 界面。在 React 得到元素树之后,React 会计算出新的树和老的树之间的差异,然后根据差异对界面进行最小化重新渲染。通过 diff 算法,React 能够精确制导哪些位置发生了改变以及应该如何改变,这就保证了按需更新,而不是全部重新渲染。
在 setState 的时候,React 会为当前节点创建一个 updateQueue 的更新列队。
然后会触发 reconciliation 过程,在这个过程中,会使用名为 Fiber 的调度算法,开始生成新的 Fiber 树, Fiber 算法的最大特点是可以做到异步可中断的执行。
然后 React Scheduler 会根据优先级高低,先执行优先级高的节点,具体是执行 doWork 方法。
在 doWork 方法中,React 会执行一遍 updateQueue 中的方法,以获得新的节点。然后对比新旧节点,为老节点打上 更新、插入、替换 等 Tag。
当前节点 doWork 完成后,会执行 performUnitOfWork 方法获得新节点,然后再重复上面的过程。
当所有节点都 doWork 完成后,会触发 commitRoot 方法,React 进入 commit 阶段。
在 commit 阶段中,React 会根据前面为各个节点打的 Tag,一次性更新整个 dom 元素
生命周期调用方法的顺序是什么?
React 生命周期分为三大周期,11 个阶段,生命周期方法调用顺序分别如下。(1)在创建期的五大阶段,调用方法的顺序如下。
getDetaultProps:定义默认属性数据。
getInitialState:初始化默认状态数据。
component WillMount:组件即将被构建。
render:渲染组件。
componentDidMount:组件构建完成
(2)在存在期的五大阶段,调用方法的顺序如下。
componentWillReceiveProps:组件即将接收新的属性数据。
shouldComponentUpdate:判断组件是否应该更新。
componnent WillUpdate:组件即将更新。
render:渲染组件。
componentDidUpdate:组件更新完成。
(3)在销毁期的一个阶段,调用方法 componentWillUnmount,表示组件即将被销毀。
参考 前端进阶面试题详细解答
如何告诉 React 它应该编译生产环境版
通常情况下我们会使用
Webpack
的DefinePlugin
方法来将NODE_ENV
变量值设置为production
。编译版本中React
会忽略propType
验证以及其他的告警信息,同时还会降低代码库的大小,React
使用了Uglify
插件来移除生产环境下不必要的注释等信息
setState 到底是异步还是同步?
先给出答案: 有时表现出异步,有时表现出同步
setState
只在合成事件和钩子函数中是“异步”的,在原生事件和setTimeout
中都是同步的setState
的“异步”并不是说内部由异步代码实现,其实本身执行的过程和代码都是同步的,只是合成事件和钩子函数的调用顺序在更新之前,导致在合成事件和钩子函数中没法立马拿到更新后的值,形成了所谓的“异步”,当然可以通过第二个参数setState(partialState, callback)
中的callback
拿到更新后的结果setState
的批量更新优化也是建立在“异步”(合成事件、钩子函数)之上的,在原生事件和setTimeout
中不会批量更新,在“异步”中如果对同一个值进行多次setState
,setState
的批量更新策略会对其进行覆盖,取最后一次的执行,如果是同时setState
多个不同的值,在更新时会对其进行合并批量更新
react 性能优化是哪个周期函数
shouldComponentUpdate
这个方法用来判断是否需要调用 render 方法重新描绘 dom。因为 dom 的描绘非常消耗性能,如果我们能在shouldComponentUpdate方
法中能够写出更优化的dom diff
算法,可以极大的提高性能
redux 的三大原则
单一数据源
整个应用的 state 被存储在一个 object tree 中,并且这个 object tree 之存在唯一一个 store 中
state 是只读的
唯一改变 state 的方式是触发 action,action 是一个用于描述已经发生时间的对象,这个保证了视图和网络请求都不能直接修改 state,相反他们只能表达想要修改的意图
使用纯函数来执行修改 state 为了描述 action 如何改变 state tree 需要编写 reduce
请说岀 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 版本中,作用域是可以改变的。
setState 到底是异步还是同步?
先给出答案: 有时表现出异步,有时表现出同步。
setState 只在合成事件和钩⼦函数中是“异步”的,在原⽣事件和 setTimeout 中都是同步的;
setState 的“异步”并不是说内部由异步代码实现,其实本身执⾏的过程和代码都是同步的,只是合成事件和钩⼦函数的调⽤顺序在更新之前,导致在合成事件和钩⼦函数中没法⽴⻢拿到更新后的值,形成了所谓的“异步”,当然可以通过第⼆个参数 setState(partialState, callback)中的 callback 拿到更新后的结果;
setState 的批量更新优化也是建⽴在“异步”(合成事件、钩⼦函数)之上的,在原⽣事件和 setTimeout 中不会批量更新,在“异步”中如果对同⼀个值进⾏多次 setState,setState 的批量更新策略会对其进⾏覆盖,取最后⼀次的执⾏,如果是同时 setState 多个不同的值,在更新时会对其进⾏合并批量更新。
在生命周期中的哪一步你应该发起 AJAX 请求
我们应当将 AJAX 请求放到
componentDidMount
函数中执行,主要原因有下
React
下一代调和算法Fiber
会通过开始或停止渲染的方式优化应用性能,其会影响到componentWillMount
的触发次数。对于componentWillMount
这个生命周期函数的调用次数会变得不确定,React
可能会多次频繁调用componentWillMount
。如果我们将AJAX
请求放到componentWillMount
函数中,那么显而易见其会被触发多次,自然也就不是好的选择。如果我们将
AJAX
请求放置在生命周期的其他函数中,我们并不能保证请求仅在组件挂载完毕后才会要求响应。如果我们的数据请求在组件挂载之前就完成,并且调用了setState
函数将数据添加到组件状态中,对于未挂载的组件则会报错。而在componentDidMount
函数中进行AJAX
请求则能有效避免这个问题
React 中 Diff 算法的原理是什么?
原理如下。(1)节点之间的比较。节点包括两种类型:一种是 React 组件,另一种是 HTML 的 DOM。如果节点类型不同,按以下方式比较。如果 HTML DOM 不同,直接使用新的替换旧的。如果组件类型不同,也直接使用新的替换旧的。如果 HTML DOM 类型相同,按以下方式比较。在 React 里样式并不是一个纯粹的字符串,而是一个对象,这样在样式发生改变时,只需要改变替换变化以后的样式。修改完当前节点之后,递归处理该节点的子节点。如果组件类型相同,按以下方式比较。如果组件类型相同,使用 React 机制处理。一般使用新的 props 替换旧的 props,并在之后调用组件的 componentWillReceiveProps 方法,之前组件的 render 方法会被调用。节点的比较机制开始递归作用于它的子节点。(2)两个列表之间的比较。一个节点列表中的一个节点发生改变, React 无法很妤地处理这个问题。循环新旧两个列表,并找出不同,这是 React 唯一的处理方法。但是,有一个办法可以把这个算法的复杂度降低。那就是在生成一个节点列表时给每个节点上添加一个 key。这个 key 只需要在这一个节点列表中唯一,不需要全局唯一。(3)取舍需要注意的是,上面的启发式算法基于两点假设。类型相近的节点总是生成同样的树,而类型不同的节点也总是生成不同的树可以为多次 render 都表现稳定的节点设置 key。上面的节点之间的比较算法基本上就是基于这两个假设而实现的。要提高 React 应用的效率,需要按照这两点假设来开发。
使用箭头函数(arrow functions)的优点是什么
作用域安全:在箭头函数之前,每一个新创建的函数都有定义自身的
this
值(在构造函数中是新对象;在严格模式下,函数调用中的this
是未定义的;如果函数被称为“对象方法”,则为基础对象等),但箭头函数不会,它会使用封闭执行上下文的this
值。简单:箭头函数易于阅读和书写
清晰:当一切都是一个箭头函数,任何常规函数都可以立即用于定义作用域。开发者总是可以查找 next-higher 函数语句,以查看
this
的值
高阶组件的应用场景
权限控制
利用高阶组件的 条件渲染 特性可以对页面进行权限控制,权限控制一般分为两个维度:页面级别 和 页面元素级别
可能你已经发现了,高阶组件其实就是装饰器模式在 React 中的实现:通过给函数传入一个组件(函数或类)后在函数内部对该组件(函数或类)进行功能的增强(不修改传入参数的前提下),最后返回这个组件(函数或类),即允许向一个现有的组件添加新的功能,同时又不去修改该组件,属于 包装模式(Wrapper Pattern) 的一种。
什么是装饰者模式:在不改变对象自身的前提下在程序运行期间动态的给对象添加一些额外的属性或行为
可以提高代码的复用性和灵活性。
再对高阶组件进行一个小小的总结:
高阶组件 不是组件,是 一个把某个组件转换成另一个组件的 函数
高阶组件的主要作用是 代码复用
高阶组件是 装饰器模式在 React 中的实现
封装组件的原则
封装原则
1、单一原则:负责单一的页面渲染
2、多重职责:负责多重职责,获取数据,复用逻辑,页面渲染等
3、明确接受参数:必选,非必选,参数尽量设置以_开头,避免变量重复
4、可扩展:需求变动能够及时调整,不影响之前代码
5、代码逻辑清晰
6、封装的组件必须具有高性能,低耦合的特性
7、组件具有单一职责:封装业务组件或者基础组件,如果不能给这个组件起一个有意义的名字,证明这个组件承担的职责可能不够单一,需要继续抽组件,直到它可以是一个独立的组件即可
react 旧版生命周期函数
初始化阶段
getDefaultProps
:获取实例的默认属性getInitialState
:获取每个实例的初始化状态componentWillMount
:组件即将被装载、渲染到页面上render
:组件在这里生成虚拟的DOM
节点componentDidMount
:组件真正在被装载之后
运行中状态
componentWillReceiveProps
:组件将要接收到属性的时候调用shouldComponentUpdate
:组件接受到新属性或者新状态的时候(可以返回 false,接收数据后不更新,阻止render
调用,后面的函数不会被继续执行了)componentWillUpdate
:组件即将更新不能修改属性和状态render
:组件重新描绘componentDidUpdate
:组件已经更新
销毁阶段
componentWillUnmount
:组件即将销毁
如何有条件地向 React 组件添加属性?
对于某些属性,React 非常聪明,如果传递给它的值是虚值,可以省略该属性。例如:
渲染结果:
另一种可能的方法是:
为什么不直接更新 state
呢 ?
如果试图直接更新 state
,则不会重新渲染组件。
需要使用setState()
方法来更新 state
。它调度对组件state
对象的更新。当state
改变时,组件通过重新渲染来响应:
hooks 为什么不能放在条件判断里
以 setState 为例,在 react 内部,每个组件(Fiber)的 hooks 都是以链表的形式存在 memoizeState 属性中
update 阶段,每次调用 setState,链表就会执行 next 向后移动一步。如果将 setState 写在条件判断中,假设条件判断不成立,没有执行里面的 setState 方法,会导致接下来所有的 setState 的取值出现偏移,从而导致异常发生。
评论