写点什么

前端一面必会 react 面试题(附答案)

作者:beifeng1996
  • 2022-12-20
    浙江
  • 本文字数:10680 字

    阅读完需:约 35 分钟

如何创建 refs

Refs 是使用 React.createRef() 创建的,并通过 ref 属性附加到 React 元素。在构造组件时,通常将 Refs 分配给实例属性,以便可以在整个组件中引用它们。


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


或者这样用:


class UserForm extends Component {  handleSubmit = () => {    console.log("Input Value is: ", this.input.value);  };  render() {    return (      <form onSubmit={this.handleSubmit}>        <input type="text" ref={(input) => (this.input = input)} /> // Access DOM input in handle submit        <button type="submit">Submit</button>      </form>    );  }}
复制代码

哪些方法会触发 React 重新渲染?重新渲染 render 会做些什么?

(1)哪些方法会触发 react 重新渲染?


  • setState()方法被调用


setState 是 React 中最常用的命令,通常情况下,执行 setState 会触发 render。但是这里有个点值得关注,执行 setState 的时候不一定会重新渲染。当 setState 传入 null 时,并不会触发 render。


class App extends React.Component {  state = {    a: 1  };
render() { console.log("render"); return ( <React.Fragement> <p>{this.state.a}</p> <button onClick={() => { this.setState({ a: 1 }); // 这里并没有改变 a 的值 }} > Click me </button> <button onClick={() => this.setState(null)}>setState null</button> <Child /> </React.Fragement> ); }}
复制代码


  • 父组件重新渲染


只要父组件重新渲染了,即使传入子组件的 props 未发生变化,那么子组件也会重新渲染,进而触发 render


(2)重新渲染 render 会做些什么?


  • 会对新旧 VNode 进行对比,也就是我们所说的 Diff 算法。

  • 对新旧两棵树进行一个深度优先遍历,这样每一个节点都会一个标记,在到深度遍历的时候,每遍历到一和个节点,就把该节点和新的节点树进行对比,如果有差异就放到一个对象里面

  • 遍历差异对象,根据差异的类型,根据对应对规则更新 VNode


React 的处理 render 的基本思维模式是每次一有变动就会去重新渲染整个应用。在 Virtual DOM 没有出现之前,最简单的方法就是直接调用 innerHTML。Virtual DOM 厉害的地方并不是说它比直接操作 DOM 快,而是说不管数据怎么变,都会尽量以最小的代价去更新 DOM。React 将 render 函数返回的虚拟 DOM 树与老的进行比较,从而确定 DOM 要不要更新、怎么更新。当 DOM 树很大时,遍历两棵树进行各种比对还是相当耗性能的,特别是在顶层 setState 一个微小的修改,默认会去遍历整棵树。尽管 React 使用高度优化的 Diff 算法,但是这个过程仍然会损耗性能.

React Hooks 和生命周期的关系?

函数组件 的本质是函数,没有 state 的概念的,因此不存在生命周期一说,仅仅是一个 render 函数而已。但是引入 Hooks 之后就变得不同了,它能让组件在不使用 class 的情况下拥有 state,所以就有了生命周期的概念,所谓的生命周期其实就是 useStateuseEffect()useLayoutEffect()


即:Hooks 组件(使用了 Hooks 的函数组件)有生命周期,而函数组件(未使用 Hooks 的函数组件)是没有生命周期的


下面是具体的 class 与 Hooks 的生命周期对应关系


  • constructor:函数组件不需要构造函数,可以通过调用 **useState 来初始化 state**。如果计算的代价比较昂贵,也可以传一个函数给 useState


const [num, UpdateNum] = useState(0)
复制代码


  • getDerivedStateFromProps:一般情况下,我们不需要使用它,可以在渲染过程中更新 state,以达到实现 getDerivedStateFromProps 的目的。


function ScrollView({row}) {  let [isScrollingDown, setIsScrollingDown] = useState(false);  let [prevRow, setPrevRow] = useState(null);  if (row !== prevRow) {    // Row 自上次渲染以来发生过改变。更新 isScrollingDown。    setIsScrollingDown(prevRow !== null && row > prevRow);    setPrevRow(row);  }  return `Scrolling down: ${isScrollingDown}`;}
复制代码


React 会立即退出第一次渲染并用更新后的 state 重新运行组件以避免耗费太多性能。


  • shouldComponentUpdate:可以用 **React.memo** 包裹一个组件来对它的 props 进行浅比较


const Button = React.memo((props) => {  // 具体的组件});
复制代码


注意:**React.memo 等效于 **``**PureComponent**,它只浅比较 props。这里也可以使用 useMemo 优化每一个节点。


  • render:这是函数组件体本身。

  • componentDidMount, componentDidUpdateuseLayoutEffect 与它们两的调用阶段是一样的。但是,我们推荐你一开始先用 useEffect,只有当它出问题的时候再尝试使用 useLayoutEffectuseEffect 可以表达所有这些的组合。


// componentDidMountuseEffect(()=>{  // 需要在 componentDidMount 执行的内容}, [])useEffect(() => {   // 在 componentDidMount,以及 count 更改时 componentDidUpdate 执行的内容  document.title = `You clicked ${count} times`;   return () => {    // 需要在 count 更改时 componentDidUpdate(先于 document.title = ... 执行,遵守先清理后更新)    // 以及 componentWillUnmount 执行的内容         } // 当函数中 Cleanup 函数会按照在代码中定义的顺序先后执行,与函数本身的特性无关}, [count]); // 仅在 count 更改时更新
复制代码


请记得 React 会等待浏览器完成画面渲染之后才会延迟调用 ,因此会使得额外操作很方便


  • componentWillUnmount:相当于 useEffect 里面返回的 cleanup 函数


// componentDidMount/componentWillUnmountuseEffect(()=>{  // 需要在 componentDidMount 执行的内容  return function cleanup() {    // 需要在 componentWillUnmount 执行的内容        }}, [])
复制代码


  • componentDidCatch and getDerivedStateFromError:目前还没有这些方法的 Hook 等价写法,但很快会加上。


对 React SSR 的理解

服务端渲染是数据与模版组成的 html,即 HTML = 数据 + 模版。将组件或页面通过服务器生成 html 字符串,再发送到浏览器,最后将静态标记"混合"为客户端上完全交互的应用程序。页面没使用服务渲染,当请求页面时,返回的 body 里为空,之后执行 js 将 html 结构注入到 body 里,结合 css 显示出来;


SSR 的优势:


  • 对 SEO 友好

  • 所有的模版、图片等资源都存在服务器端

  • 一个 html 返回所有数据

  • 减少 HTTP 请求

  • 响应快、用户体验好、首屏渲染快


1)更利于 SEO


不同爬虫工作原理类似,只会爬取源码,不会执行网站的任何脚本使用了 React 或者其它 MVVM 框架之后,页面大多数 DOM 元素都是在客户端根据 js 动态生成,可供爬虫抓取分析的内容大大减少。另外,浏览器爬虫不会等待我们的数据完成之后再去抓取页面数据。服务端渲染返回给客户端的是已经获取了异步数据并执行 JavaScript 脚本的最终 HTML,网络爬中就可以抓取到完整页面的信息。


2)更利于首屏渲染


首屏的渲染是 node 发送过来的 html 字符串,并不依赖于 js 文件了,这就会使用户更快的看到页面的内容。尤其是针对大型单页应用,打包后文件体积比较大,普通客户端渲染加载所有所需文件时间较长,首页就会有一个很长的白屏等待时间。


SSR 的局限:


1)服务端压力较大


本来是通过客户端完成渲染,现在统一到服务端 node 服务去做。尤其是高并发访问的情况,会大量占用服务端 CPU 资源;


2)开发条件受限


在服务端渲染中,只会执行到 componentDidMount 之前的生命周期钩子,因此项目引用的第三方的库也不可用其它生命周期钩子,这对引用库的选择产生了很大的限制;


3)学习成本相对较高 除了对 webpack、MVVM 框架要熟悉,还需要掌握 node、 Koa2 等相关技术。相对于客户端渲染,项目构建、部署过程更加复杂。


时间耗时比较:


1)数据请求


由服务端请求首屏数据,而不是客户端请求首屏数据,这是"快"的一个主要原因。服务端在内网进行请求,数据响应速度快。客户端在不同网络环境进行数据请求,且外网 http 请求开销大,导致时间差


  • 客户端数据请求

  • 服务端数据请求

  • 2)html 渲染 服务端渲染是先向后端服务器请求数据,然后生成完整首屏 html 返回给浏览器;而客户端渲染是等 js 代码下载、加载、解析完成后再请求数据渲染,等待的过程页面是什么都没有的,就是用户看到的白屏。就是服务端渲染不需要等待 js 代码下载完成并请求数据,就可以返回一个已有完整数据的首屏页面。

  • 非 ssr html 渲染

  • ssr html 渲染

React 高阶组件是什么,和普通组件有什么区别,适用什么场景

官方解释∶


高阶组件(HOC)是 React 中用于复用组件逻辑的一种高级技巧。HOC 自身不是 React API 的一部分,它是一种基于 React 的组合特性而形成的设计模式。


高阶组件(HOC)就是一个函数,且该函数接受一个组件作为参数,并返回一个新的组件,它只是一种组件的设计模式,这种设计模式是由 react 自身的组合性质必然产生的。我们将它们称为纯组件,因为它们可以接受任何动态提供的子组件,但它们不会修改或复制其输入组件中的任何行为。


// hoc的定义function withSubscription(WrappedComponent, selectData) {  return class extends React.Component {    constructor(props) {      super(props);      this.state = {        data: selectData(DataSource, props)      };    }    // 一些通用的逻辑处理    render() {      // ... 并使用新数据渲染被包装的组件!      return <WrappedComponent data={this.state.data} {...this.props} />;    }  };
// 使用const BlogPostWithSubscription = withSubscription(BlogPost, (DataSource, props) => DataSource.getBlogPost(props.id));
复制代码


1)HOC 的优缺点


  • 优点∶ 逻辑服用、不影响被包裹组件的内部逻辑。

  • 缺点∶hoc 传递给被包裹组件的 props 容易和被包裹后的组件重名,进而被覆盖


2)适用场景


  • 代码复用,逻辑抽象

  • 渲染劫持

  • State 抽象和更改

  • Props 更改


3)具体应用例子


