写点什么

阿里前端二面高频 react 面试题

作者:beifeng1996
  • 2022-10-24
    浙江
  • 本文字数:9360 字

    阅读完需:约 31 分钟

当调用setState时,React render 是如何工作的?

咱们可以将"render"分为两个步骤:


  1. 虚拟 DOM 渲染:当render方法被调用时,它返回一个新的组件的虚拟 DOM 结构。当调用setState()时,render会被再次调用,因为默认情况下shouldComponentUpdate总是返回true,所以默认情况下 React 是没有优化的。

  2. 原生 DOM 渲染:React 只会在虚拟 DOM 中修改真实 DOM 节点,而且修改的次数非常少——这是很棒的 React 特性,它优化了真实 DOM 的变化,使 React 变得更快。

什么是 Props

Props 是 React 中属性的简写。它们是只读组件,必须保持纯,即不可变。它们总是在整个应用中从父组件传递到子组件。子组件永远不能将 prop 送回父组件。这有助于维护单向数据流,通常用于呈现动态生成的数据。

React.Component 和 React.PureComponent 的区别

PureComponent 表示一个纯组件,可以用来优化 React 程序,减少 render 函数执行的次数,从而提高组件的性能。


在 React 中,当 prop 或者 state 发生变化时,可以通过在 shouldComponentUpdate 生命周期函数中执行 return false 来阻止页面的更新,从而减少不必要的 render 执行。React.PureComponent 会自动执行 shouldComponentUpdate。


不过,pureComponent 中的 shouldComponentUpdate() 进行的是浅比较,也就是说如果是引用数据类型的数据,只会比较不是同一个地址,而不会比较这个地址里面的数据是否一致。浅比较会忽略属性和或状态突变情况,其实也就是数据引用指针没有变化,而数据发生改变的时候 render 是不会执行的。如果需要重新渲染那么就需要重新开辟空间引用数据。PureComponent 一般会用在一些纯展示组件上。


使用 pureComponent 的好处:当组件更新时,如果组件的 props 或者 state 都没有改变,render 函数就不会触发。省去虚拟 DOM 的生成和对比过程,达到提升性能的目的。这是因为 react 自动做了一层浅比较。

React 组件的构造函数有什么作用?它是必须的吗?

构造函数主要用于两个目的:


  • 通过将对象分配给 this.state 来初始化本地状态

  • 将事件处理程序方法绑定到实例上


所以,当在 React class 中需要设置 state 的初始值或者绑定事件时,需要加上构造函数,官方 Demo:


class LikeButton extends React.Component {  constructor() {    super();    this.state = {      liked: false    };    this.handleClick = this.handleClick.bind(this);  }  handleClick() {    this.setState({liked: !this.state.liked});  }  render() {    const text = this.state.liked ? 'liked' : 'haven\'t liked';    return (      <div onClick={this.handleClick}>        You {text} this. Click to toggle.      </div>    );  }}ReactDOM.render(  <LikeButton />,  document.getElementById('example'));
复制代码


构造函数用来新建父类的 this 对象;子类必须在 constructor 方法中调用 super 方法;否则新建实例时会报错;因为子类没有自己的 this 对象,而是继承父类的 this 对象,然后对其进行加工。如果不调用 super 方法;子类就得不到 this 对象。


注意:


  • constructor () 必须配上 super(), 如果要在 constructor 内部使用 this.props 就要 传入 props , 否则不用

  • JavaScript 中的 bind 每次都会返回一个新的函数, 为了性能等考虑, 尽量在 constructor 中绑定事件

React 组件的 state 和 props 有什么区别?

(1)props


props 是一个从外部传进组件的参数,主要作为就是从父组件向子组件传递数据,它具有可读性和不变性,只能通过外部组件主动传入新的 props 来重新渲染子组件,否则子组件的 props 以及展现形式不会改变。


(2)state


state 的主要作用是用于组件保存、控制以及修改自己的状态,它只能在 constructor 中初始化,它算是组件的私有属性,不可通过外部访问和修改,只能通过组件内部的 this.setState 来修改,修改 state 属性会导致组件的重新渲染。


