写点什么

阿里前端二面常考 react 面试题(必备)

作者:xiaofeng
  • 2023-02-28
    浙江
  • 本文字数:7503 字

    阅读完需:约 25 分钟

说说 React 组件开发中关于作用域的常见问题。

在 EMAScript5 语法规范中,关于作用域的常见问题如下。(1)在 map 等方法的回调函数中,要绑定作用域 this(通过 bind 方法)。(2)父组件传递给子组件方法的作用域是父组件实例化对象,无法改变。(3)组件事件回调函数方法的作用域是组件实例化对象(绑定父组件提供的方法就是父组件实例化对象),无法改变。在 EMAScript6 语法规范中,关于作用域的常见问题如下。(1)当使用箭头函数作为 map 等方法的回调函数时,箭头函数的作用域是当前组件的实例化对象(即箭头函数的作用域是定义时的作用域),无须绑定作用域。(2)事件回调函数要绑定组件作用域。(3)父组件传递方法要绑定父组件作用域。总之,在 EMAScript6 语法规范中,组件方法的作用域是可以改变的。

描述事件在 React 中的处理方式。

为了解决跨浏览器兼容性问题, React 中的事件处理程序将传递 SyntheticEvent 的实例,它是跨浏览器事件的包装器。这些 SyntheticEvent 与你习惯的原生事件具有相同的接口,它们在所有浏览器中都兼容。React 实际上并没有将事件附加到子节点本身。而是通过事件委托模式,使用单个事件监听器监听顶层的所有事件。这对于性能是有好处的。这也意味着在更新 DOM 时, React 不需要担心跟踪事件监听器。

react 的虚拟 dom 是怎么实现的


首先说说为什么要使用Virturl DOM,因为操作真实DOM的耗费的性能代价太高,所以react内部使用js实现了一套 dom 结构,在每次操作在和真实 dom 之前,使用实现好的 diff 算法,对虚拟 dom 进行比较,递归找出有变化的 dom 节点,然后对其进行更新操作。为了实现虚拟DOM,我们需要把每一种节点类型抽象成对象,每一种节点类型有自己的属性,也就是 prop,每次进行diff的时候,react会先比较该节点类型,假如节点类型不一样,那么react会直接删除该节点,然后直接创建新的节点插入到其中,假如节点类型一样,那么会比较prop是否有更新,假如有prop不一样,那么react会判定该节点有更新,那么重渲染该节点,然后在对其子节点进行比较,一层一层往下,直到没有子节点

React 的虚拟 DOM 和 Diff 算法的内部实现

传统 diff 算法的时间复杂度是 O(n^3),这在前端 render 中是不可接受的。为了降低时间复杂度,react 的 diff 算法做了一些妥协,放弃了最优解,最终将时间复杂度降低到了 O(n)。


那么 react diff 算法做了哪些妥协呢?,参考如下:


  1. tree diff:只对比同一层的 dom 节点,忽略 dom 节点的跨层级移动


如下图,react 只会对相同颜色方框内的 DOM 节点进行比较,即同一个父节点下的所有子节点。当发现节点不存在时,则该节点及其子节点会被完全删除掉,不会用于进一步的比较。


这样只需要对树进行一次遍历,便能完成整个 DOM 树的比较。



这就意味着,如果 dom 节点发生了跨层级移动,react 会删除旧的节点,生成新的节点,而不会复用。


  1. component diff:如果不是同一类型的组件,会删除旧的组件,创建新的组件



  1. element diff:对于同一层级的一组子节点,需要通过唯一 id 进行来区分


  • 如果没有 id 来进行区分,一旦有插入动作,会导致插入位置之后的列表全部重新渲染

  • 这也是为什么渲染列表时为什么要使用唯一的 key。

react 强制刷新

component.forceUpdate() 一个不常用的生命周期方法, 它的作用就是强制刷新


官网解释如下


默认情况下,当组件的 state 或 props 发生变化时,组件将重新渲染。如果 render() 方法依赖于其他数据,则可以调用 forceUpdate() 强制让组件重新渲染。


调用 forceUpdate() 将致使组件调用 render() 方法,此操作会跳过该组件的 shouldComponentUpdate()。但其子组件会触发正常的生命周期方法,包括 shouldComponentUpdate() 方法。如果标记发生变化,React 仍将只更新 DOM。


通常你应该避免使用 forceUpdate(),尽量在 render() 中使用 this.props 和 this.state。


shouldComponentUpdate 在初始化 和 forceUpdate 不会执行

