Interview -- React 相关面试题
同 vue 一样,React 框架类面试主要考察三个方面:
- 框架的使用(基本使用, 高级特性, 周边插件)
- 框架的原理(基本原理的了解, 热门技术的深度和全面性)
- 框架的实际应用,即设计能力(组件结构和数据结构)
基本使用
jsx 的基本使用
- 变量、表达式
- class style
- 子元素和组件
- 条件判断:
if else
、三元表达式和 逻辑运算符&&
与||
事件
React 的事件函数需要进行
this
绑定,不绑定会出现this
丢失的问题。或者使用箭头函数,则无需进行this
绑定;event 事件是 SyntheticEvent, 是模拟出来的 DOM 事件所有的能力;
event.nativeEvent
是原生事件对象。在 React17 以前, 所有的事件都被挂载到 document 上,和 DOM 事件不一样,和 Vue 事件也不一样。 React17 后事件绑定到 root 组件上了,这样有利于多个 React 版本并存,例如微前端。
import React from 'react'
export default class JsxBase extends React.Component {
constructor(props) {
super(props)
this.state = {
name: 'huy',
age: 18,
flag: true,
}
}
render() {
const exprElem = <p>{this.state.flag ? 'yes' : 'no'}</p>
return (
<div>
{exprElem}
<button onClick={this.clickHandel}>点击</button>
<button onClick={this.clickHandel2}>点击事件</button>
</div>
)
}
clickHandel() {
console.log(this) // undefined
}
clickHandel2(event) {
event.preventDefault()
event.stopPropagation()
console.log('target', event.target) // 指向当前元素, 即当前元素触发
console.log('current target', event.currentTarget) // 指向当前元素
// 该 event 是 React 封装的
console.log('event', event)
console.log('event.__proto__.constructor', event.__proto__.constructor)
// 原生事件是 event.nativeEvent
console.log('nativeEvent', event.nativeEvent)
}
}
setState
React <= 17 的版本中:
- React 组件事件: 异步更新 + 会合并 state
- DOM 事件 和 setTimeout 等: 同步更新, 不会合并 state
React 18 后
- React 组件事件: 异步更新 + 会合并 state
- DOM 事件 和 setTimeout 等: 异步更新, 不会合并 state
特点:
- 不可变值(函数式编程, 纯函数)
- 可能是异步更新
- 可能会被合并
不可变值的修改
// 数组修改
const listCopy = this.state.list.slice() // 先拷贝一份出来, 再进行复运算
listCopy.splice(2, 0, 'insert string') // 中间插入/删除
this.setState({
list1: this.state.list1.concat(10), // 追加
list2: [...this.state.list2, 20], // 解构再追加
list3: this.state.list3.slice(0, 2), // 截取
list4: this.state.list4.filter((item) => item > 10), // 筛选
list5: listCopy,
})
// 对象修改, 不可以直接对 obj 的属性进行修改, 这样是违法不可变值
this.setState({
obj1: Object.assign({}, this.state.obj1, { a: 10 }),
obj2: { ...this.state.obj2, a: 20 },
})
setState 同步/异步
在 React18 以前, 可能同步, 可能异步(只有 React 组件事件才批处理是异步的)
- 直接修改是异步的;
- 在 setState 的回调函数中是同步的;
- 在 setTimeout 中是同步的;
- 在自定义的 DOM 事件中是同步的;
import React from 'react'
export default class SetStateTime extends React.Component {
constructor(props) {
super(props)
this.state = {
count: 0,
}
}
render() {
return (
<div>
<div>setState 渲染时机 同步/异步</div>
<button onClick={this.add}>+</button>
</div>
)
}
add = () => {
this.setState(
{
count: this.state.count + 1,
},
() => {
console.log('同步: 回调函数中同步保留结果:', this.state.count)
}
)
console.log('异步: count', this.state.count)
setTimeout(() => {
this.setState({
count: this.state.count + 10,
})
console.log('同步: setTimeout 中是同步的', this.state.count)
}, 0)
}
bodyClickHandler = () => {
this.setState({
count: this.state.count + 100,
})
console.log('同步: 在自定义的 DOM 事件中', this.state.count)
}
componentDidMount() {
document.body.addEventListener('click', this.bodyClickHandler)
}
componentWillUnmount() {
document.body.removeEventListener('click', this.bodyClickHandler)
}
}
可能会被合并
- 直接连续修改会被合并更新
- 在函数中是不会被合并更新的, 每一个都会执行, 因为函数式一个一个进行执行的
// 直接连续修改, 会被合并, 以最后一个为主
this.setState({
mergeCount: this.state.mergeCount + 1,
})
this.setState({
mergeCount: this.state.mergeCount + 1,
})
this.setState({
mergeCount: this.state.mergeCount + 1,
})
// 传入函数, 不会被合并(因为函数式一个一个执行的)
this.setState((preVState, props) => {
return {
mergeCount: preVState.mergeCount + 10,
}
})
this.setState((preVState, props) => {
return {
mergeCount: preVState.mergeCount + 10,
}
})
React 18 后的变化
加入了 Automatic Batching 自动批处理, 都变成异步更新了。
import { useState, useEffect } from 'react'
export default function useSetSateTime() {
const [value, setValue] = useState(100)
const clickHandler = () => {
setValue(value + 1)
setValue(value + 1)
console.log('异步更新', value)
setTimeout(() => {
setValue(value + 10)
setValue(value + 10)
console.log('setTimeout 依旧是异步更新', value)
}, 0)
}
useEffect(() => {
// 绑定 DOM 事件
document.getElementById('btn2').addEventListener('click', () => {
setValue(value + 100)
setValue(value + 100)
setValue(value + 100)
console.log('自定义 DOM 事件还是异步更新', value)
})
})
return (
<div>
<div>在 React 18 中全变为异步更新: {value}</div>
<button onClick={clickHandler}>异步更新+</button>
<button id="btn2">setTimeout +</button>
</div>
)
}
组件的生命周期
生命周期只存在于 类组件中,函数式组件中是用 hooks 来实现生命周期函数(useEffect 管理副作用)功能的。
- 挂载时: constructor -> render -> componentDidMount
- 更新时: (shouldComponentUpdate) -> render -> componentDidUpdate
- 卸载时: componentWillUnmount
父组件的生命周期和 Vue 一样的。
挂载阶段
- 父组件 constructor
- 父组件 render
- 子组件 constructor
- 子组件 render
- 子组件 componentDidMount
- 父组件 componentDidMount
更新阶段
- 父组件 shouldComponentUpdate
- 父组件 render
- 子组件 shouldComponentUpdate
- 子组件 render
- 子组件 componentDidUpdate
- 父组件 componentDidUpdate
卸载阶段
- 卸载子组件
- 父组件 shouldComponentUpdate
- 父组件 render
- 子组件 componentWillUnmount
- 父组件 componentDidUpdate
- 父组件卸载
- 父组件 componentWillUnmount
- 子组件 componentWillUnmount
高级使用
知识点有: 函数组件、受控和非受控组件、ref、protals、context、异步组件(懒加载)、性能优化、shouldComponentUpdate、纯组件、不可变值 immutablejs、高阶组件、render prop 等。
受控组件和非受控组件
受控组件的值(例如 input 的 value)受 React 的 state 控制。
任何时候只要 state 发生变化,组件就会重新渲染,这样就可以确保用户界面和数据保持同步。
受控组件需要额外的代码来处理每个状态变化,以及处理用户输入。
<input type="text" value={this.state.value} onChange={this.handleChange} />
非受控组件的值不受 React 的 state 控制,而是由 DOM 本身管理。
在大多数情况下,非受控组件可能更简单,因为不需要处理每个状态变化,也不需要在组件中存储每个状态的值。
class UncontrolledInput extends React.Component {
constructor(props) {
super(props)
this.inputRef = React.createRef()
}
handleSubmit = (event) => {
console.log('A name was submitted: ' + this.inputRef.current.value)
event.preventDefault()
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<input type="text" ref={this.inputRef} />
<button type="submit">Submit</button>
</form>
)
}
}
优先使用受控组件,符合 React 设计原则,非受控组件的使用场景:
必须手动操作 DOM 元素, setState 实现不了, 如文件上传:
<input type=file />
; 已及某些富文本编辑器, 需要传入 DOM 元素。
Protals
使用场景: 处理一些兼容性的问题
- 父组件
overflow: hidden;
子组件想要渲染出来;- 父组件
z-index
值太小;- fixed 需要放在 body 的第一层级。
Portal 是 React 16.3 版本引入的一个特性,它允许你在 DOM 树中的任何地方渲染子组件。传统的 React 组件渲染在组件的父组件的 DOM 节点中,但 Portal 可以将组件渲染到任何指定的 DOM 节点中。这对于创建弹出窗口、模态对话框、悬浮菜单等组件非常有用。
Portal 的使用方法很简单,只需要在组件的 render
方法中使用 ReactDOM.createPortal
函数:
import ReactDOM from 'react-dom'
class MyPortalComponent extends React.Component {
render() {
return ReactDOM.createPortal(
this.props.children,
document.getElementById('portal-root')
)
}
}
在上面的例子中,MyPortalComponent
组件使用 ReactDOM.createPortal
函数将它的子组件渲染到 id
为 portal-root
的 DOM 节点中。这样,无论 MyPortalComponent
组件在 DOM 树中的什么位置,它的子组件都会被渲染到指定的 DOM 节点中。
Portal 是一个非常有用的特性,但是需要小心使用,因为它可以将组件渲染到任何地方,可能会导致一些不可预料的问题。
context
Context 是一种跨组件传递数据的方式, 用于公共信息传递给各个组件,用 props 太过于繁琐,用 redux 过于重。 如定义一些主题等。
使用步骤:
要使用 Context,首先需要创建一个 Context 对象:
const MyContext = React.createContext(defaultValue)
使用
MyContext.Provider
组件在组件树中提供数据:<MyContext.Provider value={value}> {/* 这里是子组件 */} </MyContext.Provider>
在子组件中,可以使用
MyContext.Consumer
组件来消费数据:<MyContext.Consumer> {value => /* 使用 value */} </MyContext.Consumer>
或者在函数式组件中使用
useContext
钩子来消费数据:import { useContext } from 'react' function MyComponent() { const value = useContext(MyContext) // 使用 value }
异步组件
在 Vue 中使用 import()
,在 React 用使用 React.lazy
和 React.Suspense
。
const Child = React.lazy(() => import('./child'))
// 使用
class App extends React.Component {
render() {
return (
<div>
<React.Suspense fallback={<div>loading</div>}>
<Child />
</React.Suspense>
</div>
)
}
}
性能优化
SCU (shouldComponentUpdate)
React 默认: 父组件有更新, 子组件则无条件也更新; 即 SCU 默认返回 true
但是 SCU 并不是每次都要用, 而应该是需要时才优化。
基本使用:
shouldComponentUpdate (nextProps, nextState) {
if (nextProps.count !== this.state.count) {
return true // 可以渲染
}
return false // 不重复渲染
}
注意事项: SCU 必须配合 不可变值一起使用。
// 正确示例
// this.setState({
// list: this.state.list.concat({name: 'Huy'})
// })
// 错误示例
this.state.list.push({ name: 'Huy' }) // 此时 list 已经完成修改
this.setState({ list: this.state.list }) // 这是再设置, 俩者值相同, 不会触发重新渲染, 因此也不会触发 SCU 检测。
PureComponent 和 memo
- PureComponent 是在 SCU 中实现了浅比较。
- memo, 是函数式组件中的 PureComponent
浅比较能够解决绝大多数问题,深比较, 性能消耗较大,尽量不使用。
function MyComponent(props) {
/** 使用 props 渲染 */
}
function areEqual(prevProps, nextProps) {
/**
* 该函数比较前后的 props 是否一致, 最后返回是否该重新渲染的 boolean 值
*/
}
export default React.memo(MyComponent, areEqual)
immutable.js
Immutable.js 是一个 JavaScript 库,用于处理不可变(Immutable)数据结构。在 JavaScript 中,对象和数组是可变的(Mutable)数据结构,它们的值可以随时被修改。而不可变数据结构是指一旦创建,其值就不能被修改的数据结构。Immutable.js 提供了一系列不可变数据结构的实现,包括 List、Map、Set 等,以及一些操作这些数据结构的方法。
const map1 = Immutable({ a: 1, b: 2 })
const map2 = map1.set('a', 100) // 返回一个新的数据结果, 原 map1 不变
map1.get('a') // 1
map2.get('a') // 100
组件公共逻辑的抽离
- 高阶组件 HOC(Higher-Order Component)
- Render Props 设计模式
mixin 已废弃
HOC
HOC 是 React 中的一种设计模式,用于增强现有组件的功能。HOC 是一个函数,接受一个组件作为参数,并返回一个新的组件。
HOC 的基本思想是将通用的逻辑和功能抽离出来,封装成一个函数,然后通过这个函数来增强组件的功能。这样可以使代码更加清晰、易于理解和维护。
function withLogger(WrappedComponent) {
return class extends React.Component {
// 在此定义多个组件的公共逻辑
render() {
return <WrappedComponent {...this.props} />
}
}
}
const EnhancedComponent = withLogger(MyComponent)
Render Props 设计模式
Render Props 是通过一个名为 render
的 prop
来告诉组件需要渲染什么内容的一种技术。这个 prop 是一个函数,这个函数返回一个 React 元素(或者说一个组件)。
使用 Render Props 模式的组件会有一个或多个带有 render prop 的方法,这些方法允许调用者来决定组件渲染的内容。
// 使用
const App = () => (
<MyComponent
render={
/** render 是一个函数组件 */
(props) => (
<p>
{props.x} {props.y}
</p>
)
}
/>
)
// render props 实际封装
class MyComponent extends React.Component {
constructor() {
this.state = {
/** state 即多个组件的公共逻辑的数据 */
}
}
/** 修改 state 值 */
render() {
return <div>{this.props.render(this.state)}</div>
}
}
redux
具体总结使用可见 《React 之数据管理 Redux》
Redux 的基本使用
创建储存状态 store:
const store = createStore(reducer)
创建上面的 reducer 纯函数: 接收一个旧状态和一个 action, 然后返回一个新状态。
创建 action, 描述执行的操作:
const incrementAction = (num) => ({type: 'INCREMENT', num})
;发送 action 更新状态, 需要通过
store.dispatch
发送刚刚创建的 action:store.dispatch(incrementAction(10))
通过
store.subscribe
订阅状态变化:store.subscribe(() => { // 数据变化,自动执行该函数 console.log(store.getState()) })
通过
store.getState()
使用 store 中的数据
在组件中使用 react-redux
react 提供了一个 connect api 用于连接 react 组件和 redux store,它会返回一个高阶函数 HOC。
connect 的作用可以简单地概括为:将 Redux store 中的数据和方法映射到组件的 props 中。
// 通过 connect 将 mapStateToProps 和 mapDispatchToProps 俩个高阶函数映射到 App 组件的 props 中
export default connect(mapStateToProps, mapDispatchToProps)(App)
此时为了让组件拿到 store 的 state 对象, 就要用到 redux 提供的 Provider 组件, 实际上就是 Context。
<Provider store={store}>
<App />
</Provider>
同步和异步 action
异步 action 实际上就是在同步的基础上加入了 Promise
// 同步 action
export const addTodo = (text) => {
// 返回 action 对象
return {
type: 'ADD',
id: nextId++,
text,
}
}
// 异步 action
export const addTodoAsync = (text) => {
// 返回函数, 其中有 dispatch 参数
return (dispatch) => {
fetch(url).then((res) => {
dispatch(addTodo(res.text))
})
}
}
当然为了实现上面的这样的,需要引入 redux-thunk,它允许 action 返回一个函数,而不是一个普通的 js 对象(同步是对象)。
import { createStore, applyMiddleware } from 'redux'
import thunk from 'redux-thunk'
import reducer from './reducer.js'
const store = createStore(reducer, applyMiddleware(thunk))
框架原理
jsx 的本质是什么
JSX 的本质是一种语法糖,它会被编译成 JavaScript 代码。当我们在代码中写 JSX 时,Babel 或者 TypeScript 等编译工具会将 JSX 编译成 React.createElement()
函数调用。
// JSX
const element = <h1>Hello, world!</h1>
// 编译后的 JavaScript
const element = React.createElement('h1', null, 'Hello, world!')
说一下 React 的合成事件机制
- react 17 以前, 所有事件挂载到 document 上; react17 后所有事件都挂载到 root 的节点上
- event 不是原生的, 是 SyntheticEvent 合成事件对象;
- 和 Vue 事件不同, 和 DOM 事件也不同;
目的:
- 更好的兼容性和跨平台;
- 载到 document, 减少内存消耗, 避免频繁解绑;
- 方便事件的统一管理(如事件机制)。
说一下 setState 和 batchUpdate
React 18 以前:
- 有时异步(普通使用), 有时同步(setTimeout、DOM 事件);
- 有时合并(对象形式), 有时不合并(函数形式);
原理:
- setState 主流程:
- batchUpdate 机制:
- transaction (事务)机制:
setState 的主流程是: 当调用 setState 时, 会判断当前是否处于 batch update 中?
// 异步的
increase = () => {
// 开始处于 batchUpdate
// isBatchUpdates = true
this.setState({ count: this.state.count + 1 })
// 结束, isBatchUpdates = false
}
// 同步的
increase = () => {
// 开始处于 batchUpdate
// isBatchUpdates = true
setTimeout(() => {
// 此时 isBatchUpdates = false
this.setState({ count: this.state.count + 1 })
}, 0)
// 结束, isBatchUpdates = false
}
组件渲染和更新过程
组件渲染过程
- props 设置 state
- render() 生成 vNode
- patch(elem, vNode)
组件更新过程
- setState(newState) --> dirtyComponent(可能有子组件)
- render() 生成 newVNode
- patch(vNode, newVNode)
更新的俩个阶段:
- reconciliation(对帐) 阶段: 执行 diff 算法, 纯 JS 计算;
- commit 阶段: 将 diff 结果渲染成 DOM
可能存在的性能问题: js 是单线程且和 DOM 渲染共用一个线程。当组件作够复杂时,组件更新时计算和渲染压力较大,同时再有 DOM 操作需求(动画和鼠标的拖拽等)将造成卡顿。
解决方案:fiber
- 将 reconciliation(对帐) 阶段进行任务拆分(commit 无法拆分);
- 当 DOM 需要渲染时暂停, 空闲时恢复。用
window.requestIdleCallback
进行检查。
函数组件和 class 组件的区别
- 函数式组件是纯函数, 输入 props, 输出 jsx;
- 没有实例, 没有生命周期, 没有 state 只能接收 Props;
- 不能拓展其他方法;
react-router 如何配置懒加载
也是 React.lazy 和 Suspense 组合。
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom'
import { Suspense, lazy } from 'react'
const Home = lazy(() => import('./home'))
const About = lazy(() => import('./about'))
const App = () => {
;<Router>
<Suspense fallback={<div>loading</div>}>
<Switch>
<Route exact path="/" component={Home} />
<Route exact path="/about" component={About} />
</Switch>
</Suspense>
</Router>
}
React 性能优化
- 渲染列表时加 key
- 自定义事件、DOM 事件及时销毁
- 合理使用异步组件
- 减少 bind this 的次数
- 合理使用 SCU、PureComponent 和 memo
- 合理使用
Immutable.js
考虑不可变的使用成本 - 前段通用的性能优化, 如 图片懒加载、使用 SSR 等
React 和 Vue 的区别
共同点:
- 都支持组件化
- 都是数据驱动视图
- 都使用 vDOM 操作 DOM
区别:
- React 使用 JSX 拥抱 js, Vue 使用模版拥抱 html;
- React 函数式编程, Vue 声明式编程;
- React 更多的需要自动管理, Vue 框架是自动档。
React Hooks
useEffect 模拟生命周期
// 1. 模拟 componentDidMount 和 componentDidUpdate
useEffect(() => {
console.log('单纯使用, 则模拟 componentDidMount 和 componentDidUpdate')
})
// 2. 模拟 componentDidMount
useEffect(() => {
console.log('依赖为空数组则模拟 componentDidMount')
}, [])
// 3. 模拟 componentDidUpdate
useEffect(() => {
console.log('传入所需依赖, 则模拟 componentDidUpdate')
}, [count])
// 4. 模拟 componentWillUnmount
useEffect(() => {
// 副作用, 在返回函数中进行清理, 同时可单做"模拟" componentWillUnMount 使用, 但不完全相等
return () => {
console.log('在 Callback 中模拟 componentWillUnMount')
}
}, [])
// 5. 模拟 componentWillUpdate
useEffect(() => {
// 「特别注意」: props 发生变化,即组件会开始更新,这里的回调函数也会执行
// 准确的说, 返回函数, 会在下一次 effect 执行之前, 被执行
return () => {
console.log('组件更新时, 无依赖, 或者依赖数组不为空时, 这里的回调也会执行')
}
})
useEffect 依赖为 空数组
[]
, 组件销毁则执行回调, 等于 componentWillUnMountuseEffect 无依赖 或则 依赖数组不为空 [a, b], 组件更新时, 也会执行回调。即下一次执行 useEffect 之前, 会执行回调函数, 无论更新或者卸载。
为什么会有 React Hooks, 它解决了哪些问题?
- 解决了逻辑复用, 和状态复用问题
- 但是 React 官方并不推荐将 hooks 同生命周期混为一谈。例如,useEffect 无法完全替代 componentDidMount、componentDidUpdate 和 componentWillUnmount 这些生命周期方法。因为 useEffect 是在节点挂载前调用,但是它却是在挂载后执行。
Redux 解决了什么问题?
- 数据状态管理,可以让数据到视图一一对应;
- 可以实现数据回退;
- 方便调试;