(3)区别


  • props 是传递给组件的(类似于函数的形参),而 state 是在组件内被组件自己管理的(类似于在一个函数内声明的变量)。

  • props 是不可修改的,所有 React 组件都必须像纯函数一样保护它们的 props 不被更改。

  • state 是在组件中创建的,一般在 constructor 中初始化 state。state 是多变的、可以修改,每次 setState 都异步更新的。

useEffect 与 useLayoutEffect 的区别

(1)共同点


  • 运用效果: useEffect 与 useLayoutEffect 两者都是用于处理副作用,这些副作用包括改变 DOM、设置订阅、操作定时器等。在函数组件内部操作副作用是不被允许的,所以需要使用这两个函数去处理。

  • 使用方式: useEffect 与 useLayoutEffect 两者底层的函数签名是完全一致的,都是调用的 mountEffectImpl 方法,在使用上也没什么差异,基本可以直接替换。


(2)不同点


  • 使用场景: useEffect 在 React 的渲染过程中是被异步调用的,用于绝大多数场景;而 useLayoutEffect 会在所有的 DOM 变更之后同步调用,主要用于处理 DOM 操作、调整样式、避免页面闪烁等问题。也正因为是同步处理,所以需要避免在 useLayoutEffect 做计算量较大的耗时任务从而造成阻塞。

  • 使用效果: useEffect 是按照顺序执行代码的,改变屏幕像素之后执行(先渲染,后改变 DOM),当改变屏幕内容时可能会产生闪烁;useLayoutEffect 是改变屏幕像素之前就执行了(会推迟页面显示的事件,先改变 DOM 后渲染),不会产生闪烁。useLayoutEffect 总是比 useEffect 先执行。


在未来的趋势上,两个 API 是会长期共存的,暂时没有删减合并的计划,需要开发者根据场景去自行选择。React 团队的建议非常实用,如果实在分不清,先用 useEffect,一般问题不大;如果页面有异常,再直接替换为 useLayoutEffect 即可。

对 React 的插槽(Portals)的理解,如何使用,有哪些使用场景

React 官方对 Portals 的定义:


Portal 提供了一种将子节点渲染到存在于父组件以外的 DOM 节点的优秀的方案


Portals 是 React 16 提供的官方解决方案,使得组件可以脱离父组件层级挂载在 DOM 树的任何位置。通俗来讲,就是我们 render 一个组件,但这个组件的 DOM 结构并不在本组件内。


Portals 语法如下:


ReactDOM.createPortal(child, container);
复制代码


  • 第一个参数 child 是可渲染的 React 子项,比如元素,字符串或者片段等;

  • 第二个参数 container 是一个 DOM 元素。


一般情况下,组件的 render 函数返回的元素会被挂载在它的父级组件上:


import DemoComponent from './DemoComponent';render() {  // DemoComponent元素会被挂载在id为parent的div的元素上  return (    <div id="parent">        <DemoComponent />    </div>  );}
复制代码


然而,有些元素需要被挂载在更高层级的位置。最典型的应用场景:当父组件具有overflow: hidden或者z-index的样式设置时,组件有可能被其他元素遮挡,这时就可以考虑要不要使用 Portal 使组件的挂载脱离父组件。例如:对话框,模态窗。


import DemoComponent from './DemoComponent';render() {  // DemoComponent元素会被挂载在id为parent的div的元素上  return (    <div id="parent">        <DemoComponent />    </div>  );}
复制代码

为什么 React 要用 JSX?

JSX 是一个 JavaScript 的语法扩展,或者说是一个类似于 XML 的 ECMAScript 语法扩展。它本身没有太多的语法定义,也不期望引入更多的标准。


其实 React 本身并不强制使用 JSX。在没有 JSX 的时候,React 实现一个组件依赖于使用 React.createElement 函数。代码如下:


class Hello extends React.Component {  render() {    return React.createElement(        'div',        null,         `Hello ${this.props.toWhat}`      );  }}ReactDOM.render(  React.createElement(Hello, {toWhat: 'World'}, null),  document.getElementById('root'));
复制代码


而 JSX 更像是一种语法糖,通过类似 XML 的描述方式,描写函数对象。在采用 JSX 之后,这段代码会这样写:


class Hello extends React.Component {  render() {    return <div>Hello {this.props.toWhat}</div>;  }}ReactDOM.render(  <Hello toWhat="World" />,  document.getElementById('root'));
复制代码


通过对比,可以清晰地发现,代码变得更为简洁,而且代码结构层次更为清晰。


因为 React 需要将组件转化为虚拟 DOM 树,所以在编写代码时,实际上是在手写一棵结构树。而 XML 在树结构的描述上天生具有可读性强的优势。


但这样可读性强的代码仅仅是给写程序的同学看的,实际上在运行的时候,会使用 Babel 插件将 JSX 语法的代码还原为 React.createElement 的代码。


总结: JSX 是一个 JavaScript 的语法扩展,结构类似 XML。JSX 主要用于声明 React 元素,但 React 中并不强制使用 JSX。即使使用了 JSX,也会在构建过程中,通过 Babel 插件编译为 React.createElement。所以 JSX 更像是 React.createElement 的一种语法糖。


React 团队并不想引入 JavaScript 本身以外的开发体系。而是希望通过合理的关注点分离保持组件开发的纯粹性。

对 React-Fiber 的理解,它解决了什么问题?

React V15 在渲染时,会递归比对 VirtualDOM 树,找出需要变动的节点,然后同步更新它们, 一气呵成。这个过程期间, React 会占据浏览器资源,这会导致用户触发的事件得不到响应,并且会导致掉帧,导致用户感觉到卡顿


为了给用户制造一种应用很快的“假象”,不能让一个任务长期霸占着资源。 可以将浏览器的渲染、布局、绘制、资源加载(例如 HTML 解析)、事件响应、脚本执行视作操作系统的“进程”,需要通过某些调度策略合理地分配 CPU 资源,从而提高浏览器的用户响应速率, 同时兼顾任务执行效率。


所以 React 通过 Fiber 架构,让这个执行过程变成可被中断。“适时”地让出 CPU 执行权,除了可以让浏览器及时地响应用户的交互,还有其他好处:


  • 分批延时对 DOM 进行操作,避免一次性操作大量 DOM 节点,可以得到更好的用户体验;

  • 给浏览器一点喘息的机会,它会对代码进行编译优化(JIT)及进行热代码优化,或者对 reflow 进行修正。


核心思想: Fiber 也称协程或者纤程。它和线程并不一样,协程本身是没有并发或者并行能力的(需要配合线程),它只是一种控制流程的让出机制。让出 CPU 的执行权,让 CPU 能在这段时间执行其他的操作。渲染的过程可以被中断,可以将控制权交回浏览器,让位给高优先级的任务,浏览器空闲后再恢复渲染。


参考:前端react面试题详细解答

hooks 和 class 比较的优势?

一、更容易复用代码


二、清爽的代码风格+代码量更少


缺点


状态不同步


不好用的 useEffect,

React 中 refs 的作用是什么?有哪些应用场景?

Refs 提供了一种方式,用于访问在 render 方法中创建的 React 元素或 DOM 节点。Refs 应该谨慎使用,如下场景使用 Refs 比较适合:


  • 处理焦点、文本选择或者媒体的控制

  • 触发必要的动画

  • 集成第三方 DOM 库


Refs 是使用 React.createRef() 方法创建的,他通过 ref 属性附加到 React 元素上。要在整个组件中使用 Refs,需要将 ref 在构造函数中分配给其实例属性:


class MyComponent extends React.Component {  constructor(props) {    super(props)    this.myRef = React.createRef()  }  render() {    return <div ref={this.myRef} />  }}
复制代码


由于函数组件没有实例,因此不能在函数组件上直接使用 ref


function MyFunctionalComponent() {  return <input />;}class Parent extends React.Component {  constructor(props) {    super(props);    this.textInput = React.createRef();  }  render() {    // 这将不会工作!    return (      <MyFunctionalComponent ref={this.textInput} />    );  }}
复制代码


但可以通过闭合的帮助在函数组件内部进行使用 Refs:


function CustomTextInput(props) {  // 这里必须声明 textInput,这样 ref 回调才可以引用它  let textInput = null;  function handleClick() {    textInput.focus();  }  return (    <div>      <input        type="text"        ref={(input) => { textInput = input; }} />      <input        type="button"        value="Focus the text input"        onClick={handleClick}      />    </div>  );  }
复制代码


注意:


  • 不应该过度的使用 Refs

  • ref 的返回值取决于节点的类型:

  • ref 属性被用于一个普通的 HTML 元素时,React.createRef() 将接收底层 DOM 元素作为他的 current 属性以创建 ref

  • ref 属性被用于一个自定义的类组件时,ref 对象将接收该组件已挂载的实例作为他的 current

  • 当在父组件中需要访问子组件中的 ref 时可使用传递 Refs 或回调 Refs。

对 React-Intl 的理解,它的工作原理?

React-intl 是雅虎的语言国际化开源项目 FormatJS 的一部分,通过其提供的组件和 API 可以与 ReactJS 绑定。


React-intl 提供了两种使用方法,一种是引用 React 组件,另一种是直接调取 API,官方更加推荐在 React 项目中使用前者,只有在无法使用 React 组件的地方,才应该调用框架提供的 API。它提供了一系列的 React 组件,包括数字格式化、字符串格式化、日期格式化等。


在 React-intl 中,可以配置不同的语言包,他的工作原理就是根据需要,在语言包之间进行切换。

constructor 为什么不先渲染?

由 ES6 的继承规则得知,不管子类写不写 constructor,在 new 实例的过程都会给补上 constructor。


所以:constructor 钩子函数并不是不可缺少的,子组件可以在一些情况略去。比如不自己的 state,从 props 中获取的情况

React 中的高阶组件运用了什么设计模式?

使用了装饰模式,高阶组件的运用:


function withWindowWidth(BaseComponent) {  class DerivedClass extends React.Component {    state = {      windowWidth: window.innerWidth,    }    onResize = () => {      this.setState({        windowWidth: window.innerWidth,      })    }    componentDidMount() {      window.addEventListener('resize', this.onResize)    }    componentWillUnmount() {      window.removeEventListener('resize', this.onResize);    }    render() {      return <BaseComponent {...this.props} {...this.state}/>    }  }  return DerivedClass;}const MyComponent = (props) => {  return <div>Window width is: {props.windowWidth}</div>};export default withWindowWidth(MyComponent);
复制代码


装饰模式的特点是不需要改变 被装饰对象 本身,而只是在外面套一个外壳接口。JavaScript 目前已经有了原生装饰器的提案,其用法如下:


@testable   class MyTestableClass {}
复制代码

对 React 和 Vue 的理解,它们的异同

相似之处:


  • 都将注意力集中保持在核心库,而将其他功能如路由和全局状态管理交给相关的库

  • 都有自己的构建工具,能让你得到一个根据最佳实践设置的项目模板。

  • 都使用了 Virtual DOM(虚拟 DOM)提高重绘性能

  • 都有 props 的概念,允许组件间的数据传递

  • 都鼓励组件化应用,将应用分拆成一个个功能明确的模块,提高复用性


不同之处:


1)数据流


Vue 默认支持数据双向绑定,而 React 一直提倡单向数据流


2)虚拟 DOM


Vue2.x 开始引入"Virtual DOM",消除了和 React 在这方面的差异,但是在具体的细节还是有各自的特点。


  • Vue 宣称可以更快地计算出 Virtual DOM 的差异,这是由于它在渲染过程中,会跟踪每一个组件的依赖关系,不需要重新渲染整个组件树。

  • 对于 React 而言,每当应用的状态被改变时,全部子组件都会重新渲染。当然,这可以通过 PureComponent/shouldComponentUpdate 这个生命周期方法来进行控制,但 Vue 将此视为默认的优化。


3)组件化


React 与 Vue 最大的不同是模板的编写。


  • Vue 鼓励写近似常规 HTML 的模板。写起来很接近标准 HTML 元素,只是多了一些属性。

  • React 推荐你所有的模板通用 JavaScript 的语法扩展——JSX 书写。


具体来讲:React 中 render 函数是支持闭包特性的,所以我们 import 的组件在 render 中可以直接调用。但是在 Vue 中,由于模板中使用的数据都必须挂在 this 上进行一次中转,所以 import 完组件之后,还需要在 components 中再声明下。


4)监听数据变化的实现原理不同


  • Vue 通过 getter/setter 以及一些函数的劫持,能精确知道数据变化,不需要特别的优化就能达到很好的性能

  • React 默认是通过比较引用的方式进行的,如果不优化(PureComponent/shouldComponentUpdate)可能导致大量不必要的 vDOM 的重新渲染。这是因为 Vue 使用的是可变数据,而 React 更强调数据的不可变。


5)高阶组件


