【前端面试】披荆斩棘,25道高频React面试题

1,796 阅读12分钟

引言

【前端面试】从小作坊走到大厂,回头看这条路真的好长

【前端面试】面试官:我看看你的React Hooks掌握的怎么样

书接上文,继续分享React相关面试题, 有些没写答案的我近期慢慢补

广告时间

程序员和NBA球员一样,黄金时间就这么几年,如果你是一个有进取心的少年,想摆脱如今的工作困境,来找我,我可以跟你分享我是怎么一步步走过来。

作者这两年的面试经历几乎覆盖了上海地区和杭州地区的全部大厂和部分中厂,也可以给你提供帮助:

  • 找工作建议
  • 如何准备面试
  • 如何学习
  • 薪资评估
  • 未来规划和成长

因为作者也在公司做一面面试官,众所周知一面是最难的,可以帮你做

  • 大厂的模拟面试,
  • 前端知识点的探底,帮你差缺补漏。
  • 最粗暴的,你可以简历发我,可以帮你把关。

二维码:

p9-juejin.byteimg.com/tos-cn-i-k3…

只需要两杯咖啡钱/h~ 欢迎在点击链接扫描添加我的企业微信了解。祝大家在2022年面试顺利~

面试分享

1.子组件和父组件componentDidMount哪一个先执行

(待补充)

2.redux的一般流程

(待补充)

3.设计一个Input组件

import React, { PureComponent } from 'react';
import classNames from 'classnames';
import { getOtherProps } from 'utils/tool';
import styles from './Input.less';

export default class Input extends PureComponent {
  constructor(props) {
    super(props);
    this.state = {
      isHasCloseBtn: false,
    };
    this.input = React.createRef();
  }

  emptyValue = () => {
    this.input.current.value = '';
    this.input.current.focus();
    this.setState({
      isHasCloseBtn: false,
    });
  }
  
  onChange = (e) => {
    var value = e.target.value;

    if (value) {
      if (!this.state.value) {
        this.setState({
          isHasCloseBtn: true,
        });
      }
    } else {
      this.setState({
        isHasCloseBtn: false,
      });
    }
    if (isFunction(onChange)) {
      onChange(e);
    }
  }

  handleOnPressEnter = (e) => {
    const { onPressEnter } = this.props;
    if (e.key === 'Enter') {
      if (isFunction(onPressEnter)) {
        onPressEnter({
          value: e.target.value,
        }, e);
      }
    }
  }

  renderLabeledInput(children) {
    const { addonBefore, addonAfter } = this.props;

    if (!addonBefore && !addonAfter) {
      return children;
    }

    const addonAfterGroupWrapperCls = classNames({
      [styles.addonAfterGroupWrapper]: true,
      [styles.isString]: isString(addonAfter),
    });

    const addonBeforeGroupWrapper = classNames({
      [styles.addonBeforeGroupWrapper]: true,
      [styles.isString]: isString(addonBefore),
    });

    const _addonBefore = addonBefore ? (
      <span className={addonBeforeGroupWrapper}>
        {addonBefore}
      </span>      
    ) : null;

    const _addonAfter = addonAfter ? (
      <span className={addonAfterGroupWrapperCls}>
        {addonAfter}
      </span>
    ) : null;

    return (
      <span className={styles.inputWrapper}>
        {_addonBefore}
        {React.cloneElement(children)}
        {_addonAfter}
      </span>
    )
    
  }

  renderLabeledIcon = (children) => {
    const { prefix, suffix } = this.props;
    
    const _prefix = prefix ? (
      <span className={styles.prefix} onClick={this.emptyValue}>{prefix}</span>
    ) : null;

    const closeBtn = (<i className={styles.closeBtn} onClick={this.emptyValue}></i>)

    const _suffix = this.state.isHasCloseBtn ? closeBtn : (
      <span className={styles.suffix}>
        {
          suffix ? suffix : null
        }
      </span>
    )

    return (
      <span className={styles.inputGroupWrapper}>
        {_prefix}
        {React.cloneElement(children)}
        {_suffix}
      </span>
    );
  }

