Interview -- React 相关面试题

Huy大约 15 分钟anonymousnote

同 vue 一样,React 框架类面试主要考察三个方面:

  • 框架的使用(基本使用, 高级特性, 周边插件)
  • 框架的原理(基本原理的了解, 热门技术的深度和全面性)
  • 框架的实际应用,即设计能力(组件结构和数据结构)

基本使用

jsx 的基本使用

  • 变量、表达式
  • class style
  • 子元素和组件
  • 条件判断: if else、三元表达式和 逻辑运算符&&||

事件

  1. React 的事件函数需要进行 this 绑定,不绑定会出现 this 丢失的问题。或者使用箭头函数,则无需进行 this 绑定;

  2. 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 组件事件才批处理是异步的)

  1. 直接修改是异步的;
  2. 在 setState 的回调函数中是同步的;
  3. 在 setTimeout 中是同步的;
  4. 在自定义的 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)
  }
}

可能会被合并

  1. 直接连续修改会被合并更新
  2. 在函数中是不会被合并更新的, 每一个都会执行, 因为函数式一个一个进行执行的
// 直接连续修改, 会被合并, 以最后一个为主
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 一样的。

挂载阶段

  1. 父组件 constructor
  2. 父组件 render
  3. 子组件 constructor
  4. 子组件 render
  5. 子组件 componentDidMount
  6. 父组件 componentDidMount

更新阶段

  1. 父组件 shouldComponentUpdate
  2. 父组件 render
  3. 子组件 shouldComponentUpdate
  4. 子组件 render
  5. 子组件 componentDidUpdate
  6. 父组件 componentDidUpdate

卸载阶段

  • 卸载子组件
    1. 父组件 shouldComponentUpdate
    2. 父组件 render
    3. 子组件 componentWillUnmount
    4. 父组件 componentDidUpdate
  • 父组件卸载
    1. 父组件 componentWillUnmount
    2. 子组件 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

使用场景: 处理一些兼容性的问题

  1. 父组件 overflow: hidden; 子组件想要渲染出来;
  2. 父组件 z-index 值太小;
  3. 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 函数将它的子组件渲染到 idportal-root 的 DOM 节点中。这样,无论 MyPortalComponent 组件在 DOM 树中的什么位置,它的子组件都会被渲染到指定的 DOM 节点中。

Portal 是一个非常有用的特性,但是需要小心使用,因为它可以将组件渲染到任何地方,可能会导致一些不可预料的问题。

context

Context 是一种跨组件传递数据的方式, 用于公共信息传递给各个组件,用 props 太过于繁琐,用 redux 过于重。 如定义一些主题等。

使用步骤:

  1. 要使用 Context,首先需要创建一个 Context 对象:

    const MyContext = React.createContext(defaultValue)
    
  2. 使用 MyContext.Provider 组件在组件树中提供数据:

    <MyContext.Provider value={value}> {/* 这里是子组件 */} </MyContext.Provider>
    
  3. 在子组件中,可以使用 MyContext.Consumer 组件来消费数据:

    <MyContext.Consumer> {value => /* 使用 value */} </MyContext.Consumer>
    
  4. 或者在函数式组件中使用 useContext 钩子来消费数据:

    import { useContext } from 'react'
    function MyComponent() {
      const value = useContext(MyContext) // 使用 value
    }
    

异步组件

在 Vue 中使用 import(),在 React 用使用 React.lazyReact.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 是通过一个名为 renderprop 来告诉组件需要渲染什么内容的一种技术。这个 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 的基本使用

Redux 流程图open in new window

  1. 创建储存状态 store: const store = createStore(reducer)

  2. 创建上面的 reducer 纯函数: 接收一个旧状态和一个 action, 然后返回一个新状态。

  3. 创建 action, 描述执行的操作: const incrementAction = (num) => ({type: 'INCREMENT', num});

  4. 发送 action 更新状态, 需要通过 store.dispatch 发送刚刚创建的 action: store.dispatch(incrementAction(10))

  5. 通过 store.subscribe 订阅状态变化:

    store.subscribe(() => {
      // 数据变化,自动执行该函数
      console.log(store.getState())
    })
    
  6. 通过 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 事件也不同;
合成事件
合成事件

目的:

  1. 更好的兼容性和跨平台;
  2. 载到 document, 减少内存消耗, 避免频繁解绑;
  3. 方便事件的统一管理(如事件机制)。

说一下 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
}

组件渲染和更新过程

组件渲染过程

  1. props 设置 state
  2. render() 生成 vNode
  3. patch(elem, vNode)

组件更新过程

  1. setState(newState) --> dirtyComponent(可能有子组件)
  2. render() 生成 newVNode
  3. patch(vNode, newVNode)

更新的俩个阶段:

  1. reconciliation(对帐) 阶段: 执行 diff 算法, 纯 JS 计算;
  2. commit 阶段: 将 diff 结果渲染成 DOM

可能存在的性能问题: js 是单线程且和 DOM 渲染共用一个线程。当组件作够复杂时,组件更新时计算和渲染压力较大,同时再有 DOM 操作需求(动画和鼠标的拖拽等)将造成卡顿。

解决方案:fiber

  1. 将 reconciliation(对帐) 阶段进行任务拆分(commit 无法拆分);
  2. 当 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 性能优化

  1. 渲染列表时加 key
  2. 自定义事件、DOM 事件及时销毁
  3. 合理使用异步组件
  4. 减少 bind this 的次数
  5. 合理使用 SCU、PureComponent 和 memo
  6. 合理使用 Immutable.js 考虑不可变的使用成本
  7. 前段通用的性能优化, 如 图片懒加载、使用 SSR 等

React 和 Vue 的区别

共同点:

  1. 都支持组件化
  2. 都是数据驱动视图
  3. 都使用 vDOM 操作 DOM

区别:

  1. React 使用 JSX 拥抱 js, Vue 使用模版拥抱 html;
  2. React 函数式编程, Vue 声明式编程;
  3. 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 依赖为 空数组 [], 组件销毁则执行回调, 等于 componentWillUnMount

useEffect 无依赖 或则 依赖数组不为空 [a, b], 组件更新时, 也会执行回调。即下一次执行 useEffect 之前, 会执行回调函数, 无论更新或者卸载。

为什么会有 React Hooks, 它解决了哪些问题?

  1. 解决了逻辑复用, 和状态复用问题
  2. 但是 React 官方并不推荐将 hooks 同生命周期混为一谈。例如,useEffect 无法完全替代 componentDidMount、componentDidUpdate 和 componentWillUnmount 这些生命周期方法。因为 useEffect 是在节点挂载前调用,但是它却是在挂载后执行。

Redux 解决了什么问题?

  1. 数据状态管理,可以让数据到视图一一对应;
  2. 可以实现数据回退;
  3. 方便调试;

使用 React Hooks 遇到哪些坑?

Hook 相比 HOC 和 Render Props 有哪些优点?

Loading...