React高频面试题梳理,看看面试怎么答?(上)

前段时间准备面试,总结了很多,下面是我在准备React面试时,结合自己的实际面试经历,以及我以前源码分析的文章,总结出来的一些React高频面试题目。

以前我写的源码分析的文章,并没有很多人看,因为大部分情况下你不需要深入源码也能懂得其中原理,并解决实际问题,这也是我总结这些面试题的原因,让你在更短的时间内获得更大的收益。

由于是以面试题的角度来讨论,所以某些点可能不能非常深入,我在问题下面都贴了相关链接,如果想深入理解,请点击这些文章。

由于题目较多,分为上、下两篇,本篇文章我们先来讨论如下19个题目:

React生命周期有哪些,16版本生命周期发生了哪些变化?

setState是同步的还是异步的?

为什么有时连续多次setState只有一次生效?

React如何实现自己的事件机制?

为何React事件要自己绑定this?

原生事件和React事件的区别?

React的合成事件是什么?

React和原生事件的执行顺序是什么?可以混用吗?

虚拟Dom是什么?

虚拟Dom比普通Dom更快吗?

虚拟Dom中的$$typeof属性的作用是什么?

React组件的渲染流程是什么?

为什么代码中一定要引入React?

为什么React组件首字母必须大写?

React在渲染真实Dom时做了哪些性能优化?

什么是高阶组件?如何实现?

HOC在业务场景中有哪些实际应用场景?

高阶组件(HOC)和Mixin的异同点是什么?

Hook有哪些优势?

React生命周期有哪些,16版本生命周期发生了哪些变化?

15生命周期

初始化阶段

constructor构造函数

getDefaultPropsprops默认值

getInitialStatestate默认值

挂载阶段

componentWillMount组件初始化渲染前调用

render组件渲染

componentDidMount组件挂载到DOM后调用

更新阶段

componentWillReceiveProps组件将要接收新props前调用

shouldComponentUpdate组件是否需要更新

componentWillUpdate组件更新前调用

render组件渲染

componentDidUpdate组件更新后调用

卸载阶段

componentWillUnmount组件卸载前调用

16生命周期

初始化阶段

constructor构造函数

getDefaultPropsprops默认值

getInitialStatestate默认值

挂载阶段

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)

setState是同步的还是异步的?

生命周期和合成事件中

在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如何实现自己的事件机制?

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事件要自己绑定this?

在上面提到的事件处理流程中,React在document上进行统一的事件分发,dispatchEvent通过循环调用所有层级的事件来模拟事件冒泡和捕获。

在React源码中,当具体到某一事件处理函数将要调用时,将调用invokeGuardedCallback方法。

function invokeGuardedCallback(name, func, a) {

  try {

    func(a);

  } catch (x) {

    if (caughtError === null) {

      caughtError = x;

    }

  }

}

可见,事件处理函数是直接调用的,并没有指定调用的组件,所以不进行手动绑定的情况下直接获取到的this是不准确的,所以我们需要手动将当前组件绑定到this上。

原生事件和React事件的区别?

React事件使用驼峰命名,而不是全部小写。

通过JSX, 你传递一个函数作为事件处理程序,而不是一个字符串。

在React中你不能通过返回false来阻止默认行为。必须明确调用preventDefault。

React的合成事件是什么?

React根据W3C规范定义了每个事件处理函数的参数,即合成事件。

事件处理程序将传递SyntheticEvent的实例,这是一个跨浏览器原生事件包装器。它具有与浏览器原生事件相同的接口,包括stopPropagation()和preventDefault(),在所有浏览器中他们工作方式都相同。

React合成的SyntheticEvent采用了事件池,这样做可以大大节省内存,而不会频繁的创建和销毁事件对象。

另外,不管在什么浏览器环境下,浏览器会将该事件类型统一创建为合成事件,从而达到了浏览器兼容的目的。

React和原生事件的执行顺序是什么?可以混用吗?

React的所有事件都通过document进行统一分发。当真实Dom触发事件后冒泡到document后才会对React事件进行处理。

所以原生的事件会先执行,然后执行React合成事件,最后执行真正在document上挂载的事件

React事件和原生事件最好不要混用。原生事件中如果执行了stopPropagation方法,则会导致其他React事件失效。因为所有元素的事件将无法冒泡到document上,导致所有的React事件都将无法被触发。。

虚拟Dom是什么?

在原生的JavaScript程序中,我们直接对DOM进行创建和更改,而DOM元素通过我们监听的事件和我们的应用程序进行通讯。

而React会先将你的代码转换成一个JavaScript对象,然后这个JavaScript对象再转换成真实DOM。这个JavaScript对象就是所谓的虚拟DOM。

当我们需要创建或更新元素时,React首先会让这个VitrualDom对象进行创建和更改,然后再将VitrualDom对象渲染成真实DOM。

当我们需要对DOM进行事件监听时,首先对VitrualDom进行事件监听,VitrualDom会代理原生的DOM事件从而做出响应。

推荐阅读:【React深入】深入分析虚拟DOM的渲染过程和特性

虚拟Dom比普通Dom更快吗?

很多文章说VitrualDom可以提升性能,这一说法实际上是很片面的。

直接操作DOM是非常耗费性能的,这一点毋庸置疑。但是React使用VitrualDom也是无法避免操作DOM的。

如果是首次渲染,VitrualDom不具有任何优势,甚至它要进行更多的计算,消耗更多的内存。

