写点什么

腾讯前端经典 react 面试题(附答案)

作者:beifeng1996
  • 2023-02-19
    浙江
  • 本文字数:9000 字

    阅读完需:约 30 分钟

React 性能优化在哪个生命周期?它优化的原理是什么?

react 的父级组件的 render 函数重新渲染会引起子组件的 render 方法的重新渲染。但是,有的时候子组件的接受父组件的数据没有变动。子组件 render 的执行会影响性能,这时就可以使用 shouldComponentUpdate 来解决这个问题。


使用方法如下:


shouldComponentUpdate(nexrProps) {    if (this.props.num === nexrProps.num) {        return false    }    return true;}
复制代码


shouldComponentUpdate 提供了两个参数 nextProps 和 nextState,表示下一次 props 和一次 state 的值,当函数返回 false 时候,render()方法不执行,组件也就不会渲染,返回 true 时,组件照常重渲染。此方法就是拿当前 props 中值和下一次 props 中的值进行对比,数据相等时,返回 false,反之返回 true。


需要注意,在进行新旧对比的时候,是浅对比,也就是说如果比较的数据时引用数据类型,只要数据的引用的地址没变,即使内容变了,也会被判定为 true。


面对这个问题,可以使用如下方法进行解决:(1)使用 setState 改变数据之前,先采用 ES6 中 assgin 进行拷贝,但是 assgin 只深拷贝的数据的第一层,所以说不是最完美的解决办法:


const o2 = Object.assign({},this.state.obj)    o2.student.count = '00000';    this.setState({        obj: o2,    })
复制代码


(2)使用 JSON.parse(JSON.stringfy())进行深拷贝,但是遇到数据为 undefined 和函数时就会错。


const o2 = JSON.parse(JSON.stringify(this.state.obj))    o2.student.count = '00000';    this.setState({        obj: o2,    })
复制代码

vue 或者 react 优化整体优化

  1. 虚拟 dom


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


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


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

对 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 /> </> );}
复制代码

在哪个生命周期中你会发出 Ajax 请求?为什么?

Ajax 请求应该写在组件创建期的第五个阶段,即 componentDidMount 生命周期方法中。原因如下。在创建期的其他阶段,组件尚未渲染完成。而在存在期的 5 个阶段,又不能确保生命周期方法一定会执行(如通过 shouldComponentUpdate 方法优化更新等)。在销毀期,组件即将被销毁,请求数据变得无意义。因此在这些阶段发岀 Ajax 请求显然不是最好的选择。在组件尚未挂载之前,Ajax 请求将无法执行完毕,如果此时发出请求,将意味着在组件挂载之前更新状态(如执行 setState),这通常是不起作用的。在 componentDidMount 方法中,执行 Ajax 即可保证组件已经挂载,并且能够正常更新组件。

Hooks 可以取代 render props 和高阶组件吗?

通常,render props和高阶组件仅渲染一个子组件。React 团队认为,Hooks 是服务此用例的更简单方法。这两种模式仍然有一席之地(例如,一个虚拟的 scroller 组件可能有一个 renderItem prop,或者一个可视化的容器组件可能有它自己的 DOM 结构)。但在大多数情况下,Hooks 就足够了,可以帮助减少树中的嵌套。

createElement 过程

React.createElement(): 根据指定的第一个参数创建一个 React 元素


React.createElement(  type,  [props],  [...children])
复制代码


  • 第一个参数是必填,传入的是似 HTML 标签名称,eg: ul, li

  • 第二个参数是选填,表示的是属性,eg: className

  • 第三个参数是选填, 子节点,eg: 要显示的文本内容


//写法一:
var child1 = React.createElement('li', null, 'one'); var child2 = React.createElement('li', null, 'two'); var content = React.createElement('ul', { className: 'teststyle' }, child1, child2); // 第三个参数可以分开也可以写成一个数组 ReactDOM.render( content, document.getElementById('example') );
//写法二:
var child1 = React.createElement('li', null, 'one'); var child2 = React.createElement('li', null, 'two'); var content = React.createElement('ul', { className: 'teststyle' }, [child1, child2]); ReactDOM.render( content, document.getElementById('example') );
复制代码


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