react 可以通过高阶组件(Higher Order Components-- HOC)来扩展,而 vue 需要通过 mixins 来扩展。


原因高阶组件就是高阶函数,而 React 的组件本身就是纯粹的函数,所以高阶函数对 React 来说易如反掌。相反 Vue.js 使用 HTML 模板创建视图组件,这时模板无法有效的编译,因此 Vue 不采用 HOC 来实现。


6)构建工具


两者都有自己的构建工具


  • React ==> Create React APP

  • Vue ==> vue-cli


7)跨平台


  • React ==> React Native

  • Vue ==> Weex

React 中 props.children 和 React.Children 的区别

在 React 中,当涉及组件嵌套,在父组件中使用props.children把所有子组件显示出来。如下:


function ParentComponent(props){    return (        <div>            {props.children}        </div>    )}
复制代码


如果想把父组件中的属性传给所有的子组件,需要使用React.Children方法。


比如,把几个 Radio 组合起来,合成一个 RadioGroup,这就要求所有的 Radio 具有同样的 name 属性值。可以这样:把 Radio 看做子组件,RadioGroup 看做父组件,name 的属性值在 RadioGroup 这个父组件中设置。


首先是子组件:


//子组件function RadioOption(props) {  return (    <label>      <input type="radio" value={props.value} name={props.name} />      {props.label}    </label>  )}
复制代码