  renderInput = () => {
    const {
      type,
      value,
      size='default',
    } = this.props;

    // 这里只对text和password做处理,因为其他type会自带一些功能,像number、date可以基于这个基础input开发
    const _type = type === 'password' ? 'password' : 'text';

    // 控制input的尺寸,提高了small、large、default, 具体大小
    const inputCls = classNames({
      [styles.input]: true,
      [styles.small]: (size === 'small'),
      [styles.large]: (size === 'large'),
      [styles.default]: (size === 'default'),
    });

    // 定义了getOhterProps方法,用来获取除了第二个参数包含的其他props
    const otherProps = getOtherProps(this.props, ['size', 'addonAfter', 'addonBefore', 'prefix', 'suffix', 'type', 'onPressEnter', 'className', 'onChange']);
    
    if ('value' in otherProps) {
      otherProps.value = fixControlledValue(value);
    }

    return this.renderLabeledIcon(
      <input
        className={inputCls}
        type={_type}
        onChange={this.onChange}
        onKeyPress={this.handleOnPressEnter}
        {...otherProps}
        ref={this.input}
      />       
    )
  }

  render() {
    return this.renderLabeledInput(this.renderInput());
  }
}

function isFunction(el) {
  if (getType(el) === "[object Function]") {
    return true;
  }
  return false;
}

function isString(el) {
  if (getType(el) === '[object String]') {
    return true;
  }
  return false;
}

function getType(el) {
  return Object.prototype.toString.call(el);
}

function fixControlledValue(value) {
  if (typeof value === 'undefined' || value === null) {
    return '';
  }
  return value;
}

4.react组件的优化

从pureRenderMixin、ShouldComponentUpdate等方面说了下,以及组件的设计和木偶组件的函数编写方式说了下

5.react组件的通信

6.react 的virtual dom和diff算法的实现方式

7.react的ssr

8.装饰器的原理

9.自己实现一个通用方法,做到不需要使用bind与装饰器达到调用事件处理方法(里面要用到this)怎么调用

柯里化 + apply

10.React render做了什么

11.Redux的原理

Redux是JavaScript状态容器,能提供可预测化的状态管理,本质上是一个发布-订阅模式

12.Redux 遵循的三个原则是什么?

  • 单一事实来源:整个应用程序的状态存储在单个存储中的对象/状态树中
  • 状态是只读的:更改状态的惟一方法是触发一个动作
  • 使用纯函数来修改state:为了描述 action 如何改变 state tree ,你需要编写reducers

13.redux 做状态管理和发布订阅模式有什么区别

redux 其实也是一个发布订阅,但是 redux 可以做到数据的可预测和可回溯。

14.react-redux 的原理,是怎么跟 react 关联起来的

react-redux 的核心组件只有两个,Provider 和 connect,Provider 存放 Redux 里 store 的数据到 context 里,通过 connect 从 context 拿数据,通过 props 传递给 connect 所包裹的组件。

15.React组件中类组件,函数组件,高阶组件,纯函数组件区别

函数组件

函数组件只包含一个render的方法 使用简单,我们不需要定义继承 React.Component类。我们可以定义一个函数 这个函数作为接受props作为参数

不包含this, state,生命周期

function Square(props) { 

return ( 

{props.value} 

); 

}

纯组件PureComponent

“浅比较”的模式来检查 props 和 state 中所有的字段,以此来决定是否组件需要更新。

当 props 或者 state 某种程度是可变的话,浅比较会有遗漏

不要在props和state中改变对象和数组,如果你在你的父组件中改变对象,你的PureComponent将不会更新。虽然值已经被改变,但是子组件比较的是之前props的引用是否相同,所以不会检测到不同,所以要强制返回一个新的对象。

数据结构太复杂就会出现性能问题

高阶组件HOC

高阶组件仅仅只是是一个接受组件组作输入并返回组件的函数。

17.React为什么要有合成事件

React 采用的是顶层事件代理机制,能够保证冒泡一致性,可以跨浏览器执行。 React 提供的合成事件用来抹平不同浏览器事件对象之间的差异,将不同平台事件模拟合成事件。 事件对象可能会被频繁创建和回收,因此React 引入事件池,在事件池中获取或释放事件对象。

