美团前端一面必会 react 面试题
state 和 props 触发更新的生命周期分别有什么区别?
state 更新流程: 这个过程当中涉及的函数:
shouldComponentUpdate: 当组件的 state 或 props 发生改变时,都会首先触发这个生命周期函数。它会接收两个参数:nextProps, nextState——它们分别代表传入的新 props 和新的 state 值。拿到这两个值之后,我们就可以通过一些对比逻辑来决定是否有 re-render(重渲染)的必要了。如果该函数的返回值为 false,则生命周期终止,反之继续;
注意:此方法仅作为性能优化的方式而存在。不要企图依靠此方法来“阻止”渲染,因为这可能会产生 bug。应该考虑使用内置的 PureComponent 组件,而不是手动编写
shouldComponentUpdate()
componentWillUpdate:当组件的 state 或 props 发生改变时,会在渲染之前调用 componentWillUpdate。componentWillUpdate 是 React16 废弃的三个生命周期之一。过去,我们可能希望能在这个阶段去收集一些必要的信息(比如更新前的 DOM 信息等等),现在我们完全可以在 React16 的 getSnapshotBeforeUpdate 中去做这些事;
componentDidUpdate:componentDidUpdate() 会在 UI 更新后会被立即调用。它接收 prevProps(上一次的 props 值)作为入参,也就是说在此处我们仍然可以进行 props 值对比(再次说明 componentWillUpdate 确实鸡肋哈)。
props 更新流程: 相对于 state 更新,props 更新后唯一的区别是增加了对 componentWillReceiveProps 的调用。关于 componentWillReceiveProps,需要知道这些事情:
componentWillReceiveProps:它在 Component 接受到新的 props 时被触发。componentWillReceiveProps 会接收一个名为 nextProps 的参数(对应新的 props 值)。该生命周期是 React16 废弃掉的三个生命周期之一。在它被废弃前,可以用它来比较 this.props 和 nextProps 来重新 setState。在 React16 中,用一个类似的新生命周期 getDerivedStateFromProps 来代替它。
React 组件的 state 和 props 有什么区别?
(1)props
props 是一个从外部传进组件的参数,主要作为就是从父组件向子组件传递数据,它具有可读性和不变性,只能通过外部组件主动传入新的 props 来重新渲染子组件,否则子组件的 props 以及展现形式不会改变。
(2)state
state 的主要作用是用于组件保存、控制以及修改自己的状态,它只能在 constructor 中初始化,它算是组件的私有属性,不可通过外部访问和修改,只能通过组件内部的 this.setState 来修改,修改 state 属性会导致组件的重新渲染。
(3)区别
props 是传递给组件的(类似于函数的形参),而 state 是在组件内被组件自己管理的(类似于在一个函数内声明的变量)。
props 是不可修改的,所有 React 组件都必须像纯函数一样保护它们的 props 不被更改。
state 是在组件中创建的,一般在 constructor 中初始化 state。state 是多变的、可以修改,每次 setState 都异步更新的。
React 中什么是受控组件和非控组件?
(1)受控组件 在使用表单来收集用户输入时,例如<input><select><textearea>
等元素都要绑定一个 change 事件,当表单的状态发生变化,就会触发 onChange 事件,更新组件的 state。这种组件在 React 中被称为受控组件,在受控组件中,组件渲染出的状态与它的 value 或 checked 属性相对应,react 通过这种方式消除了组件的局部状态,使整个状态可控。react 官方推荐使用受控表单组件。
受控组件更新 state 的流程:
可以通过初始 state 中设置表单的默认值
每当表单的值发生变化时,调用 onChange 事件处理器
事件处理器通过事件对象 e 拿到改变后的状态,并更新组件的 state
一旦通过 setState 方法更新 state,就会触发视图的重新渲染,完成表单组件的更新
受控组件缺陷: 表单元素的值都是由 React 组件进行管理,当有多个输入框,或者多个这种组件时,如果想同时获取到全部的值就必须每个都要编写事件处理函数,这会让代码看着很臃肿,所以为了解决这种情况,出现了非受控组件。
(2)非受控组件 如果一个表单组件没有 value props(单选和复选按钮对应的是 checked props)时,就可以称为非受控组件。在非受控组件中,可以使用一个 ref 来从 DOM 获得表单值。而不是为每个状态更新编写一个事件处理程序。
React 官方的解释:
要编写一个非受控组件,而不是为每个状态更新都编写数据处理函数,你可以使用 ref 来从 DOM 节点中获取表单数据。因为非受控组件将真实数据储存在 DOM 节点中,所以在使用非受控组件时,有时候反而更容易同时集成 React 和非 React 代码。如果你不介意代码美观性,并且希望快速编写代码,使用非受控组件往往可以减少你的代码量。否则,你应该使用受控组件。
例如,下面的代码在非受控组件中接收单个属性:
总结: 页面中所有输入类的 DOM 如果是现用现取的称为非受控组件,而通过 setState 将输入的值维护到了 state 中,需要时再从 state 中取出,这里的数据就受到了 state 的控制,称为受控组件。
如何避免重复发起 ajax 获取数据?
数据放在 redux 里面
在使用 React Router 时,如何获取当前页面的路由或浏览器中地址栏中的地址?
在当前组件的 props 中,包含 location 属性对象,包含当前页面路由地址信息,在 match 中存储当前路由的参数等数据信息。可以直接通过 this .props 使用它们。
connect 原理
首先
connect
之所以会成功,是因为Provider
组件:在原应用组件上包裹一层,使原来整个应用成为
Provider
的子组件 接收Redux
的store
作为props
,通过context
对象传递给子孙组件上的connect
connect
做了些什么。它真正连接Redux
和React
,它包在我们的容器组件的外一层,它接收上面Provider
提供的store
里面的state
和dispatch
,传给一个构造函数,返回一个对象,以属性形式传给我们的容器组件
connect
是一个高阶函数,首先传入mapStateToProps
、mapDispatchToProps
,然后返回一个生产Component
的函数(wrapWithConnect
),然后再将真正的Component
作为参数传入wrapWithConnect
,这样就生产出一个经过包裹的Connect
组件,
该组件具有如下特点
通过
props.store
获取祖先Component
的store props
包括stateProps
、dispatchProps
、parentProps
,合并在一起得到nextState
,作为props
传给真正的Component componentDidMount
时,添加事件this.store.subscribe(this.handleChange)
,实现页面交互shouldComponentUpdate
时判断是否有避免进行渲染,提升页面性能,并得到nextState
componentWillUnmount
时移除注册的事件this.handleChange
由于
connect
的源码过长,我们只看主要逻辑
参考 前端进阶面试题详细解答
redux 有什么缺点
一个组件所需要的数据,必须由父组件传过来,而不能像 flux 中直接从 store 取
当一个组件相关数据更新时,即使父组件不需要用到这个组件,父组件还是会重新 render,可能会有效率影响,或者需要写复杂的
shouldComponentUpdate
进行判断
react 的虚拟 dom 是怎么实现的
首先说说为什么要使用
Virturl DOM
,因为操作真实DOM
的耗费的性能代价太高,所以react
内部使用js
实现了一套 dom 结构,在每次操作在和真实 dom 之前,使用实现好的 diff 算法,对虚拟 dom 进行比较,递归找出有变化的 dom 节点,然后对其进行更新操作。为了实现虚拟DOM
,我们需要把每一种节点类型抽象成对象,每一种节点类型有自己的属性,也就是 prop,每次进行diff
的时候,react
会先比较该节点类型,假如节点类型不一样,那么react
会直接删除该节点,然后直接创建新的节点插入到其中,假如节点类型一样,那么会比较prop
是否有更新,假如有prop
不一样,那么react
会判定该节点有更新,那么重渲染该节点,然后在对其子节点进行比较,一层一层往下,直到没有子节点
哪些方法会触发 React 重新渲染?重新渲染 render 会做些什么?
(1)哪些方法会触发 react 重新渲染?
setState()方法被调用
setState 是 React 中最常用的命令,通常情况下,执行 setState 会触发 render。但是这里有个点值得关注,执行 setState 的时候不一定会重新渲染。当 setState 传入 null 时,并不会触发 render。
父组件重新渲染
只要父组件重新渲染了,即使传入子组件的 props 未发生变化,那么子组件也会重新渲染,进而触发 render
(2)重新渲染 render 会做些什么?
会对新旧 VNode 进行对比,也就是我们所说的 Diff 算法。
对新旧两棵树进行一个深度优先遍历,这样每一个节点都会一个标记,在到深度遍历的时候,每遍历到一和个节点,就把该节点和新的节点树进行对比,如果有差异就放到一个对象里面
遍历差异对象,根据差异的类型,根据对应对规则更新 VNode
React 的处理 render 的基本思维模式是每次一有变动就会去重新渲染整个应用。在 Virtual DOM 没有出现之前,最简单的方法就是直接调用 innerHTML。Virtual DOM 厉害的地方并不是说它比直接操作 DOM 快,而是说不管数据怎么变,都会尽量以最小的代价去更新 DOM。React 将 render 函数返回的虚拟 DOM 树与老的进行比较,从而确定 DOM 要不要更新、怎么更新。当 DOM 树很大时,遍历两棵树进行各种比对还是相当耗性能的,特别是在顶层 setState 一个微小的修改,默认会去遍历整棵树。尽管 React 使用高度优化的 Diff 算法,但是这个过程仍然会损耗性能.
为什么 React 并不推荐优先考虑使用 Context?
Context 目前还处于实验阶段,可能会在后面的发行版本中有很大的变化,事实上这种情况已经发生了,所以为了避免给今后升级带来大的影响和麻烦,不建议在 app 中使用 context。
尽管不建议在 app 中使用 context,但是独有组件而言,由于影响范围小于 app,如果可以做到高内聚,不破坏组件树之间的依赖关系,可以考虑使用 context
对于组件之间的数据通信或者状态管理,有效使用 props 或者 state 解决,然后再考虑使用第三方的成熟库进行解决,以上的方法都不是最佳的方案的时候,在考虑 context。
context 的更新需要通过 setState()触发,但是这并不是很可靠的,Context 支持跨组件的访问,但是如果中间的子组件通过一些方法不影响更新,比如 shouldComponentUpdate() 返回 false 那么不能保证 Context 的更新一定可以使用 Context 的子组件,因此,Context 的可靠性需要关注
react hooks,它带来了那些便利
代码逻辑聚合,逻辑复用
HOC 嵌套地狱
代替 class
React 中通常使用 类定义 或者 函数定义 创建组件:
在类定义中,我们可以使用到许多 React 特性,例如 state、 各种组件生命周期钩子等,但是在函数定义中,我们却无能为力,因此 React 16.8 版本推出了一个新功能 (React Hooks),通过它,可以更好的在函数定义组件中使用 React 特性。
好处:
跨组件复用: 其实 render props / HOC 也是为了复用,相比于它们,Hooks 作为官方的底层 API,最为轻量,而且改造成本小,不会影响原来的组件层次结构和传说中的嵌套地狱;
类定义更为复杂
不同的生命周期会使逻辑变得分散且混乱,不易维护和管理;
时刻需要关注 this 的指向问题;
代码复用代价高,高阶组件的使用经常会使整个组件树变得臃肿;
状态与 UI 隔离: 正是由于 Hooks 的特性,状态逻辑会变成更小的粒度,并且极容易被抽象成一个自定义 Hooks,组件中的状态和 UI 变得更为清晰和隔离。
注意:
避免在 循环/条件判断/嵌套函数 中调用 hooks,保证调用顺序的稳定;
只有 函数定义组件 和 hooks 可以调用 hooks,避免在 类组件 或者 普通函数 中调用;
不能在 useEffect 中使用 useState,React 会报错提示;
类组件不会被替换或废弃,不需要强制改造类组件,两种方式能并存;
重要钩子
状态钩子 (useState): 用于定义组件的 State,其到类定义中 this.state 的功能;
生命周期钩子 (useEffect):
类定义中有许多生命周期函数,而在 React Hooks 中也提供了一个相应的函数 (useEffect),这里可以看做 componentDidMount、componentDidUpdate 和 componentWillUnmount 的结合。
useEffect(callback, [source])接受两个参数
callback: 钩子回调函数;
source: 设置触发条件,仅当 source 发生改变时才会触发;
useEffect 钩子在没有传入[source]参数时,默认在每次 render 时都会优先调用上次保存的回调中返回的函数,后再重新调用回调;
通过第二个参数,我们便可模拟出几个常用的生命周期:
componentDidMount: 传入[]时,就只会在初始化时调用一次
componentWillUnmount: 传入[],回调中的返回的函数也只会被最终执行一次
mounted: 可以使用 useState 封装成一个高度可复用的 mounted 状态;
componentDidUpdate: useEffect 每次均会执行,其实就是排除了 DidMount 后即可;
其它内置钩子:
useContext
: 获取 context 对象useReducer
: 类似于 Redux 思想的实现,但其并不足以替代 Redux,可以理解成一个组件内部的 redux:并不是持久化存储,会随着组件被销毁而销毁;
属于组件内部,各个组件是相互隔离的,单纯用它并无法共享数据;
配合 useContext`的全局性,可以完成一个轻量级的 Redux;(easy-peasy)
useCallback
: 缓存回调函数,避免传入的回调每次都是新的函数实例而导致依赖组件重新渲染,具有性能优化的效果;useMemo
: 用于缓存传入的 props,避免依赖的组件每次都重新渲染;useRef
: 获取组件的真实节点;useLayoutEffect
DOM 更新同步钩子。用法与 useEffect 类似,只是区别于执行时间点的不同
useEffect 属于异步执行,并不会等待 DOM 真正渲染后执行,而 useLayoutEffect 则会真正渲染后才触发;
可以获取更新后的 state;
自定义钩子(useXxxxx): 基于 Hooks 可以引用其它 Hooks 这个特性,我们可以编写自定义钩子,如上面的 useMounted。又例如,我们需要每个页面自定义标题:
React 中发起网络请求应该在哪个生命周期中进行?为什么?
对于异步请求,最好放在 componentDidMount 中去操作,对于同步的状态改变,可以放在 componentWillMount 中,一般用的比较少。
如果认为在 componentWillMount 里发起请求能提早获得结果,这种想法其实是错误的,通常 componentWillMount 比 componentDidMount 早不了多少微秒,网络上任何一点延迟,这一点差异都可忽略不计。
react 的生命周期: constructor() -> componentWillMount() -> render() -> componentDidMount()
上面这些方法的调用是有次序的,由上而下依次调用。
constructor 被调用是在组件准备要挂载的最开始,此时组件尚未挂载到网页上。
componentWillMount 方法的调用在 constructor 之后,在 render 之前,在这方法里的代码调用 setState 方法不会触发重新 render,所以它一般不会用来作加载数据之用。
componentDidMount 方法中的代码,是在组件已经完全挂载到网页上才会调用被执行,所以可以保证数据的加载。此外,在这方法中调用 setState 方法,会触发重新渲染。所以,官方设计这个方法就是用来加载外部数据用的,或处理其他的副作用代码。与组件上的数据无关的加载,也可以在 constructor 里做,但 constructor 是做组件 state 初绐化工作,并不是做加载数据这工作的,constructor 里也不能 setState,还有加载的时间太长或者出错,页面就无法加载出来。所以有副作用的代码都会集中在 componentDidMount 方法里。
总结:
跟服务器端渲染(同构)有关系,如果在 componentWillMount 里面获取数据,fetch data 会执行两次,一次在服务器端一次在客户端。在 componentDidMount 中可以解决这个问题,componentWillMount 同样也会 render 两次。
在 componentWillMount 中 fetch data,数据一定在 render 后才能到达,如果忘记了设置初始状态,用户体验不好。
react16.0 以后,componentWillMount 可能会被执行多次。
diff 算法?
把树形结构按照层级分解,只比较同级元素。
给列表结构的每个单元添加唯一的
key
属性,方便比较。React
只会匹配相同class
的component
(这里面的class
指的是组件的名字)合并操作,调用
component
的setState
方法的时候,React
将其标记为 -dirty
.到每一个事件循环结束,React
检查所有标记dirty
的component
重新绘制.选择性子树渲染。开发人员可以重写
shouldComponentUpdate
提高diff
的性能
Component, Element, Instance 之间有什么区别和联系?
元素: 一个元素
element
是一个普通对象(plain object),描述了对于一个 DOM 节点或者其他组件component
,你想让它在屏幕上呈现成什么样子。元素element
可以在它的属性props
中包含其他元素(译注:用于形成元素树)。创建一个 React 元素element
成本很低。元素element
创建之后是不可变的。组件: 一个组件
component
可以通过多种方式声明。可以是带有一个render()
方法的类,简单点也可以定义为一个函数。这两种情况下,它都把属性props
作为输入,把返回的一棵元素树作为输出。实例: 一个实例
instance
是你在所写的组件类component class
中使用关键字this
所指向的东西(译注:组件实例)。它用来存储本地状态和响应生命周期事件很有用。
函数式组件(Functional component
)根本没有实例instance
。类组件(Class component
)有实例instance
,但是永远也不需要直接创建一个组件的实例,因为 React 帮我们做了这些。
React 中 refs 的作用是什么?有哪些应用场景?
Refs 提供了一种方式,用于访问在 render 方法中创建的 React 元素或 DOM 节点。Refs 应该谨慎使用,如下场景使用 Refs 比较适合:
处理焦点、文本选择或者媒体的控制
触发必要的动画
集成第三方 DOM 库
Refs 是使用 React.createRef() 方法创建的,他通过 ref 属性附加到 React 元素上。
要在整个组件中使用 Refs,需要将 ref 在构造函数中分配给其实例属性:
如何用 React 构建( build)生产模式?
通常,使用 Webpack 的 DefinePlugin 方法将 NODE ENV 设置为 production。这将剥离 propType 验证和额外的警告。除此之外,还可以减少代码,因为 React 使用 Uglify 的 dead-code 来消除开发代码和注释,这将大大减少包占用的空间。
概述一下 React 中的事件处理逻辑。
为了解决跨浏览器兼容性问题, React 会将浏览器原生事件( Browser Native Event)封装为合成事件( Synthetic Event)并传入设置的事件处理程序中。这里的合成事件提供了与原生事件相同的接口,不过它们屏蔽了底层浏览器的细节差异,保证了行为的一致性。另外, React 并没有直接将事件附着到子元素上,而是以单一事件监听器的方式将所有的事件发送到顶层进行处理(基于事件委托原理)。这样 React 在更新 DOM 时就不需要考虑如何处理附着在 DOM 上的事件监听器,最终达到优化性能的目的。
说说 React 组件开发中关于作用域的常见问题。
在 EMAScript5 语法规范中,关于作用域的常见问题如下。(1)在 map 等方法的回调函数中,要绑定作用域 this(通过 bind 方法)。(2)父组件传递给子组件方法的作用域是父组件实例化对象,无法改变。(3)组件事件回调函数方法的作用域是组件实例化对象(绑定父组件提供的方法就是父组件实例化对象),无法改变。在 EMAScript6 语法规范中,关于作用域的常见问题如下。(1)当使用箭头函数作为 map 等方法的回调函数时,箭头函数的作用域是当前组件的实例化对象(即箭头函数的作用域是定义时的作用域),无须绑定作用域。(2)事件回调函数要绑定组件作用域。(3)父组件传递方法要绑定父组件作用域。总之,在 EMAScript6 语法规范中,组件方法的作用域是可以改变的。
React 中可以在 render 访问 refs 吗?为什么?
不可以,render 阶段 DOM 还没有生成,无法获取 DOM。DOM 的获取需要在 pre-commit 阶段和 commit 阶段:
refs 的作用是什么,你在什么样的业务场景下使用 refs
操作 DOM,为什么操作 DOM?
场景
图片渲染好后,操作图片宽高。比如做个放大镜功能
评论