前段时间准备面试,总结了不少,下面是我在准备React面试时,结合本身的实际面试经历,以及我之前源码分析的文章,总结出来的一些 React
高频面试题目。html
之前我写的源码分析的文章,并无不少人看,由于大部分状况下你不须要深刻源码也能懂得其中原理,并解决实际问题,这也是我总结这些面试题的缘由,让你在更短的时间内得到更大的收益。react
因为是以面试题的角度来讨论,因此某些点可能不能很是深刻,我在问题下面都贴了相关连接,若是想深刻理解,请点击这些文章。面试
因为题目较多,分为上、下两篇,本篇文章咱们先来讨论以下19个题目:算法
React
生命周期有哪些,16版本生命周期发生了哪些变化?浏览器
setState
是同步的仍是异步的?性能优化
为何有时连续屡次 setState
只有一次生效?bash
React
如何实现本身的事件机制?服务器
为什么 React
事件要本身绑定 this
?babel
原生事件和 React
事件的区别?网络
React
的合成事件是什么?
React
和原生事件的执行顺序是什么?能够混用吗?
虚拟Dom
是什么?
虚拟Dom
比 普通Dom
更快吗?
虚拟Dom
中的 $$typeof
属性的做用是什么?
React
组件的渲染流程是什么?
为何代码中必定要引入 React
?
为何 React
组件首字母必须大写?
React
在渲染 真实Dom
时作了哪些性能优化?
什么是高阶组件?如何实现?
HOC
在业务场景中有哪些实际应用场景?
高阶组件( HOC
)和 Mixin
的异同点是什么?
Hook
有哪些优点?
初始化阶段
constructor
构造函数
getDefaultProps
props
默认值
getInitialState
state
默认值
挂载阶段
componentWillMount
组件初始化渲染前调用
render
组件渲染
componentDidMount
组件挂载到 DOM
后调用
更新阶段
componentWillReceiveProps
组件将要接收新 props
前调用
shouldComponentUpdate
组件是否须要更新
componentWillUpdate
组件更新前调用
render
组件渲染
componentDidUpdate
组件更新后调用
卸载阶段
componentWillUnmount
组件卸载前调用
初始化阶段
constructor
构造函数
getDefaultProps
props
默认值
getInitialState
state
默认值
挂载阶段
staticgetDerivedStateFromProps(props,state)
render
componentDidMount
getDerivedStateFromProps
:组件每次被rerender
的时候,包括在组件构建以后(虚拟dom
以后,实际dom
挂载以前),每次获取新的props
或state
以后;每次接收新的props以后都会返回一个对象做为新的state
,返回null则说明不须要更新state
;配合componentDidUpdate
,能够覆盖componentWillReceiveProps
的全部用法
更新阶段
staticgetDerivedStateFromProps(props,state)
shouldComponentUpdate
render
getSnapshotBeforeUpdate(prevProps,prevState)
componentDidUpdate
getSnapshotBeforeUpdate
:触发时间:update
发生的时候,在render
以后,在组件dom
渲染以前;返回一个值,做为componentDidUpdate
的第三个参数;配合componentDidUpdate
, 能够覆盖componentWillUpdate
的全部用法
卸载阶段
componentWillUnmount
错误处理
componentDidCatch
React16
新的生命周期弃用了 componentWillMount、componentWillReceivePorps,componentWillUpdate
新增了 getDerivedStateFromProps、getSnapshotBeforeUpdate
来代替弃用的三个钩子函数。
React16
并无删除这三个钩子函数,可是不能和新增的钩子函数混用,React17
将会删除这三个钩子函数,新增了对错误的处理(componentDidCatch
)
生命周期和合成事件中
在 React
的生命周期和合成事件中, React
仍然处于他的更新机制中,这时不管调用多少次 setState
,都会不会当即执行更新,而是将要更新的·存入 _pendingStateQueue
,将要更新的组件存入 dirtyComponent
。
当上一次更新机制执行完毕,以生命周期为例,全部组件,即最顶层组件 didmount
后会将批处理标志设置为 false
。这时将取出 dirtyComponent
中的组件以及 _pendingStateQueue
中的 state
进行更新。这样就能够确保组件不会被从新渲染屡次。
componentDidMount() { this.setState({ index: this.state.index + 1 }) console.log('state', this.state.index); }复制代码
因此,如上面的代码,当咱们在执行 setState
后当即去获取 state
,这时是获取不到更新后的 state
的,由于处于 React
的批处理机制中, state
被暂存起来,待批处理机制完成以后,统一进行更新。
因此。setState
自己并非异步的,而是 React
的批处理机制给人一种异步的假象。
异步代码和原生事件中
componentDidMount() { setTimeout(() => { console.log('调用setState'); this.setState({ index: this.state.index + 1 }) console.log('state', this.state.index); }, 0); }复制代码
如上面的代码,当咱们在异步代码中调用 setState
时,根据 JavaScript
的异步机制,会将异步代码先暂存,等全部同步代码执行完毕后在执行,这时 React
的批处理机制已经走完,处理标志设被设置为 false
,这时再调用 setState
便可当即执行更新,拿到更新后的结果。
在原生事件中调用 setState
并不会出发 React
的批处理机制,因此当即能拿到最新结果。
最佳实践
setState
的第二个参数接收一个函数,该函数会在 React
的批处理机制完成以后调用,因此你想在调用 setState
后当即获取更新后的值,请在该回调函数中获取。
this.setState({ index: this.state.index + 1 }, () => { console.log(this.state.index); })复制代码
推荐阅读:由实际问题探究setState的执行机制
为何有时连续屡次setState只有一次生效?
例以下面的代码,两次打印出的结果是相同的:
componentDidMount() { this.setState({ index: this.state.index + 1 }, () => { console.log(this.state.index); }) this.setState({ index: this.state.index + 1 }, () => { console.log(this.state.index); }) }复制代码
缘由就是 React
会批处理机制中存储的多个 setState
进行合并,来看下 React
源码中的 _assign
函数,相似于 Object
的 assign
:
_assign(nextState, typeof partial === 'function' ? partial.call(inst, nextState, props, context) : partial);复制代码
若是传入的是对象,很明显会被合并成一次,因此上面的代码两次打印的结果是相同的:
Object.assign( nextState, {index: state.index+ 1}, {index: state.index+ 1})复制代码
注意, assign
函数中对函数作了特殊处理,处理第一个参数传入的是函数,函数的参数 preState
是前一次合并后的结果,因此计算结果是准确的:
componentDidMount() { this.setState((state, props) => ({ index: state.index + 1 }), () => { console.log(this.state.index); }) this.setState((state, props) => ({ index: state.index + 1 }), () => { console.log(this.state.index); }) }复制代码
因此上面的代码两次打印的结果是不一样的。
最佳实践
React
会对屡次连续的 setState
进行合并,若是你想当即使用上次 setState
后的结果进行下一次 setState
,可让 setState
接收一个函数而不是一个对象。这个函数用上一个 state
做为第一个参数,将这次更新被应用时的 props
作为第二个参数。
React
事件并无绑定在真实的 Dom
节点上,而是经过事件代理,在最外层的 document
上对事件进行统一分发。
组件挂载、更新时:
经过 lastProps
、 nextProps
判断是否新增、删除事件分别调用事件注册、卸载方法。
调用 EventPluginHub
的 enqueuePutListener
进行事件存储
获取 document
对象。
根据事件名称(如 onClick
、 onCaptureClick
)判断是进行冒泡仍是捕获。
判断是否存在 addEventListener
方法,不然使用 attachEvent
(兼容IE)。
给 document
注册原生事件回调为 dispatchEvent
(统一的事件分发机制)。
事件初始化:
EventPluginHub
负责管理 React
合成事件的 callback
,它将 callback
存储在 listenerBank
中,另外还存储了负责合成事件的 Plugin
。
获取绑定事件的元素的惟一标识 key
。
将 callback
根据事件类型,元素的惟一标识 key
存储在 listenerBank
中。
listenerBank
的结构是: listenerBank[registrationName][key]
。
触发事件时:
触发 document
注册原生事件的回调 dispatchEvent
获取到触发这个事件最深一级的元素
遍历这个元素的全部父元素,依次对每一级元素进行处理。
构造合成事件。
将每一级的合成事件存储在 eventQueue
事件队列中。
遍历 eventQueue
。
经过 isPropagationStopped
判断当前事件是否执行了阻止冒泡方法。
若是阻止了冒泡,中止遍历,不然经过 executeDispatch
执行合成事件。
释放处理完成的事件。
React
在本身的合成事件中重写了stopPropagation
方法,将isPropagationStopped
设置为true
,而后在遍历每一级事件的过程当中根据此遍历判断是否继续执行。这就是React
本身实现的冒泡机制。
推荐阅读:【React深刻】React事件机制
在上面提到的事件处理流程中, React
在 document
上进行统一的事件分发, dispatchEvent
经过循环调用全部层级的事件来模拟事件冒泡和捕获。
在 React
源码中,当具体到某一事件处理函数将要调用时,将调用 invokeGuardedCallback
方法。
function invokeGuardedCallback(name, func, a) { try { func(a); } catch (x) { if (caughtError === null) { caughtError = x; } }}复制代码
可见,事件处理函数是直接调用的,并无指定调用的组件,因此不进行手动绑定的状况下直接获取到的 this
是不许确的,因此咱们须要手动将当前组件绑定到 this
上。
React
事件使用驼峰命名,而不是所有小写。
经过 JSX
, 你传递一个函数做为事件处理程序,而不是一个字符串。
在 React
中你不能经过返回 false
来阻止默认行为。必须明确调用 preventDefault
。
React
根据 W3C
规范定义了每一个事件处理函数的参数,即合成事件。
事件处理程序将传递 SyntheticEvent
的实例,这是一个跨浏览器原生事件包装器。它具备与浏览器原生事件相同的接口,包括 stopPropagation()
和 preventDefault()
,在全部浏览器中他们工做方式都相同。
React
合成的 SyntheticEvent
采用了事件池,这样作能够大大节省内存,而不会频繁的建立和销毁事件对象。
另外,无论在什么浏览器环境下,浏览器会将该事件类型统一建立为合成事件,从而达到了浏览器兼容的目的。
React
的全部事件都经过 document
进行统一分发。当真实 Dom
触发事件后冒泡到 document
后才会对 React
事件进行处理。
因此原生的事件会先执行,而后执行 React
合成事件,最后执行真正在 document
上挂载的事件
React
事件和原生事件最好不要混用。原生事件中若是执行了 stopPropagation
方法,则会致使其余 React
事件失效。由于全部元素的事件将没法冒泡到 document
上,致使全部的 React
事件都将没法被触发。。
在原生的 JavaScript
程序中,咱们直接对 DOM
进行建立和更改,而 DOM
元素经过咱们监听的事件和咱们的应用程序进行通信。
而 React
会先将你的代码转换成一个 JavaScript
对象,而后这个 JavaScript
对象再转换成真实 DOM
。这个 JavaScript
对象就是所谓的虚拟 DOM
。
当咱们须要建立或更新元素时, React
首先会让这个 VitrualDom
对象进行建立和更改,而后再将 VitrualDom
对象渲染成真实DOM。
当咱们须要对 DOM
进行事件监听时,首先对 VitrualDom
进行事件监听, VitrualDom
会代理原生的 DOM
事件从而作出响应。
推荐阅读:【React深刻】深刻分析虚拟DOM的渲染过程和特性
不少文章说 VitrualDom
能够提高性能,这一说法其实是很片面的。
直接操做 DOM
是很是耗费性能的,这一点毋庸置疑。可是 React
使用 VitrualDom
也是没法避免操做 DOM
的。
若是是首次渲染, VitrualDom
不具备任何优点,甚至它要进行更多的计算,消耗更多的内存。
VitrualDom
的优点在于 React
的 Diff
算法和批处理策略, React
在页面更新以前,提早计算好了如何进行更新和渲染 DOM
。实际上,这个计算过程咱们在直接操做 DOM
时,也是能够本身判断和实现的,可是必定会耗费很是多的精力和时间,并且每每咱们本身作的是不如 React
好的。因此,在这个过程当中 React
帮助咱们"提高了性能"。
因此,我更倾向于说, VitrualDom
帮助咱们提升了开发效率,在重复渲染时它帮助咱们计算如何更高效的更新,而不是它比 DOM
操做更快。
ReactElement
中有一个 $$typeof
属性,它被赋值为 REACT_ELEMENT_TYPE
:
var REACT_ELEMENT_TYPE = (typeof Symbol === 'function' && Symbol.for && Symbol.for('react.element')) || 0xeac7;复制代码
可见, $$typeof
是一个 Symbol
类型的变量,这个变量能够防止 XSS
。
若是你的服务器有一个漏洞,容许用户存储任意 JSON
对象, 而客户端代码须要一个字符串,这可能会成为一个问题:
// JSONlet expectedTextButGotJSON = { type: 'div', props: { dangerouslySetInnerHTML: { __html: '/* put your exploit here */' }, },};let message = { text: expectedTextButGotJSON };<p> {message.text}</p>复制代码
JSON
中不能存储 Symbol
类型的变量。
ReactElement.isValidElement
函数用来判断一个 React
组件是不是有效的,下面是它的具体实现。
ReactElement.isValidElement = function (object) { return typeof object === 'object' && object !== null && object.$$typeof === REACT_ELEMENT_TYPE;};复制代码
可见 React
渲染时会把没有 $$typeof
标识,以及规则校验不经过的组件过滤掉。
当你的环境不支持 Symbol
时, $$typeof
被赋值为 0xeac7
,至于为何, React
开发者给出了答案:
0xeac7
看起来有点像React
。
使用 React.createElement
或 JSX
编写 React
组件,实际上全部的 JSX
代码最后都会转换成 React.createElement(...)
, Babel
帮助咱们完成了这个转换的过程。
createElement
函数对 key
和 ref
等特殊的 props
进行处理,并获取 defaultProps
对默认 props
进行赋值,而且对传入的孩子节点进行处理,最终构形成一个 ReactElement
对象(所谓的虚拟 DOM
)。
ReactDOM.render
将生成好的虚拟 DOM
渲染到指定容器上,其中采用了批处理、事务等机制而且对特定浏览器进行了性能优化,最终转换为真实 DOM
。
JSX
只是为 React.createElement(component,props,...children)
方法提供的语法糖。
全部的 JSX
代码最后都会转换成 React.createElement(...)
, Babel
帮助咱们完成了这个转换的过程。
因此使用了 JSX
的代码都必须引入 React
。
babel
在编译时会判断 JSX
中组件的首字母,当首字母为小写时,其被认定为原生 DOM
标签, createElement
的第一个变量被编译为字符串;当首字母为大写时,其被认定为自定义组件, createElement
的第一个变量被编译为对象;
在 IE(8-11)
和 Edge
浏览器中,一个一个插入无子孙的节点,效率要远高于插入一整个序列化完整的节点树。
React
经过 lazyTree
,在 IE(8-11)
和 Edge
中进行单个节点依次渲染节点,而在其余浏览器中则首先将整个大的 DOM
结构构建好,而后再总体插入容器。
而且,在单独渲染节点时, React
还考虑了 fragment
等特殊节点,这些节点则不会一个一个插入渲染。
高阶组件能够看做 React
对装饰模式的一种实现,高阶组件就是一个函数,且该函数接受一个组件做为参数,并返回一个新的组件。
高阶组件(
HOC
)是React
中的高级技术,用来重用组件逻辑。但高阶组件自己并非ReactAPI
。它只是一种模式,这种模式是由React
自身的组合性质必然产生的。
function visible(WrappedComponent) { return class extends Component { render() { const { visible, ...props } = this.props; if (visible === false) return null; return <WrappedComponent {...props} />; } }}复制代码
上面的代码就是一个 HOC
的简单应用,函数接收一个组件做为参数,并返回一个新组件,新组建能够接收一个 visible props
,根据 visible
的值来判断是否渲染Visible。
咱们能够经过如下两种方式实现高阶组件:
函数返回一个咱们本身定义的组件,而后在 render
中返回要包裹的组件,这样咱们就能够代理全部传入的 props
,而且决定如何渲染,实际上 ,这种方式生成的高阶组件就是原组件的父组件,上面的函数 visible
就是一个 HOC
属性代理的实现方式。
function proxyHOC(WrappedComponent) { return class extends Component { render() { return <WrappedComponent {...this.props} />; } }}复制代码
对比原生组件加强的项:
可操做全部传入的 props
可操做组件的生命周期
可操做组件的 static
方法
获取 refs
返回一个组件,继承原组件,在 render
中调用原组件的 render
。因为继承了原组件,能经过this访问到原组件的 生命周期、props、state、render
等,相比属性代理它能操做更多的属性。
function inheritHOC(WrappedComponent) { return class extends WrappedComponent { render() { return super.render(); } }}复制代码
对比原生组件加强的项:
可操做全部传入的 props
可操做组件的生命周期
可操做组件的 static
方法
获取 refs
可操做 state
能够渲染劫持
推荐阅读:【React深刻】从Mixin到HOC再到Hook
HOC
能够实现的功能:
组合渲染
条件渲染
操做 props
获取 refs
状态管理
操做 state
渲染劫持
HOC
在业务中的实际应用场景:
日志打点
权限控制
双向绑定
表单校验
具体实现请参考我这篇文章:https://juejin.im/post/5cad39b3f265da03502b1c0a
Mixin
和 HOC
均可以用来解决 React
的代码复用问题。
图片来源于网络
Mixin
可能会相互依赖,相互耦合,不利于代码维护
不一样的 Mixin
中的方法可能会相互冲突
Mixin
很是多时,组件是能够感知到的,甚至还要为其作相关处理,这样会给代码形成滚雪球式的复杂性
而 HOC
的出现能够解决这些问题:
高阶组件就是一个没有反作用的纯函数,各个高阶组件不会互相依赖耦合
高阶组件也有可能形成冲突,但咱们能够在遵照约定的状况下避免这些行为
高阶组件并不关心数据使用的方式和缘由,而被包裹的组件也不关心数据来自何处。高阶组件的增长不会为原组件增长负担
减小状态逻辑复用的风险
Hook
和 Mixin
在用法上有必定的类似之处,可是 Mixin
引入的逻辑和状态是能够相互覆盖的,而多个 Hook
之间互不影响,这让咱们不须要在把一部分精力放在防止避免逻辑复用的冲突上。在不遵照约定的状况下使用 HOC
也有可能带来必定冲突,好比 props
覆盖等等,使用 Hook
则能够避免这些问题。
避免地狱式嵌套
大量使用 HOC
的状况下让咱们的代码变得嵌套层级很是深,使用 HOC
,咱们能够实现扁平式的状态逻辑复用,而避免了大量的组件嵌套。
让组件更容易理解
在使用 class
组件构建咱们的程序时,他们各自拥有本身的状态,业务逻辑的复杂使这些组件变得愈来愈庞大,各个生命周期中会调用愈来愈多的逻辑,愈来愈难以维护。使用 Hook
,可让你更大限度的将公用逻辑抽离,将一个组件分割成更小的函数,而不是强制基于生命周期方法进行分割。
使用函数代替class
相比函数,编写一个 class
可能须要掌握更多的知识,须要注意的点也越多,好比 this
指向、绑定事件等等。另外,计算机理解一个 class
比理解一个函数更快。Hooks
让你能够在 classes
以外使用更多 React
的新特性。
下篇预告:
ReactDiff
算法的策略是什么?
React
中 key
的做用是什么?
ReactFiber
是什么?为何要引入?
为何推荐在 componentDidMount
中发起网络请求?
React
代码优化?
React
组件设计要掌握哪些原则?
Redux
的核心原理是什么?
什么是 Redux
中间件?
Reduxconnect
函数的实现策略?
Mox
的核心原理是什么?
Redux
和 Mobx
的异同点,如何选择?
转载 查看原文