VitrualDom的优势在于React的Diff算法和批处理策略,React在页面更新之前,提前计算好了如何进行更新和渲染DOM。实际上,这个计算过程我们在直接操作DOM时,也是可以自己判断和实现的,但是一定会耗费非常多的精力和时间,而且往往我们自己做的是不如React好的。所以,在这个过程中React帮助我们"提升了性能"。

所以,我更倾向于说,VitrualDom帮助我们提高了开发效率,在重复渲染时它帮助我们计算如何更高效的更新,而不是它比DOM操作更快。

虚拟Dom中的$$typeof属性的作用是什么?

ReactElement中有一个$$typeof属性,它被赋值为REACT_ELEMENT_TYPE:

var REACT_ELEMENT_TYPE =

  (typeof Symbol === 'function' && Symbol.for && Symbol.for('react.element')) ||

  0xeac7;

可见,$$typeof是一个Symbol类型的变量,这个变量可以防止XSS。

如果你的服务器有一个漏洞,允许用户存储任意JSON对象, 而客户端代码需要一个字符串,这可能会成为一个问题:

// JSON

let expectedTextButGotJSON = {

  type: 'div',

  props: {

    dangerouslySetInnerHTML: {

      __html: '/* put your exploit here */'

    },

  },

};

let message = { text: expectedTextButGotJSON };

  {message.text}

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组件的渲染流程是什么?

使用React.createElement或JSX编写React组件,实际上所有的JSX代码最后都会转换成React.createElement(...),Babel帮助我们完成了这个转换的过程。

createElement函数对key和ref等特殊的props进行处理,并获取defaultProps对默认props进行赋值,并且对传入的孩子节点进行处理,最终构造成一个ReactElement对象(所谓的虚拟DOM)。

ReactDOM.render将生成好的虚拟DOM渲染到指定容器上,其中采用了批处理、事务等机制并且对特定浏览器进行了性能优化,最终转换为真实DOM。

为什么代码中一定要引入React?

JSX只是为React.createElement(component,props,...children)方法提供的语法糖。

所有的JSX代码最后都会转换成React.createElement(...),Babel帮助我们完成了这个转换的过程。

所以使用了JSX的代码都必须引入React。

为什么React组件首字母必须大写?

babel在编译时会判断JSX中组件的首字母,当首字母为小写时,其被认定为原生DOM标签,createElement的第一个变量被编译为字符串;当首字母为大写时,其被认定为自定义组件,createElement的第一个变量被编译为对象;

React在渲染真实Dom时做了哪些性能优化?

在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 ;

    }

  }

}

上面的代码就是一个HOC的简单应用,函数接收一个组件作为参数,并返回一个新组件,新组建可以接收一个visible props,根据visible的值来判断是否渲染Visible。

我们可以通过以下两种方式实现高阶组件:

属性代理

函数返回一个我们自己定义的组件,然后在render中返回要包裹的组件,这样我们就可以代理所有传入的props,并且决定如何渲染,实际上 ,这种方式生成的高阶组件就是原组件的父组件,上面的函数visible就是一个HOC属性代理的实现方式。

function proxyHOC(WrappedComponent) {

  return class extends Component {

    render() {

      return ;

    }

  }

}

对比原生组件增强的项:

可操作所有传入的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在业务场景中有哪些实际应用场景?

HOC可以实现的功能:

组合渲染

条件渲染

操作props

获取refs

状态管理

操作state

渲染劫持

HOC在业务中的实际应用场景:

日志打点

权限控制

双向绑定

表单校验

具体实现请参考我这篇文章:https://juejin.im/post/5cad39b3f265da03502b1c0a

高阶组件(HOC)和Mixin的异同点是什么?

Mixin和HOC都可以用来解决React的代码复用问题。

图片来源于网络

Mixin可能会相互依赖,相互耦合,不利于代码维护

不同的Mixin中的方法可能会相互冲突

Mixin非常多时,组件是可以感知到的,甚至还要为其做相关处理,这样会给代码造成滚雪球式的复杂性

而HOC的出现可以解决这些问题:

高阶组件就是一个没有副作用的纯函数,各个高阶组件不会互相依赖耦合

高阶组件也有可能造成冲突,但我们可以在遵守约定的情况下避免这些行为

高阶组件并不关心数据使用的方式和原因,而被包裹的组件也不关心数据来自何处。高阶组件的增加不会为原组件增加负担

Hook有哪些优势?

减少状态逻辑复用的风险

Hook和Mixin在用法上有一定的相似之处,但是Mixin引入的逻辑和状态是可以相互覆盖的,而多个Hook之间互不影响,这让我们不需要在把一部分精力放在防止避免逻辑复用的冲突上。在不遵守约定的情况下使用HOC也有可能带来一定冲突,比如props覆盖等等,使用Hook则可以避免这些问题。

避免地狱式嵌套

大量使用HOC的情况下让我们的代码变得嵌套层级非常深,使用HOC,我们可以实现扁平式的状态逻辑复用,而避免了大量的组件嵌套。

让组件更容易理解

在使用class组件构建我们的程序时,他们各自拥有自己的状态,业务逻辑的复杂使这些组件变得越来越庞大,各个生命周期中会调用越来越多的逻辑,越来越难以维护。使用Hook,可以让你更大限度的将公用逻辑抽离,将一个组件分割成更小的函数,而不是强制基于生命周期方法进行分割。

使用函数代替class

相比函数,编写一个class可能需要掌握更多的知识,需要注意的点也越多,比如this指向、绑定事件等等。另外,计算机理解一个class比理解一个函数更快。Hooks让你可以在classes之外使用更多React的新特性。

你可能感兴趣的:(React高频面试题梳理,看看面试怎么答?(上))