React 16.X 中 props 改变后在哪个生命周期中处理

在 getDerivedStateFromProps 中进行处理。


这个生命周期函数是为了替代componentWillReceiveProps存在的,所以在需要使用componentWillReceiveProps时,就可以考虑使用getDerivedStateFromProps来进行替代。


两者的参数是不相同的,而getDerivedStateFromProps是一个静态函数,也就是这个函数不能通过 this 访问到 class 的属性,也并不推荐直接访问属性。而是应该通过参数提供的 nextProps 以及 prevState 来进行判断,根据新传入的 props 来映射到 state。


需要注意的是,如果 props 传入的内容不需要影响到你的 state,那么就需要返回一个 null,这个返回值是必须的,所以尽量将其写到函数的末尾:


static getDerivedStateFromProps(nextProps, prevState) {    const {type} = nextProps;    // 当传入的type发生变化的时候,更新state    if (type !== prevState.type) {        return {            type,        };    }    // 否则,对于state不进行任何操作    return null;}
复制代码


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

组件是什么?类是什么?类变编译成什么

  • 组件指的是页面的一部分,本质就是一个类,最本质就是一个构造函数

  • 类编译成构造函数

React-Router 的路由有几种模式?

React-Router 支持使用 hash(对应 HashRouter)和 browser(对应 BrowserRouter) 两种路由规则, react-router-dom 提供了 BrowserRouter 和 HashRouter 两个组件来实现应用的 UI 和 URL 同步:


  • BrowserRouter 创建的 URL 格式:xxx.com/path

  • HashRouter 创建的 URL 格式:xxx.com/#/path


(1)BrowserRouter


它使用 HTML5 提供的 history API(pushState、replaceState 和 popstate 事件)来保持 UI 和 URL 的同步。由此可以看出,BrowserRouter 是使用 HTML 5 的 history API 来控制路由跳转的:


<BrowserRouter    basename={string}    forceRefresh={bool}    getUserConfirmation={func}    keyLength={number}/>
复制代码


其中的属性如下:


  • basename 所有路由的基准 URL。basename 的正确格式是前面有一个前导斜杠,但不能有尾部斜杠;


<BrowserRouter basename="/calendar">    <Link to="/today" /></BrowserRouter>
复制代码


等同于


<a href="/calendar/today" />
复制代码


  • forceRefresh 如果为 true,在导航的过程中整个页面将会刷新。一般情况下,只有在不支持 HTML5 history API 的浏览器中使用此功能;

  • getUserConfirmation 用于确认导航的函数,默认使用 window.confirm。例如,当从 /a 导航至 /b 时,会使用默认的 confirm 函数弹出一个提示,用户点击确定后才进行导航,否则不做任何处理;


// 这是默认的确认函数const getConfirmation = (message, callback) => {  const allowTransition = window.confirm(message);  callback(allowTransition);}<BrowserRouter getUserConfirmation={getConfirmation} />
复制代码


需要配合<Prompt> 一起使用。


  • KeyLength 用来设置 Location.Key 的长度。


(2)HashRouter


使用 URL 的 hash 部分(即 window.location.hash)来保持 UI 和 URL 的同步。由此可以看出,HashRouter 是通过 URL 的 hash 属性来控制路由跳转的:


<HashRouter    basename={string}    getUserConfirmation={func}    hashType={string}  />
复制代码


其参数如下


  • basename, getUserConfirmation 和 BrowserRouter 功能一样;

  • hashType window.location.hash 使用的 hash 类型,有如下几种:

  • slash - 后面跟一个斜杠,例如 #/ 和 #/sunshine/lollipops;

  • noslash - 后面没有斜杠,例如 # 和 #sunshine/lollipops;

  • hashbang - Google 风格的 ajax crawlable,例如 #!/ 和 #!/sunshine/lollipops。

传入 setstate 函数的第二个参数的作用是什么?

第二个参数是一个函数,该函数会在 setState 函数调用完成并且组件开始重渲染时调用,可以用该函数来监听渲染是否完成。


this.setstate(  {    username: "有课前端网",  },  () => console.log("re-rendered success. "));
复制代码

对 Redux 的理解,主要解决什么问题

React 是视图层框架。Redux 是一个用来管理数据状态和 UI 状态的 JavaScript 应用工具。随着 JavaScript 单页应用(SPA)开发日趋复杂, JavaScript 需要管理比任何时候都要多的 state(状态), Redux 就是降低管理难度的。(Redux 支持 React、Angular、jQuery 甚至纯 JavaScript)。


在 React 中,UI 以组件的形式来搭建,组件之间可以嵌套组合。但 React 中组件间通信的数据流是单向的,顶层组件可以通过 props 属性向下层组件传递数据,而下层组件不能向上层组件传递数据,兄弟组件之间同样不能。这样简单的单向数据流支撑起了 React 中的数据可控性。


当项目越来越大的时候,管理数据的事件或回调函数将越来越多,也将越来越不好管理。管理不断变化的 state 非常困难。如果一个 model 的变化会引起另一个 model 变化,那么当 view 变化时,就可能引起对应 model 以及另一个 model 的变化,依次地,可能会引起另一个 view 的变化。直至你搞不清楚到底发生了什么。state 在什么时候,由于什么原因,如何变化已然不受控制。 当系统变得错综复杂的时候,想重现问题或者添加新功能就会变得举步维艰。如果这还不够糟糕,考虑一些来自前端开发领域的新需求,如更新调优、服务端渲染、路由跳转前请求数据等。state 的管理在大项目中相当复杂。


Redux 提供了一个叫 store 的统一仓储库,组件通过 dispatch 将 state 直接传入 store,不用通过其他的组件。并且组件通过 subscribe 从 store 获取到 state 的改变。使用了 Redux,所有的组件都可以从 store 中获取到所需的 state,他们也能从 store 获取到 state 的改变。这比组件之间互相传递数据清晰明朗的多。


主要解决的问题: 单纯的 Redux 只是一个状态机,是没有 UI 呈现的,react- redux 作用是将 Redux 的状态机和 React 的 UI 呈现绑定在一起,当你 dispatch action 改变 state 的时候,会自动更新页面。

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 的插槽(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>  );}
复制代码