react-router4 的核心

  • 路由变成了组件

  • 分散到各个页面,不需要配置 比如<link> <route></route>

这段代码有什么问题吗?

这段代码有什么问题:


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


答案:没有什么问题。这种方式很少被使用,咱们可以将一个函数传递给setState,该函数接收上一个 state 的值和当前的props,并返回一个新的状态,如果咱们需要根据以前的状态重新设置状态,推荐使用这种方式。

这三个点(...)在 React 干嘛用的?

... 在 React(使用 JSX)代码中做什么?它叫什么?


<Modal {...this.props} title='Modal heading' animation={false}/>
复制代码


这个叫扩展操作符号或者展开操作符,例如,如果this.props包含a:1b:2,则


<Modal {...this.props} title='Modal heading' animation={false}>
复制代码


等价于下面内容:


<Modal a={this.props.a} b={this.props.b} title='Modal heading' animation={false}>
复制代码


扩展符号不仅适用于该用例,而且对于创建具有现有对象的大多数(或全部)属性的新对象非常方便,在更新state 咱们就经常这么做:


this.setState((prevState) => {  return { foo: { ...prevState.foo, a: "updated" } };});
复制代码

react-redux 的实现原理?

通过 redux 和 react context 配合使用,并借助高阶函数,实现了 react-redux

React 中的 StrictMode(严格模式)是什么??

React 的StrictMode是一种辅助组件,可以帮助咱们编写更好的 react 组件,可以使用<StrictMode />包装一组组件,并且可以帮咱们以下检查:


  • 验证内部组件是否遵循某些推荐做法,如果没有,会在控制台给出警告。

  • 验证是否使用的已经废弃的方法,如果有,会在控制台给出警告。

  • 通过识别潜在的风险预防一些副作用。

什么情况下使用异步组件

  • 提高页面加载速度,使用reloadable把各个页面分别单独打包,按需加载

如果创建了类似于下面的 Icketang 元素,那么该如何实现 Icketang 类?

