写点什么

2022 前端二面 react 面试题(边面边更)

作者:beifeng1996
  • 2023-01-02
    浙江
  • 本文字数:9863 字

    阅读完需:约 32 分钟

何为 JSX

JSX 是 JavaScript 语法的一种语法扩展,并拥有 JavaScript 的全部功能。JSX 生产 React "元素",你可以将任何的 JavaScript 表达式封装在花括号里,然后将其嵌入到 JSX 中。在编译完成之后,JSX 表达式就变成了常规的 JavaScript 对象,这意味着你可以在 if 语句和 for 循环内部使用 JSX,将它赋值给变量,接受它作为参数,并从函数中返回它。

调用 setState 之后发生了什么

在代码中调用 setState 函数之后,React 会将传入的参数与之前的状态进行合并,然后触发所谓的调和过程(Reconciliation)。经过调和过程,React 会以相对高效的方式根据新的状态构建 React 元素树并且着手重新渲染整个 UI 界面。在 React 得到元素树之后,React 会计算出新的树和老的树之间的差异,然后根据差异对界面进行最小化重新渲染。通过 diff 算法,React 能够精确制导哪些位置发生了改变以及应该如何改变,这就保证了按需更新,而不是全部重新渲染。


  • 在 setState 的时候,React 会为当前节点创建一个 updateQueue 的更新列队。

  • 然后会触发 reconciliation 过程,在这个过程中,会使用名为 Fiber 的调度算法,开始生成新的 Fiber 树, Fiber 算法的最大特点是可以做到异步可中断的执行。

  • 然后 React Scheduler 会根据优先级高低,先执行优先级高的节点,具体是执行 doWork 方法。

  • 在 doWork 方法中,React 会执行一遍 updateQueue 中的方法,以获得新的节点。然后对比新旧节点,为老节点打上 更新、插入、替换 等 Tag。

  • 当前节点 doWork 完成后,会执行 performUnitOfWork 方法获得新节点,然后再重复上面的过程。

  • 当所有节点都 doWork 完成后,会触发 commitRoot 方法,React 进入 commit 阶段。

  • 在 commit 阶段中,React 会根据前面为各个节点打的 Tag,一次性更新整个 dom 元素

vue 或者 react 优化整体优化

  1. 虚拟 dom


为什么虚拟 dom 会提高性能?(必考)


虚拟 dom 相当于在 js 和真实 dom 中间加了一个缓存,利用 dom diff 算法避免了没有必要的 dom 操作,从而提高性能。


用 JavaScript 对象结构表示 DOM 树的结构;然后用这个树构建一个真正的 DOM 树,插到文档当中当状态变更的时候,重新构造一棵新的对象树。然后用新的树和旧的树进行比较,记录两棵树差异把 2 所记录的差异应用到步骤 1 所构建的真正的 DOM 树上,视图就更新了。

React 的严格模式如何使用,有什么用处?

StrictMode 是一个用来突出显示应用程序中潜在问题的工具。与 Fragment 一样,StrictMode 不会渲染任何可见的 UI。它为其后代元素触发额外的检查和警告。可以为应用程序的任何部分启用严格模式。例如:


import React from 'react';function ExampleApplication() {  return (    <div>      <Header />      <React.StrictMode>                <div>          <ComponentOne />          <ComponentTwo />        </div>      </React.StrictMode>            <Footer />    </div>  );}

复制代码


在上述的示例中,不会对 HeaderFooter 组件运行严格模式检查。但是,ComponentOneComponentTwo 以及它们的所有后代元素都将进行检查。


StrictMode 目前有助于:


  • 识别不安全的生命周期

  • 关于使用过时字符串 ref API 的警告

  • 关于使用废弃的 findDOMNode 方法的警告

  • 检测意外的副作用

  • 检测过时的 context API

hooks 父子传值