然后是父组件,不仅需要把它所有的子组件显示出来,还需要为每个子组件赋上 name 属性和值:


//父组件用,props是指父组件的propsfunction renderChildren(props) {
//遍历所有子组件 return React.Children.map(props.children, child => { if (child.type === RadioOption) return React.cloneElement(child, { //把父组件的props.name赋值给每个子组件 name: props.name }) else return child })}//父组件function RadioGroup(props) { return ( <div> {renderChildren(props)} </div> )}function App() { return ( <RadioGroup name="hello"> <RadioOption label="选项一" value="1" /> <RadioOption label="选项二" value="2" /> <RadioOption label="选项三" value="3" /> </RadioGroup> )}export default App;
复制代码


以上,React.Children.map让我们对父组件的所有子组件又更灵活的控制。

虚拟 DOM 的引入与直接操作原生 DOM 相比,哪一个效率更高,为什么

虚拟 DOM 相对原生的 DOM 不一定是效率更高,如果只修改一个按钮的文案,那么虚拟 DOM 的操作无论如何都不可能比真实的 DOM 操作更快。在首次渲染大量 DOM 时,由于多了一层虚拟 DOM 的计算,虚拟 DOM 也会比 innerHTML 插入慢。它能保证性能下限,在真实 DOM 操作的时候进行针对性的优化时,还是更快的。所以要根据具体的场景进行探讨。