  • 权限控制: 利用高阶组件的 条件渲染 特性可以对页面进行权限控制,权限控制一般分为两个维度:页面级别和 页面元素级别


// HOC.jsfunction withAdminAuth(WrappedComponent) {    return class extends React.Component {        state = {            isAdmin: false,        }        async UNSAFE_componentWillMount() {            const currentRole = await getCurrentUserRole();            this.setState({                isAdmin: currentRole === 'Admin',            });        }        render() {            if (this.state.isAdmin) {                return <WrappedComponent {...this.props} />;            } else {                return (<div>您没有权限查看该页面,请联系管理员!</div>);            }        }    };}
// pages/page-a.jsclass PageA extends React.Component { constructor(props) { super(props); // something here... } UNSAFE_componentWillMount() { // fetching data } render() { // render page with data }}export default withAdminAuth(PageA);

// pages/page-b.jsclass PageB extends React.Component { constructor(props) { super(props); // something here... } UNSAFE_componentWillMount() { // fetching data } render() { // render page with data }}export default withAdminAuth(PageB);
复制代码


  • 组件渲染性能追踪: 借助父组件子组件生命周期规则捕获子组件的生命周期,可以方便的对某个组件的渲染时间进行记录∶


class Home extends React.Component {        render() {            return (<h1>Hello World.</h1>);        }    }    function withTiming(WrappedComponent) {        return class extends WrappedComponent {            constructor(props) {                super(props);                this.start = 0;                this.end = 0;            }            UNSAFE_componentWillMount() {                super.componentWillMount && super.componentWillMount();                this.start = Date.now();            }            componentDidMount() {                super.componentDidMount && super.componentDidMount();                this.end = Date.now();                console.log(`${WrappedComponent.name} 组件渲染时间为 ${this.end - this.start} ms`);            }            render() {                return super.render();            }        };    }
export default withTiming(Home);


复制代码


注意:withTiming 是利用 反向继承 实现的一个高阶组件,功能是计算被包裹组件(这里是 Home 组件)的渲染时间。