父传子在父组件中用useState声明数据 const [ data, setData ] = useState(false)
把数据传递给子组件<Child data={data} />
子组件接收export default function (props) { const { data } = props console.log(data)}子传父子传父可以通过事件方法传值,和父传子有点类似。在父组件中用useState声明数据 const [ data, setData ] = useState(false)
把更新数据的函数传递给子组件<Child setData={setData} />
子组件中触发函数更新数据,就会直接传递给父组件export default function (props) { const { setData } = props setData(true)}如果存在多个层级的数据传递,也可依照此方法依次传递
// 多层级用useContextconst User = () => { // 直接获取,不用回调 const { user, setUser } = useContext(UserContext); return <Avatar user={user} setUser={setUser} />;};
复制代码

react 生命周期

初始化阶段:


  • getDefaultProps:获取实例的默认属性

  • getInitialState:获取每个实例的初始化状态

  • componentWillMount:组件即将被装载、渲染到页面上

  • render:组件在这里生成虚拟的 DOM 节点

  • componentDidMount:组件真正在被装载之后


运行中状态:


  • componentWillReceiveProps:组件将要接收到属性的时候调用

  • shouldComponentUpdate:组件接受到新属性或者新状态的时候(可以返回 false,接收数据后不更新,阻止 render 调用,后面的函数不会被继续执行了)

  • componentWillUpdate:组件即将更新不能修改属性和状态

  • render:组件重新描绘

  • componentDidUpdate:组件已经更新


销毁阶段:


  • componentWillUnmount:组件即将销毁


shouldComponentUpdate 是做什么的,(react 性能优化是哪个周期函数?)


shouldComponentUpdate 这个方法用来判断是否需要调用 render 方法重新描绘 dom。因为 dom 的描绘非常消耗性能,如果我们能在 shouldComponentUpdate 方法中能够写出更优化的 dom diff 算法,可以极大的提高性能。


在 react17 会删除以下三个生命周期


componentWillMount,componentWillReceiveProps , componentWillUpdate


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

state 和 props 触发更新的生命周期分别有什么区别?

state 更新流程: 这个过程当中涉及的函数:


  1. shouldComponentUpdate: 当组件的 state 或 props 发生改变时,都会首先触发这个生命周期函数。它会接收两个参数:nextProps, nextState——它们分别代表传入的新 props 和新的 state 值。拿到这两个值之后,我们就可以通过一些对比逻辑来决定是否有 re-render(重渲染)的必要了。如果该函数的返回值为 false,则生命周期终止,反之继续;


注意:此方法仅作为性能优化的方式而存在。不要企图依靠此方法来“阻止”渲染,因为这可能会产生 bug。应该考虑使用内置的 PureComponent 组件,而不是手动编写 shouldComponentUpdate()


  1. componentWillUpdate:当组件的 state 或 props 发生改变时,会在渲染之前调用 componentWillUpdate。componentWillUpdate 是 React16 废弃的三个生命周期之一。过去,我们可能希望能在这个阶段去收集一些必要的信息(比如更新前的 DOM 信息等等),现在我们完全可以在 React16 的 getSnapshotBeforeUpdate 中去做这些事;

  2. componentDidUpdate:componentDidUpdate() 会在 UI 更新后会被立即调用。它接收 prevProps(上一次的 props 值)作为入参,也就是说在此处我们仍然可以进行 props 值对比(再次说明 componentWillUpdate 确实鸡肋哈)。



props 更新流程: 相对于 state 更新,props 更新后唯一的区别是增加了对 componentWillReceiveProps 的调用。关于 componentWillReceiveProps,需要知道这些事情:


  • componentWillReceiveProps:它在 Component 接受到新的 props 时被触发。componentWillReceiveProps 会接收一个名为 nextProps 的参数(对应新的 props 值)。该生命周期是 React16 废弃掉的三个生命周期之一。在它被废弃前,可以用它来比较 this.props 和 nextProps 来重新 setState。在 React16 中,用一个类似的新生命周期 getDerivedStateFromProps 来代替它。

为什么 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 父子传值

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


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


connection

React 数据持久化有什么实践吗?

封装数据持久化组件:


let storage={    // 增加    set(key, value){        localStorage.setItem(key, JSON.stringify(value));    },    // 获取    get(key){        return JSON.parse(localStorage.getItem(key));    },    // 删除    remove(key){        localStorage.removeItem(key);    }};export default Storage;
复制代码


在 React 项目中,通过 redux 存储全局数据时,会有一个问题,如果用户刷新了网页,那么通过 redux 存储的全局数据就会被全部清空,比如登录信息等。这时就会有全局数据持久化存储的需求。首先想到的就是 localStorage,localStorage 是没有时间限制的数据存储,可以通过它来实现数据的持久化存储。


但是在已经使用 redux 来管理和存储全局数据的基础上,再去使用 localStorage 来读写数据,这样不仅是工作量巨大,还容易出错。那么有没有结合 redux 来达到持久数据存储功能的框架呢?当然,它就是 redux-persist。redux-persist 会将 redux 的 store 中的数据缓存到浏览器的 localStorage 中。其使用步骤如下:


(1)首先要安装 redux-persist:


npm i redux-persist
复制代码


(2)对于 reducer 和 action 的处理不变,只需修改 store 的生成代码,修改如下:


import {createStore} from 'redux'import reducers from '../reducers/index'import {persistStore, persistReducer} from 'redux-persist';import storage from 'redux-persist/lib/storage';import autoMergeLevel2 from 'redux-persist/lib/stateReconciler/autoMergeLevel2';const persistConfig = {    key: 'root',    storage: storage,    stateReconciler: autoMergeLevel2 // 查看 'Merge Process' 部分的具体情况};const myPersistReducer = persistReducer(persistConfig, reducers)const store = createStore(myPersistReducer)export const persistor = persistStore(store)export default store
复制代码


(3)在 index.js 中,将 PersistGate 标签作为网页内容的父标签:


import React from 'react';import ReactDOM from 'react-dom';import {Provider} from 'react-redux'import store from './redux/store/store'import {persistor} from './redux/store/store'import {PersistGate} from 'redux-persist/lib/integration/react';ReactDOM.render(<Provider store={store}>            <PersistGate loading={null} persistor={persistor}>                {/*网页内容*/}            </PersistGate>        </Provider>, document.getElementById('root'));
复制代码


这就完成了通过 redux-persist 实现 React 持久化本地数据存储的简单应用。

Redux 中的 connect 有什么作用

connect 负责连接 React 和 Redux


(1)获取 state


connect 通过 context 获取 Provider 中的 store,通过 store.getState() 获取整个 store tree 上所有 state


(2)包装原组件


将 state 和 action 通过 props 的方式传入到原组件内部 wrapWithConnect 返回—个 ReactComponent 对 象 Connect,Connect 重 新 render 外部传入的原组件 WrappedComponent ,并把 connect 中传入的 mapStateToProps,mapDispatchToProps 与组件上原有的 props 合并后,通过属性的方式传给 WrappedComponent


(3)监听 store tree 变化


connect 缓存了 store tree 中 state 的状态,通过当前 state 状态 和变更前 state 状态进行比较,从而确定是否调用 this.setState()方法触发 Connect 及其子组件的重新渲染

为什么 useState 要使用数组而不是对象

useState 的用法:


const [count, setCount] = useState(0)
复制代码


可以看到 useState 返回的是一个数组,那么为什么是返回数组而不是返回对象呢?


这里用到了解构赋值,所以先来看一下 ES6 的解构赋值:

数组的解构赋值
const foo = [1, 2, 3];const [one, two, three] = foo;console.log(one);    // 1console.log(two);    // 2console.log(three);    // 3
复制代码
对象的解构赋值
const user = {  id: 888,  name: "xiaoxin"};const { id, name } = user;console.log(id);    // 888console.log(name);    // "xiaoxin"
复制代码


看完这两个例子,答案应该就出来了:


  • 如果 useState 返回的是数组,那么使用者可以对数组中的元素命名,代码看起来也比较干净

  • 如果 useState 返回的是对象,在解构对象的时候必须要和 useState 内部实现返回的对象同名,想要使用多次的话,必须得设置别名才能使用返回值


下面来看看如果 useState 返回对象的情况:


// 第一次使用const { state, setState } = useState(false);// 第二次使用const { state: counter, setState: setCounter } = useState(0) 
复制代码


这里可以看到,返回对象的使用方式还是挺麻烦的,更何况实际项目中会使用的更频繁。 总结:useState 返回的是 array 而不是 object 的原因就是为了降低使用的复杂度,返回数组的话可以直接根据顺序解构,而返回对象的话要想使用多次就需要定义别名了。

React-Router 4 的 Switch 有什么用?

Switch 通常被用来包裹 Route,用于渲染与路径匹配的第一个子 <Route><Redirect>,它里面不能放其他元素。


假如不加 <Switch>


import { Route } from 'react-router-dom'
<Route path="/" component={Home}></Route><Route path="/login" component={Login}></Route>
复制代码


Route 组件的 path 属性用于匹配路径,因为需要匹配 /Home,匹配 /loginLogin,所以需要两个 Route,但是不能这么写。这样写的话,当 URL 的 path 为 “/login” 时,<Route path="/" /><Route path="/login" /> 都会被匹配,因此页面会展示 Home 和 Login 两个组件。这时就需要借助 <Switch> 来做到只显示一个匹配组件:


import { Switch, Route} from 'react-router-dom'
<Switch> <Route path="/" component={Home}></Route> <Route path="/login" component={Login}></Route></Switch>
复制代码


此时,再访问 “/login” 路径时,却只显示了 Home 组件。这是就用到了 exact 属性,它的作用就是精确匹配路径,经常与<Switch> 联合使用。只有当 URL 和该 <Route> 的 path 属性完全一致的情况下才能匹配上:


import { Switch, Route} from 'react-router-dom'
<Switch> <Route exact path="/" component={Home}></Route> <Route exact path="/login" component={Login}></Route></Switch>
复制代码

react 最新版本解决了什么问题,增加了哪些东西

React 16.x 的三大新特性 Time Slicing、Suspense、 hooks


  • Time Slicing(解决 CPU 速度问题)使得在执行任务的期间可以随时暂停,跑去干别的事情,这个特性使得 react 能在性能极其差的机器跑时,仍然保持有良好的性能

  • Suspense (解决网络 IO 问题) 和 lazy 配合,实现异步加载组件。 能暂停当前组件的渲染, 当完成某件事以后再继续渲染,解决从 react 出生到现在都存在的「异步副作用」的问题,而且解决得非的优雅,使用的是 T 异步但是同步的写法,这是最好的解决异步问题的方式

  • 提供了一个内置函数 componentDidCatch,当有错误发生时,可以友好地展示 fallback 组件; 可以捕捉到它的子元素(包括嵌套子元素)抛出的异常; 可以复用错误组件。


(1)React16.8 加入 hooks,让 React 函数式组件更加灵活,hooks 之前,React 存在很多问题:


  • 在组件间复用状态逻辑很难

  • 复杂组件变得难以理解,高阶组件和函数组件的嵌套过深。

  • class 组件的 this 指向问题

  • 难以记忆的生命周期


hooks 很好的解决了上述问题,hooks 提供了很多方法


  • useState 返回有状态值,以及更新这个状态值的函数

  • useEffect 接受包含命令式,可能有副作用代码的函数。

  • useContext 接受上下文对象(从 React.createContext 返回的值)并返回当前上下文值,

  • useReducer useState 的替代方案。接受类型为 (state,action)=> newState 的 reducer,并返回与 dispatch 方法配对的当前状态。

  • useCalLback 返回一个回忆的 memoized 版本,该版本仅在其中一个输入发生更改时才会更改。纯函数的输入输出确定性 o useMemo 纯的一个记忆函数 o useRef 返回一个可变的 ref 对象,其 Current 属性被初始化为传递的参数,返回的 ref 对象在组件的整个生命周期内保持不变。

  • useImperativeMethods 自定义使用 ref 时公开给父组件的实例值

  • useMutationEffect 更新兄弟组件之前,它在 React 执行其 DOM 改变的同一阶段同步触发

  • useLayoutEffect DOM 改变后同步触发。使用它来从 DOM 读取布局并同步重新渲染


(2)React16.9


  • 重命名 Unsafe 的生命周期方法。新的 UNSAFE_前缀将有助于在代码 review 和 debug 期间,使这些有问题的字样更突出

  • 废弃 javascrip:形式的 URL。以 javascript:开头的 URL 非常容易遭受攻击,造成安全漏洞。

  • 废弃"Factory"组件。 工厂组件会导致 React 变大且变慢。

  • act()也支持异步函数,并且你可以在调用它时使用 await。

  • 使用 <React.ProfiLer> 进行性能评估。在较大的应用中追踪性能回归可能会很方便


(3)React16.13.0


  • 支持在渲染期间调用 setState,但仅适用于同一组件

  • 可检测冲突的样式规则并记录警告

  • 废弃 unstable_createPortal,使用 CreatePortal

  • 将组件堆栈添加到其开发警告中,使开发人员能够隔离 bug 并调试其程序,这可以清楚地说明问题所在,并更快地定位和修复错误。

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

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


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

对 React 中 Fragment 的理解,它的使用场景是什么?

在 React 中,组件返回的元素只能有一个根元素。为了不添加多余的 DOM 节点,我们可以使用 Fragment 标签来包裹所有的元素,Fragment 标签不会渲染出任何元素。React 官方对 Fragment 的解释:


React 中的一个常见模式是一个组件返回多个元素。Fragments 允许你将子列表分组,而无需向 DOM 添加额外节点。


import React, { Component, Fragment } from 'react'
// 一般形式render() { return ( <React.Fragment> <ChildA /> <ChildB /> <ChildC /> </React.Fragment> );}// 也可以写成以下形式render() { return ( <> <ChildA /> <ChildB /> <ChildC /> </> );}
复制代码

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 中 constructor 和 getInitialState 的区别?

两者都是用来初始化 state 的。前者是 ES6 中的语法,后者是 ES5 中的语法,新版本的 React 中已经废弃了该方法。


getInitialState 是 ES5 中的方法,如果使用 createClass 方法创建一个 Component 组件,可以自动调用它的 getInitialState 方法来获取初始化的 State 对象,


var APP = React.creatClass ({  getInitialState() {    return {         userName: 'hi',        userId: 0     }; }})
复制代码


React 在 ES6 的实现中去掉了 getInitialState 这个 hook 函数,规定 state 在 constructor 中实现,如下:


Class App extends React.Component{    constructor(props){      super(props);      this.state={};    }  }
复制代码

react router

import React from 'react'import { render } from 'react-dom'import { browserHistory, Router, Route, IndexRoute } from 'react-router'
import App from '../components/App'import Home from '../components/Home'import About from '../components/About'import Features from '../components/Features'
render( <Router history={browserHistory}> // history 路由 <Route path='/' component={App}> <IndexRoute component={Home} /> <Route path='about' component={About} /> <Route path='features' component={Features} /> </Route> </Router>, document.getElementById('app'))render( <Router history={browserHistory} routes={routes} />, document.getElementById('app'))
复制代码


React Router 提供一个 routerWillLeave 生命周期钩子,这使得 React 组件可以拦截正在发生的跳转,或在离开 route 前提示用户。routerWillLeave 返回值有以下两种:


return false 取消此次跳转


return 返回提示信息,在离开 route 前提示用户进行确认。

生命周期调用方法的顺序是什么?

React 生命周期分为三大周期,11 个阶段,生命周期方法调用顺序分别如下。(1)在创建期的五大阶段,调用方法的顺序如下。


  • getDetaultProps:定义默认属性数据。

  • getInitialState:初始化默认状态数据。

  • component WillMount:组件即将被构建。

  • render:渲染组件。

  • componentDidMount:组件构建完成


(2)在存在期的五大阶段,调用方法的顺序如下。


  • componentWillReceiveProps:组件即将接收新的属性数据。

  • shouldComponentUpdate:判断组件是否应该更新。

  • componnent WillUpdate:组件即将更新。

  • render:渲染组件。

  • componentDidUpdate:组件更新完成。


(3)在销毁期的一个阶段,调用方法 componentWillUnmount,表示组件即将被销毀。


用户头像

beifeng1996

关注

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

还未添加个人简介

评论

发布
暂无评论
2022前端二面react面试题(边面边更)_React_beifeng1996_InfoQ写作社区