在整个 DOM 操作的演化过程中,其实主要矛盾并不在于性能,而在于开发者写得爽不爽,在于研发体验/研发效率。虚拟 DOM 不是别的,正是前端开发们为了追求更好的研发体验和研发效率而创造出来的高阶产物。虚拟 DOM 并不一定会带来更好的性能,React 官方也从来没有把虚拟 DOM 作为性能层面的卖点对外输出过。**虚拟 DOM 的优越之处在于,它能够在提供更爽、更高效的研发模式(也就是函数式的 UI 编程方式)的同时,仍然保持一个还不错的性能。

在 React 中遍历的方法有哪些?

(1)遍历数组:map && forEach


import React from 'react';
class App extends React.Component { render() { let arr = ['a', 'b', 'c', 'd']; return ( <ul> { arr.map((item, index) => { return <li key={index}>{item}</li> }) } </ul> ) }}
class App extends React.Component { render() { let arr = ['a', 'b', 'c', 'd']; return ( <ul> { arr.forEach((item, index) => { return <li key={index}>{item}</li> }) } </ul> ) }}

复制代码


(2)遍历对象:map && for in


class App extends React.Component {  render() {    let obj = {      a: 1,      b: 2,      c: 3    }    return (      <ul>        {          (() => {            let domArr = [];            for(const key in obj) {              if(obj.hasOwnProperty(key)) {                const value = obj[key]                domArr.push(<li key={key}>{value}</li>)              }            }            return domArr;          })()        }      </ul>    )  }}
// Object.entries() 把对象转换成数组class App extends React.Component { render() { let obj = { a: 1, b: 2, c: 3 } return ( <ul> { Object.entries(obj).map(([key, value], index) => { // item是一个数组,把item解构,写法是[key, value] return <li key={key}>{value}</li> }) } </ul> ) }}

复制代码

React 中 Diff 算法的原理是什么?

原理如下。(1)节点之间的比较。节点包括两种类型:一种是 React 组件,另一种是 HTML 的 DOM。如果节点类型不同,按以下方式比较。如果 HTML DOM 不同,直接使用新的替换旧的。如果组件类型不同,也直接使用新的替换旧的。如果 HTML DOM 类型相同,按以下方式比较。在 React 里样式并不是一个纯粹的字符串,而是一个对象,这样在样式发生改变时,只需要改变替换变化以后的样式。修改完当前节点之后,递归处理该节点的子节点。如果组件类型相同,按以下方式比较。如果组件类型相同,使用 React 机制处理。一般使用新的 props 替换旧的 props,并在之后调用组件的 componentWillReceiveProps 方法,之前组件的 render 方法会被调用。节点的比较机制开始递归作用于它的子节点。(2)两个列表之间的比较。一个节点列表中的一个节点发生改变, React 无法很妤地处理这个问题。循环新旧两个列表,并找出不同,这是 React 唯一的处理方法。但是,有一个办法可以把这个算法的复杂度降低。那就是在生成一个节点列表时给每个节点上添加一个 key。这个 key 只需要在这一个节点列表中唯一,不需要全局唯一。(3)取舍需要注意的是,上面的启发式算法基于两点假设。类型相近的节点总是生成同样的树,而类型不同的节点也总是生成不同的树可以为多次 render 都表现稳定的节点设置 key。上面的节点之间的比较算法基本上就是基于这两个假设而实现的。要提高 React 应用的效率,需要按照这两点假设来开发。

diff 虚拟 DOM 比较的规则

  • 【旧虚拟 DOM】 与 【新虚拟 DOM】中相同 key

  • 若虚拟 DOM 中的内容没有发生改变,直接使用旧的虚拟 DOM

  • 若虚拟 DOM 中的内容发生改变了,则生成新真实的 DOM,随后替换页面中之前的真实 DOM

  • 【旧虚拟 DOM】 中未找到 与 【新虚拟 DOM】相同的 key

  • 根据数据创建真实 DOM,随后渲染到页面


用户头像

beifeng1996

关注

还未添加个人签名 2022-09-01 加入

还未添加个人简介

评论

发布
暂无评论
阿里前端二面高频react面试题_React_beifeng1996_InfoQ写作社区