  • 页面复用


const withFetching = fetching => WrappedComponent => {    return class extends React.Component {        state = {            data: [],        }        async UNSAFE_componentWillMount() {            const data = await fetching();            this.setState({                data,            });        }        render() {            return <WrappedComponent data={this.state.data} {...this.props} />;        }    }}
// pages/page-a.jsexport default withFetching(fetching('science-fiction'))(MovieList);// pages/page-b.jsexport default withFetching(fetching('action'))(MovieList);// pages/page-other.jsexport default withFetching(fetching('some-other-type'))(MovieList);
复制代码

何为 Children

在 JSX 表达式中,一个开始标签(比如<a>)和一个关闭标签(比如</a>)之间的内容会作为一个特殊的属性props.children被自动传递给包含着它的组件。


这个属性有许多可用的方法,包括 React.Children.mapReact.Children.forEachReact.Children.countReact.Children.onlyReact.Children.toArray


参考 前端进阶面试题详细解答

为何 React 事件要自己绑定 this

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


function invokeGuardedCallback(name, func, a) {  try {    func(a);  } catch (x) {    if (caughtError === null) {      caughtError = x;    }  }}
复制代码


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

React 的工作原理

React 会创建一个虚拟 DOM(virtual DOM)。当一个组件中的状态改变时,React 首先会通过 "diffing" 算法来标记虚拟 DOM 中的改变,第二步是调节(reconciliation),会用 diff 的结果来更新 DOM。

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 都异步更新的。

react 父子传值

父传子——在调用子组件上绑定,子组件中获取 this.props


子传父——引用子组件的时候传过去一个方法,子组件通过 this.props.methed()传过去参数


connection

React 如何判断什么时候重新渲染组件?

组件状态的改变可以因为props的改变,或者直接通过setState方法改变。组件获得新的状态,然后 React 决定是否应该重新渲染组件。只要组件的 state 发生变化,React 就会对组件进行重新渲染。这是因为 React 中的shouldComponentUpdate方法默认返回true,这就是导致每次更新都重新渲染的原因。


当 React 将要渲染组件时会执行shouldComponentUpdate方法来看它是否返回true(组件应该更新,也就是重新渲染)。所以需要重写shouldComponentUpdate方法让它根据情况返回true或者false来告诉 React 什么时候重新渲染什么时候跳过重新渲染。

高阶组件

高阶函数:如果一个函数接受一个或多个函数作为参数或者返回一个函数就可称之为高阶函数


高阶组件:如果一个函数 接受一个或多个组件作为参数并且返回一个组件 就可称之为 高阶组件


react 中的高阶组件


React 中的高阶组件主要有两种形式:属性代理反向继承


属性代理 Proxy