<Icketang username="雨夜清荷">{(user) => (user ? <Info user={user} /> : <Loading />)}</Icketang>;import React, { Component } from "react";export class Icketang extends Component {  //请实现你的代码}
复制代码


在上面的案例中,一个组件接受一个函数作为它的子组件。Icketang 组件的子组件是一个函数,而不是一个常用的组件。这意味着在实现 Icketang 组件时,需要将 props. children 作为一个函数来处理。具体实现如下。


import React, { Component } from "react";class Icketang extends Component {  constructor(props) {    super(props);    this.state = {      user: props.user,    };  }  componentDidMount() {    //模拟异步获取数据操作,更新状态    setTimeout(      () =>        this.setstate({          user: "有课前端网",        }),      2000    );  }  render() {    return this.props.children(this.state.user);  }}class Loading extends Component {  render() {    return <p>Loading.</p>;  }}class Info extends Component {  render() {    return <h1> {this.props.user}</h1>;  }}
复制代码


调用 Icketang 组件,并传递给 user 属性数据,把 props.children 作为一个函数来处理。这种模式的好处是,我们已经将父组件与子组件分离了,父组件管理状态。父组件的使用者可以决定父组件以何种形式渲染子组件。为了演示这一点,在渲染 Icketang 组件时,分别传递和不传递 user 属性数据来观察渲染结果。


import { render } from "react-dom";render(<Icketang>{(user) => (user ? <Info user={user} /> : <Loading />)}</Icketang>, ickt);
复制代码


上述代码没有为 Icketang 组件传递 user 属性数据,因此将首先渲染 Loading 组件,当父组件的 user 状态数据发生改变时,我们发现 Info 组件可以成功地渲染出来。


render(<Icketang user="雨夜清荷">{(user) => (user ? <Info user={user} /> : <Loading />)}</Icketang>, ickt);
复制代码


上述代码为 Icketang 组件传递了 user 属性数据,因此将直接渲染 Info 组件,当父组件的 user 状态数据发生改变时,我们发现 Info 组件产生了更新,在整个过程中, Loading 组件都未渲染。

如何用 React 构建( build)生产模式?

通常,使用 Webpack 的 DefinePlugin 方法将 NODE ENV 设置为 production。这将剥离 propType 验证和额外的警告。除此之外,还可以减少代码,因为 React 使用 Uglify 的 dead-code 来消除开发代码和注释,这将大大减少包占用的空间。

key 的作用

是给每一个 vnode 的唯一 id,可以依靠 key,更准确,更快的拿到 oldVnode 中对应的 vnode 节点


<!-- 更新前 --><div>  <p key="ka">ka</p>  <h3 key="song">song</he></div>
<!-- 更新后 --><div> <h3 key="song">song</h3> <p key="ka">ka</p></div>
复制代码


如果没有 key,React 会认为 div 的第一个子节点由 p 变成 h3,第二个子节点由 h3 变成 p,则会销毁这两个节点并重新构造。


但是当我们用 key 指明了节点前后对应关系后,React 知道 key === "ka" 的 p 更新后还在,所以可以复用该节点,只需要交换顺序。


key 是 React 用来追踪哪些列表元素被修改、被添加或者被移除的辅助标志。


在开发过程中,我们需要保证某个元素的 key 在其同级元素中具有唯一性。在 React diff 算法中,React 会借助元素的 Key 值来判断该元素是新近创建的还是被移动而来的元素,从而减少不必要的元素重新渲染。同时,React 还需要借助 key 来判断元素与本地状态的关联关系。

调用 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 元素

为什么有些 react 生命周期钩子被标记为 UNSAFE

componentWillMount

componentWillMount 生命周期发生在首次渲染前,一般使用的小伙伴大多在这里初始化数据或异步获取外部数据赋值。初始化数据,react 官方建议放在 constructor 里面。而异步获取外部数据,渲染并不会等待数据返回后再去渲染


class Example extends React.Component {       state = {        value: ''    };    componentWillMount() {            this.setState({                   value: this.props.source.value        });               this.props.source.subscribe(this.handleChange);    }       componentWillUnmount() {            this.props.source.unsubscribe(this.handleChange );     }       handleChange = source => {            this.setState({            value: source.value        });       }; }
复制代码


  • 试想一下,假如组件在第一次渲染的时候被中断,由于组件没有完成渲染,所以并不会执行 componentWillUnmount 生命周期(注:很多人经常认为 componentWillMount 和 componentWillUnmount 总是配对,但这并不是一定的。只有调用 componentDidMount 后,React 才能保证稍后调用 componentWillUnmount 进行清理)。因此 handleSubscriptionChange 还是会在数据返回成功后被执行,这时候 setState 由于组件已经被移除,就会导致内存泄漏。所以建议把异步获取外部数据写在 componentDidMount 生命周期里,这样就能保证 componentWillUnmount 生命周期会在组件移除的时候被执行,避免内存泄漏的风险。

  • 现在,小伙伴清楚为什么了要用UNSAFE_componentWillMount替换componentWillMount了吧

componentWillReceiveProps

componentWillReceiveProps 生命周期是在 props 更新时触发。一般用于 props 参数更新时同步更新 state 参数。但如果在 componentWillReceiveProps 生命周期直接调用父组件的某些有调用 setState 的函数,会导致程序死循环


// 如下是子组件componentWillReceiveProps里调用父组件改变state的函数示例
class Parent extends React.Component{ constructor(){ super(); this.state={ list: [], selectedData: {} }; }
changeSelectData = selectedData => { this.setState({ selectedData }); }
render(){ return ( <Clild list={this.state.list} changeSelectData={this.changeSelectData}/> ); }}
...class Child extends React.Component{ constructor(){ super(); this.state={ list: [] }; } componentWillReceiveProps(nextProps){ this.setState({ list: nextProps.list }) nextProps.changeSelectData(nextProps.list[0]); //默认选择第一个 } ...}
复制代码


  • 如上代码,在 Child 组件的 componentWillReceiveProps 里直接调用 Parent 组件的 changeSelectData 去更新 Parent 组件 state 的 selectedData 值。会触发 Parent 组件重新渲染,而 Parent 组件重新渲染会触发 Child 组件的 componentWillReceiveProps 生命周期函数执行。如此就会陷入死循环。导致程序崩溃。

  • 所以,React 官方把 componentWillReceiveProps 替换为 UNSAFE_componentWillReceiveProps,让小伙伴在使用这个生命周期的时候注意它会有缺陷,要注意避免,比如上面例子,Child 在 componentWillReceiveProps 调用 changeSelectData 时先判断 list 是否有更新再确定是否要调用,就可以避免死循环。

componentWillUpdate

componentWillUpdate 生命周期在视图更新前触发。一般用于视图更新前保存一些数据方便视图更新完成后赋值。 案例三:如下是列表加载更新后回到当前滚动条位置的案例


class ScrollingList extends React.Component {       listRef = null;       previousScrollOffset = null;       componentWillUpdate(nextProps, nextState) {            if (this.props.list.length < nextProps.list.length) {                  this.previousScrollOffset = this.listRef.scrollHeight - this.listRef.scrollTop;            }     }       componentDidUpdate(prevProps, prevState) {            if (this.previousScrollOffset !== null) {                  this.listRef.scrollTop = this.listRef.scrollHeight - this.previousScrollOffset;              this.previousScrollOffset = null;            }       }       render() {            return (                   `<div>` {/* ...contents... */}`</div>`             );       }       setListRef = ref => {    this.listRef = ref;   };
复制代码


  • 由于 componentWillUpdate 和 componentDidUpdate 这两个生命周期函数有一定的时间差(componentWillUpdate 后经过渲染、计算、再更新 DOM 元素,最后才调用 componentDidUpdate),如果这个时间段内用户刚好拉伸了浏览器高度,那 componentWillUpdate 计算的 previousScrollOffset 就不准确了。如果在 componentWillUpdate 进行 setState 操作,会出现多次调用只更新一次的问题,把 setState 放在 componentDidUpdate,能保证每次更新只调用一次。

  • 所以,react 官方建议把 componentWillUpdate 替换为 UNSAFE_componentWillUpdate。如果真的有以上案例的需求,可以使用 16.3 新加入的一个周期函数 getSnapshotBeforeUpdat

结论

  • React 意识到 componentWillMount、componentWillReceiveProps 和 componentWillUpdate 这三个生命周期函数有缺陷,比较容易导致崩溃。但是由于旧的项目已经在用以及有些老开发者习惯用这些生命周期函数,于是通过给它加 UNSAFE_来提醒用它的人要注意它们的缺陷

  • React 加入了两个新的生命周期函数 getSnapshotBeforeUpdate 和 getDerivedStateFromProps,目的为了即使不使用这三个生命周期函数,也能实现只有这三个生命周期能实现的功能

createElement 和 cloneElement 有什么区别?

createElement 是 JSX 被转载得到的,在 React 中用来创建 React 元素(即虚拟 DOM)的内容。cloneElement 用于复制元素并传递新的 props。

概述下 React 中的事件处理逻辑

  • 抹平浏览器差异,实现更好的跨平台。

  • 避免垃圾回收,React 引入事件池,在事件池中获取或释放事件对象,避免频繁地去创建和销毁。

  • 方便事件统一管理和事务机制。


为了解决跨浏览器兼容性问题,React 会将浏览器原生事件(Browser Native Event)封装为合成事件(SyntheticEvent)传入设置的事件处理器中。这里的合成事件提供了与原生事件相同的接口,不过它们屏蔽了底层浏览器的细节差异,保证了行为的一致性。另外有意思的是,React 并没有直接将事件附着到子元素上,而是以单一事件监听器的方式将所有的事件发送到顶层进行处理。这样 React 在更新 DOM 的时候就不需要考虑如何去处理附着在 DOM 上的事件监听器,最终达到优化性能的目的

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}
复制代码


用户头像

beifeng1996

关注

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

还未添加个人简介

评论

发布
暂无评论
腾讯前端经典react面试题(附答案)_React_beifeng1996_InfoQ写作社区