18.React大的业务组件怎么拆分

slot 高阶组件 组件拆分

19.函数组件和类组件的区别

1)状态同步问题,函数组件会捕获当前渲染时所用的值

首先我们知道,不论是函数式组件还是类组件,只要状态或者props发生变化了那就会重新渲染,而且对于没有进行过性能优化的子组件来说,只要父组件重新渲染了,子组件就会重新渲染。而且在react中props是不可变的,而this是一直在改变的。所以类组件中的方法可以获取到最新的实例即this,而函数组件在渲染的时候因为闭包的原因捕获了渲染时的值,所以改例子会出现这种现象。

2)函数组件useEffect与类组件生命周期

针对依赖数组的维护,我们在下面性能优化中还会继续提到。由上可见,当组件,业务逻辑很复杂的时候,响应式的useEffect是很麻烦去管理的。而类组件会减少我们在管理上的压力。

3)性能优化

类组件shouldComponentUpdate这个生命周期,通常我们在这个生命周期中进行组件的优化,通过判断前一个props和当前的props是否有变化来判断组件是否需要渲染,或者通过PureComponent实现;

那么在函数组件中我们通过React.memo()来实现,具体看下面这个例子,React.memo(),点击增加count按钮,观察console,发现只打印了“NotUseMemoComponent ”,这就说明当父组件传递给子组件的值没有发生改变的情况下,使用了memo包裹的子组件不会因为父组件重新渲染而重新渲染,而没有使用memo包裹的组件只要父组件渲染了,子组件也会渲染。

但是当父组件将自己定义的引用类型的值传递给子组件时,即使值没有改变。但是由于每次渲染的时候都会生成新的变量,导致引用发生了改变,所以子组件仍然会渲染,具体看这个例子,传递函数对象或者数组,由打印可知,每次父组件重渲染都会生成新的sayHi函数,这就使得子组件重渲染并且由于useEffect依赖了这个函数,useEffect也重新执行。这就会导致子组件做了很多无用的渲染。

针对上面这个现象,通常考虑使用useCallback,useMemo来实现优化,看下面这个例子,useCallback,useMemo,现在我们发现即使我们不停的点击按钮,也不会重新触发子组件的渲染,并且useEffect也不会执行。这是因为useCallback,useMemo在依赖数组没变的情况下,都读取了缓存,没有重新生成函数或者对象。

注意,用useState定义的状态和改变状态的方法如果成为了依赖,不会因为重渲染而导致回调函数被重新执行,因此不需要用useCallback或useMemo包裹。

4)代码复用

函数式组件自定义hook的方式使用的代码量更少,而且相比HOC更加直观,代码可读性更高也更易于理解。而且通过观察HOC的代码,一个HOC相当于对原来的组件做了一层代理,那么就避免不了‘嵌套地狱’的出现。

20.hash和history两种模式的区别

hash模式和history模式的不同

最直观的区别就是在url中 hash 带了一个很丑的 # 而history是没有#的

对于vue这类渐进式前端开发框架,为了构建 SPA(单页面应用),需要引入前端路由系统,这也就是 Vue-Router 存在的意义。前端路由的核心,就在于 —— 改变视图的同时不会向后端发出请求。

为了达到这一目的,浏览器当前提供了以下两种支持

hash —— 即地址栏 URL 中的 # 符号(此 hash 不是密码学里的散列运算)。比如这个 URL:www.abc.com/#/hello hash 的值为 #/hello。它的特点在于:hash 虽然出现在 URL 中,但不会被包括在 HTTP 请求中,对后端完全没有影响,因此改变 hash 不会重新加载页面。

history —— 利用了 HTML5 History Interface 中新增的 pushState() 和 replaceState() 方法。(需要特定浏览器支持)这两个方法应用于浏览器的历史记录栈,在当前已有的 back、forward、go 的基础之上,它们提供了对历史记录进行修改的功能。只是当它们执行修改时,虽然改变了当前的 URL,但浏览器不会立即向后端发送请求。