  • 操作 props

  • 抽离 state

  • 通过 ref 访问到组件实例

  • 用其他元素包裹传入的组件 WrappedComponent


反向继承


会发现其属性代理和反向继承的实现有些类似的地方,都是返回一个继承了某个父类的子类,只不过属性代理中继承的是 React.Component,反向继承中继承的是传入的组件 WrappedComponent


反向继承可以用来做什么:


1.操作 state


高阶组件中可以读取、编辑和删除WrappedComponent组件实例中的state。甚至可以增加更多的state项,但是非常不建议这么做因为这可能会导致state难以维护及管理。


function withLogging(WrappedComponent) {        return class extends WrappedComponent {            render() {                return (                    <div>;                        <h2>;Debugger Component Logging...<h2>;                        <p>;state:<p>;                        <pre>;{JSON.stringify(this.state, null, 4)}<pre>;                        <p>props:<p>;                        <pre>{JSON.stringify(this.props, null, 4)}<pre>;                        {super.render()}                    <div>;                );            }        };    }
复制代码


2.渲染劫持(Render Highjacking)


条件渲染通过 props.isLoading 这个条件来判断渲染哪个组件。


修改由 render() 输出的 React 元素树

React 组件命名推荐的方式是哪个?

通过引用而不是使用来命名组件 displayName。


使用 displayName 命名组件:


export default React.createClass({  displayName: 'TodoApp',  // ...})
复制代码


React 推荐的方法:


export default class TodoApp extends React.Component {  // ...}
复制代码

Component, Element, Instance 之间有什么区别和联系?

  • 元素: 一个元素element是一个普通对象(plain object),描述了对于一个 DOM 节点或者其他组件component,你想让它在屏幕上呈现成什么样子。元素element可以在它的属性props中包含其他元素(译注:用于形成元素树)。创建一个 React 元素element成本很低。元素element创建之后是不可变的。

  • 组件: 一个组件component可以通过多种方式声明。可以是带有一个render()方法的类,简单点也可以定义为一个函数。这两种情况下,它都把属性props作为输入,把返回的一棵元素树作为输出。

  • 实例: 一个实例instance是你在所写的组件类component class中使用关键字this所指向的东西(译注:组件实例)。它用来存储本地状态和响应生命周期事件很有用。


函数式组件(Functional component)根本没有实例instance。类组件(Class component)有实例instance,但是永远也不需要直接创建一个组件的实例,因为 React 帮我们做了这些。

Redux Thunk 的作用是什么

Redux thunk 是一个允许你编写返回一个函数而不是一个 action 的 actions creators 的中间件。如果满足某个条件,thunk 则可以用来延迟 action 的派发(dispatch),这可以处理异步 action 的派发(dispatch)。

React 组件生命周期有哪些不同阶段?

在组件生命周期中有四个不同的阶段:


  1. Initialization:在这个阶段,组件准备设置初始化状态和默认属性。

  2. Mounting:react 组件已经准备好挂载到浏览器 DOM 中。这个阶段包括componentWillMountcomponentDidMount生命周期方法。

  3. Updating:在这个阶段,组件以两种方式更新,发送新的 props 和 state 状态。此阶段包括shouldComponentUpdatecomponentWillUpdatecomponentDidUpdate生命周期方法。

  4. Unmounting:在这个阶段,组件已经不再被需要了,它从浏览器 DOM 中卸载下来。这个阶段包含 componentWillUnmount 生命周期方法。除以上四个常用生命周期外,还有一个错误处理的阶段:Error Handling:在这个阶段,不论在渲染的过程中,还是在生命周期方法中或是在任何子组件的构造函数中发生错误,该组件都会被调用。这个阶段包含了 componentDidCatch 生命周期方法。


React 中有使用过 getDefaultProps 吗?它有什么作用?

通过实现组件的 getDefaultProps,对属性设置默认值(ES5 的写法):


var ShowTitle = React.createClass({  getDefaultProps:function(){    return{      title : "React"    }  },  render : function(){    return <h1>{this.props.title}</h1>  }});
复制代码

为什么使用 jsx 的组件中没有看到使用 react 却需要引入 react?

本质上来说 JSX 是React.createElement(component, props, ...children)方法的语法糖。在 React 17 之前,如果使用了 JSX,其实就是在使用 React, babel 会把组件转换为 CreateElement 形式。在 React 17 之后,就不再需要引入,因为 babel 已经可以帮我们自动引入 react。

state 是怎么注入到组件的,从 reducer 到组件经历了什么样的过程

通过 connect 和 mapStateToProps 将 state 注入到组件中:


import { connect } from 'react-redux'import { setVisibilityFilter } from '@/reducers/Todo/actions'import Link from '@/containers/Todo/components/Link'
const mapStateToProps = (state, ownProps) => ({ active: ownProps.filter === state.visibilityFilter})
const mapDispatchToProps = (dispatch, ownProps) => ({ setFilter: () => { dispatch(setVisibilityFilter(ownProps.filter)) }})
export default connect( mapStateToProps, mapDispatchToProps)(Link)
复制代码


上面代码中,active 就是注入到 Link 组件中的状态。 mapStateToProps(state,ownProps)中带有两个参数,含义是∶


  • state-store 管理的全局状态对象,所有都组件状态数据都存储在该对象中。

  • ownProps 组件通过 props 传入的参数。


reducer 到组件经历的过程:


  • reducer 对 action 对象处理,更新组件状态,并将新的状态值返回 store。

  • 通过 connect(mapStateToProps,mapDispatchToProps)(Component)对组件 Component 进行升级,此时将状态值从 store 取出并作为 props 参数传递到组件。


高阶组件实现源码∶


import React from 'react'import PropTypes from 'prop-types'
// 高阶组件 contect export const connect = (mapStateToProps, mapDispatchToProps) => (WrappedComponent) => { class Connect extends React.Component { // 通过对context调用获取store static contextTypes = { store: PropTypes.object }
constructor() { super() this.state = { allProps: {} } }
// 第一遍需初始化所有组件初始状态 componentWillMount() { const store = this.context.store this._updateProps() store.subscribe(() => this._updateProps()); // 加入_updateProps()至store里的监听事件列表 }
// 执行action后更新props,使组件可以更新至最新状态(类似于setState) _updateProps() { const store = this.context.store; let stateProps = mapStateToProps ? mapStateToProps(store.getState(), this.props) : {} // 防止 mapStateToProps 没有传入 let dispatchProps = mapDispatchToProps ? mapDispatchToProps(store.dispatch, this.props) : { dispatch: store.dispatch } // 防止 mapDispatchToProps 没有传入 this.setState({ allProps: { ...stateProps, ...dispatchProps, ...this.props } }) }
render() { return <WrappedComponent {...this.state.allProps} /> } } return Connect}
复制代码

React 父组件如何调用子组件中的方法?

  1. 如果是在方法组件中调用子组件(>= react@16.8),可以使用 useRef 和 useImperativeHandle:


const { forwardRef, useRef, useImperativeHandle } = React;
const Child = forwardRef((props, ref) => { useImperativeHandle(ref, () => ({ getAlert() { alert("getAlert from Child"); } })); return <h1>Hi</h1>;});
const Parent = () => { const childRef = useRef(); return ( <div> <Child ref={childRef} /> <button onClick={() => childRef.current.getAlert()}>Click</button> </div> );};
复制代码


  1. 如果是在类组件中调用子组件(>= react@16.4),可以使用 createRef:


const { Component } = React;
class Parent extends Component { constructor(props) { super(props); this.child = React.createRef(); }
onClick = () => { this.child.current.getAlert(); };
render() { return ( <div> <Child ref={this.child} /> <button onClick={this.onClick}>Click</button> </div> ); }}
class Child extends Component { getAlert() { alert('getAlert from Child'); }
render() { return <h1>Hello</h1>; }}
复制代码


用户头像

beifeng1996

关注

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

还未添加个人简介

评论

发布
暂无评论
前端一面必会react面试题(附答案)_React_beifeng1996_InfoQ写作社区