传入 setState 函数的第二个参数的作用是什么?

该函数会在 setState 函数调用完成并且组件开始重渲染的时候被调用,我们可以用该函数来监听渲染是否完成:


this.setState(  { username: 'tylermcginnis33' },  () => console.log('setState has finished and the component has re-rendered.'))
复制代码


this.setState((prevState, props) => {  return {    streak: prevState.streak + props.count  }})
复制代码

组件通信的方式有哪些

  • ⽗组件向⼦组件通讯: ⽗组件可以向⼦组件通过传 props 的⽅式,向⼦组件进⾏通讯

  • ⼦组件向⽗组件通讯: props+回调的⽅式,⽗组件向⼦组件传递 props 进⾏通讯,此 props 为作⽤域为⽗组件⾃身的函 数,⼦组件调⽤该函数,将⼦组件想要传递的信息,作为参数,传递到⽗组件的作⽤域中

  • 兄弟组件通信: 找到这两个兄弟节点共同的⽗节点,结合上⾯两种⽅式由⽗节点转发信息进⾏通信

  • 跨层级通信: Context 设计⽬的是为了共享那些对于⼀个组件树⽽⾔是“全局”的数据,例如当前认证的⽤户、主题或⾸选语⾔,对于跨越多层的全局数据通过 Context 通信再适合不过

  • 发布订阅模式: 发布者发布事件,订阅者监听事件并做出反应,我们可以通过引⼊event 模块进⾏通信

  • 全局状态管理⼯具: 借助 Redux 或者 Mobx 等全局状态管理⼯具进⾏通信,这种⼯具会维护⼀个全局状态中⼼Store,并根据不同的事件产⽣新的状态

解释 React 中 render() 的目的。

每个 React 组件强制要求必须有一个 **render()**。它返回一个 React 元素,是原生 DOM 组件的表示。如果需要渲染多个 HTML 元素,则必须将它们组合在一个封闭标记内,例如 <form><group><div> 等。此函数必须保持纯净,即必须每次调用时都返回相同的结果。

哪个生命周期发送 ajax

  • componentWillMount 在新版本 react 中已经被废弃了

  • 在做 ssr 项目时候,componentWillMount 要做服务端数据的获取,不能被占用

  • 所以在 componentDidMount 中请求

什么是 Props

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

React-Router 4 怎样在路由变化时重新渲染同一个组件?

当路由变化时,即组件的 props 发生了变化,会调用 componentWillReceiveProps 等生命周期钩子。那需要做的只是: 当路由改变时,根据路由,也去请求数据:


class NewsList extends Component {  componentDidMount () {     this.fetchData(this.props.location);  }
fetchData(location) { const type = location.pathname.replace('/', '') || 'top' this.props.dispatch(fetchListData(type)) } componentWillReceiveProps(nextProps) { if (nextProps.location.pathname != this.props.location.pathname) { this.fetchData(nextProps.location); } } render () { ... }}
复制代码


利用生命周期 componentWillReceiveProps,进行重新 render 的预处理操作。

componentWillReceiveProps 调用时机

  • 已经被废弃掉

  • 当 props 改变的时候才调用,子组件第二次接收到 props 的时候

介绍一下 react

  1. 以前我们没有 jquery 的时候,我们大概的流程是从后端通过 ajax 获取到数据然后使用 jquery 生成 dom 结果然后更新到页面当中,但是随着业务发展,我们的项目可能会越来越复杂,我们每次请求到数据,或则数据有更改的时候,我们又需要重新组装一次 dom 结构,然后更新页面,这样我们手动同步 dom 和数据的成本就越来越高,而且频繁的操作 dom,也使我我们页面的性能慢慢的降低。

  2. 这个时候 mvvm 出现了,mvvm 的双向数据绑定可以让我们在数据修改的同时同步 dom 的更新,dom 的更新也可以直接同步我们数据的更改,这个特定可以大大降低我们手动去维护 dom 更新的成本,mvvm 为 react 的特性之一,虽然 react 属于单项数据流,需要我们手动实现双向数据绑定。

  3. 有了 mvvm 还不够,因为如果每次有数据做了更改,然后我们都全量更新 dom 结构的话,也没办法解决我们频繁操作 dom 结构(降低了页面性能)的问题,为了解决这个问题,react 内部实现了一套虚拟 dom 结构,也就是用 js 实现的一套 dom 结构,他的作用是讲真实 dom 在 js 中做一套缓存,每次有数据更改的时候,react 内部先使用算法,也就是鼎鼎有名的 diff 算法对 dom 结构进行对比,找到那些我们需要新增、更新、删除的 dom 节点,然后一次性对真实 DOM 进行更新,这样就大大降低了操作 dom 的次数。 那么 diff 算法是怎么运作的呢,首先,diff 针对类型不同的节点,会直接判定原来节点需要卸载并且用新的节点来装载卸载的节点的位置;针对于节点类型相同的节点,会对比这个节点的所有属性,如果节点的所有属性相同,那么判定这个节点不需要更新,如果节点属性不相同,那么会判定这个节点需要更新,react 会更新并重渲染这个节点。

  4. react 设计之初是主要负责 UI 层的渲染,虽然每个组件有自己的 state,state 表示组件的状态,当状态需要变化的时候,需要使用 setState 更新我们的组件,但是,我们想通过一个组件重渲染它的兄弟组件,我们就需要将组件的状态提升到父组件当中,让父组件的状态来控制这两个组件的重渲染,当我们组件的层次越来越深的时候,状态需要一直往下传,无疑加大了我们代码的复杂度,我们需要一个状态管理中心,来帮我们管理我们状态 state。

  5. 这个时候,redux 出现了,我们可以将所有的 state 交给 redux 去管理,当我们的某一个 state 有变化的时候,依赖到这个 state 的组件就会进行一次重渲染,这样就解决了我们的我们需要一直把 state 往下传的问题。redux 有 action、reducer 的概念,action 为唯一修改 state 的来源,reducer 为唯一确定 state 如何变化的入口,这使得 redux 的数据流非常规范,同时也暴露出了 redux 代码的复杂,本来那么简单的功能,却需要完成那么多的代码。

  6. 后来,社区就出现了另外一套解决方案,也就是 mobx,它推崇代码简约易懂,只需要定义一个可观测的对象,然后哪个组价使用到这个可观测的对象,并且这个对象的数据有更改,那么这个组件就会重渲染,而且 mobx 内部也做好了是否重渲染组件的生命周期 shouldUpdateComponent,不建议开发者进行更改,这使得我们使用 mobx 开发项目的时候可以简单快速的完成很多功能,连 redux 的作者也推荐使用 mobx 进行项目开发。但是,随着项目的不断变大,mobx 也不断暴露出了它的缺点,就是数据流太随意,出了 bug 之后不好追溯数据的流向,这个缺点正好体现出了 redux 的优点所在,所以针对于小项目来说,社区推荐使用 mobx,对大项目推荐使用 redux


用户头像

xiaofeng

关注

努力写代码中 2022-08-18 加入

努力写代码中

评论

发布
暂无评论
阿里前端二面常考react面试题(必备)_前端_xiaofeng_InfoQ写作社区