因此可以说,hash 模式和 history 模式都属于浏览器自身的特性,Vue-Router 只是利用了这两个特性(通过调用浏览器提供的接口)来实现前端路由.

21.ReactSSR

node server 接收客户端请求,得到当前的req url path,然后在已有的路由表内查找到对应的组件,拿到需要请求的数据,将数据作为 props

、context或者store 形式传入组件,然后基于 react 内置的服务端渲染api renderToString() or renderToNodeStream() 把组件渲染为 html字符串或者 stream 流, 在把最终的 html 进行输出前需要将数据注入到浏览器端(注水),server 输出(response)后浏览器端可以得到数据(脱水),浏览器开始进行渲染和节点对比,然后执行组件的componentDidMount 完成组件内事件绑定和一些交互,浏览器重用了服务端输出的 html 节点,整个流程结束。

22.Render Props

Class DataProvider extends React.Components {
    state = {
        name: "Alice"
    }   
    render() {
        return (
            <div>
                <p>共享数据组件自己内部的 render</p>
                { this.props.render(this.state) }
            <div/>
        )
    }
}

<DataProvider render={
data => (<p>共享的 render {data.name}</>)
} />

相较于 hooks 和 hoc,render props 使用的场景较少,它具有以下优缺点:

  • 优点:
    • 数据共享
    • 逻辑复用
  • 缺点:
    • 无法在 return 语句外访问数据
    • 嵌套

23.HOC

function draggable(Component) {
  return class NewComponent extends React.Component {
    // 增加拖拽相关的功能
    render() {
      //render 和其他生命周期函数可以干各种逻辑,甚至把原有的组件再包一层
      return <Component />
    }
  }
}
  • 优点
    • 逻辑复用
    • 不影响被包裹的组件的内部逻辑
  • 缺点
    • 高阶组件传递给被包裹组件的 props 如果重名的话,会发生覆盖

23.手写一个diff

24.React Router原理

H5 提供了一个好用的 history API,使用 window.history.pushState() 使得我们即可以修改 url 也可以不刷新页面,一举两得。

现在只需要修改点击回调里的 window.location.pathname = 'xxx' 就可以了,用 window.history.pushState() 去代替。

window.onhashchange

当 一个窗口的 hash (URL 中 # 后面的部分)改变时就会触发 hashchange 事件(参见 location.hash)。

History.replaceState()

replaceState()方法使用state objectstitle,和 URL 作为参数, 修改当前历史记录实体,如果你想更新当前的state对象或者当前历史实体的URL来响应用户的的动作的话这个方法将会非常有用。

popstate

当活动历史记录条目更改时,将触发popstate事件。如果被激活的历史记录条目是通过对history.pushState()的调用创建的,或者受到对history.replaceState()的调用的影响,popstate事件的state属性包含历史条目的状态对象的副本。

需要注意的是调用history.pushState()history.replaceState()不会触发popstate事件。只有在做出浏览器动作时,才会触发该事件,如用户点击浏览器的回退按钮(或者在Javascript代码中调用history.back()或者history.forward()方法)

25.diff算法为什么不是BFS

  • 在深度优先遍历的时候,不同状态之间的切换很容易 ,可以再看一下上面有很多箭头的那张图,每两个状态之间的差别只有 1 处,因此回退非常方便,这样全局才能使用一份状态变量完成搜索;

  • 如果使用广度优先遍历,从浅层转到深层,状态的变化就很大,此时我们不得不在每一个状态都新建变量去保存它,从性能来说是不划算的;

  • 如果使用广度优先遍历就得使用队列,然后编写结点类。队列中需要存储每一步的状态信息,需要存储的数据很大,真正能用到的很少 。

  • 使用深度优先遍历,直接使用了系统栈,系统栈帮助我们保存了每一个结点的状态信息。我们不用编写结点类,不必手动编写栈完成深度优先遍历。

如果你发现我的解答有什么问题,欢迎评论和私信我。


企业微信

p9-juejin.byteimg.com/tos-cn-i-k3…