写点什么

前端二面经典 react 面试题

作者:beifeng1996
  • 2023-02-14
    浙江
  • 本文字数:8805 字

    阅读完需:约 29 分钟

如何解决 props 层级过深的问题

  • 使用 Context API:提供一种组件之间的状态共享,而不必通过显式组件树逐层传递 props;

  • 使用 Redux 等状态库。

react 实现一个全局的 dialog

import React, { Component } from 'react';import { is, fromJS } from 'immutable';import ReactDOM from 'react-dom';import ReactCSSTransitionGroup from 'react-addons-css-transition-group';import './dialog.css';let defaultState = {  alertStatus:false,  alertTip:"提示",  closeDialog:function(){},  childs:''}class Dialog extends Component{  state = {    ...defaultState  };  // css动画组件设置为目标组件  FirstChild = props => {    const childrenArray = React.Children.toArray(props.children);    return childrenArray[0] || null;  }  //打开弹窗  open =(options)=>{    options = options || {};    options.alertStatus = true;    var props = options.props || {};    var childs = this.renderChildren(props,options.childrens) || '';    console.log(childs);    this.setState({      ...defaultState,      ...options,      childs    })  }  //关闭弹窗  close(){    this.state.closeDialog();    this.setState({      ...defaultState    })  }  renderChildren(props,childrens) {    //遍历所有子组件    var childs = [];    childrens = childrens || [];    var ps = {        ...props,  //给子组件绑定props        _close:this.close  //给子组件也绑定一个关闭弹窗的事件           };    childrens.forEach((currentItem,index) => {        childs.push(React.createElement(            currentItem,            {                ...ps,                key:index            }        ));    })    return childs;  }  shouldComponentUpdate(nextProps, nextState){    return !is(fromJS(this.props), fromJS(nextProps)) || !is(fromJS(this.state), fromJS(nextState))  }     render(){    return (      <ReactCSSTransitionGroup        component={this.FirstChild}        transitionName='hide'        transitionEnterTimeout={300}        transitionLeaveTimeout={300}>        <div className="dialog-con" style={this.state.alertStatus? {display:'block'}:{display:'none'}}>            {this.state.childs}        </div>      </ReactCSSTransitionGroup>    );  }}let div = document.createElement('div');let props = {   };document.body.appendChild(div);let Box = ReactD
复制代码


子类:


//子类jsximport React, { Component } from 'react';class Child extends Component {    constructor(props){        super(props);        this.state = {date: new Date()};  }  showValue=()=>{    this.props.showValue && this.props.showValue()  }  render() {    return (      <div className="Child">        <div className="content">           Child           <button onClick={this.showValue}>调用父的方法</button>        </div>      </div>    );  }}export default Child;
复制代码


css:


.dialog-con{    position: fixed;    top: 0;    left: 0;    width: 100%;    height: 100%;    background: rgba(0, 0, 0, 0.3);}
复制代码

使用状态要注意哪些事情?

要注意以下几点。


  • 不要直接更新状态

  • 状态更新可能是异步的

  • 状态更新要合并。

  • 数据从上向下流动

跨级组件的通信方式?

父组件向子组件的子组件通信,向更深层子组件通信:


  • 使用 props,利用中间组件层层传递,但是如果父组件结构较深,那么中间每一层组件都要去传递 props,增加了复杂度,并且这些 props 并不是中间组件自己需要的。

  • 使用 context,context 相当于一个大容器,可以把要通信的内容放在这个容器中,这样不管嵌套多深,都可以随意取用,对于跨越多层的全局数据可以使用 context 实现。


// context方式实现跨级组件通信 // Context 设计目的是为了共享那些对于一个组件树而言是“全局”的数据const BatteryContext = createContext();//  子组件的子组件 class GrandChild extends Component {    render(){        return (            <BatteryContext.Consumer>                {                    color => <h1 style={{"color":color}}>我是红色的:{color}</h1>                }            </BatteryContext.Consumer>        )    }}//  子组件const Child = () =>{    return (        <GrandChild/>    )}// 父组件class Parent extends Component {      state = {          color:"red"      }      render(){          const {color} = this.state          return (          <BatteryContext.Provider value={color}>              <Child></Child>          </BatteryContext.Provider>          )      }}
复制代码

React 16 中新生命周期有哪些

关于 React16 开始应用的新生命周期: 可以看出,React16 自上而下地对生命周期做了另一种维度的解读:


  • Render 阶段:用于计算一些必要的状态信息。这个阶段可能会被 React 暂停,这一点和 React16 引入的 Fiber 架构(我们后面会重点讲解)是有关的;

  • Pre-commit 阶段:所谓“commit”,这里指的是“更新真正的 DOM 节点”这个动作。所谓 Pre-commit,就是说我在这个阶段其实还并没有去更新真实的 DOM,不过 DOM 信息已经是可以读取的了;

  • Commit 阶段:在这一步,React 会完成真实 DOM 的更新工作。Commit 阶段,我们可以拿到真实 DOM(包括 refs)。


与此同时,新的生命周期在流程方面,仍然遵循“挂载”、“更新”、“卸载”这三个广义的划分方式。它们分别对应到:


  • 挂载过程:

  • constructor

  • getDerivedStateFromProps

  • render

  • componentDidMount

  • 更新过程:

  • getDerivedStateFromProps

  • shouldComponentUpdate

  • render

  • getSnapshotBeforeUpdate

  • componentDidUpdate

  • 卸载过程:

  • componentWillUnmount

React 事件机制

<div onClick={this.handleClick.bind(this)}>点我</div>
复制代码


React 并不是将 click 事件绑定到了 div 的真实 DOM 上,而是在 document 处监听了所有的事件,当事件发生并且冒泡到 document 处的时候,React 将事件内容封装并交由真正的处理函数运行。这样的方式不仅仅减少了内存的消耗,还能在组件挂在销毁时统一订阅和移除事件。


除此之外,冒泡到 document 上的事件也不是原生的浏览器事件,而是由 react 自己实现的合成事件(SyntheticEvent)。因此如果不想要是事件冒泡的话应该调用 event.preventDefault()方法,而不是调用 event.stopProppagation()方法。 JSX 上写的事件并没有绑定在对应的真实 DOM 上,而是通过事件代理的方式,将所有的事件都统一绑定在了 document 上。这样的方式不仅减少了内存消耗,还能在组件挂载销毁时统一订阅和移除事件。


另外冒泡到 document 上的事件也不是原生浏览器事件,而是 React 自己实现的合成事件(SyntheticEvent)。因此我们如果不想要事件冒泡的话,调用 event.stopPropagation 是无效的,而应该调用 event.preventDefault


实现合成事件的目的如下:


  • 合成事件首先抹平了浏览器之间的兼容问题,另外这是一个跨浏览器原生事件包装器,赋予了跨浏览器开发的能力;

  • 对于原生浏览器事件来说,浏览器会给监听器创建一个事件对象。如果你有很多的事件监听,那么就需要分配很多的事件对象,造成高额的内存分配问题。但是对于合成事件来说,有一个事件池专门来管理它们的创建和销毁,当事件需要被使用时,就会从池子中复用对象,事件回调结束后,就会销毁事件对象上的属性,从而便于下次复用事件对象。


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

React setState 调用之后发生了什么?是同步还是异步?

(1)React 中 setState 后发生了什么


在代码中调用 setState 函数之后,React 会将传入的参数对象与组件当前的状态合并,然后触发调和过程(Reconciliation)。经过调和过程,React 会以相对高效的方式根据新的状态构建 React 元素树并且着手重新渲染整个 UI 界面。


在 React 得到元素树之后,React 会自动计算出新的树与老树的节点差异,然后根据差异对界面进行最小化重渲染。在差异计算算法中,React 能够相对精确地知道哪些位置发生了改变以及应该如何改变,这就保证了按需更新,而不是全部重新渲染。


如果在短时间内频繁 setState。React 会将 state 的改变压入栈中,在合适的时机,批量更新 state 和视图,达到提高性能的效果。


(2)setState 是同步还是异步的


假如所有 setState 是同步的,意味着每执行一次 setState 时(有可能一个同步代码中,多次 setState),都重新 vnode diff + dom 修改,这对性能来说是极为不好的。如果是异步,则可以把一个同步代码中的多个 setState 合并成一次组件更新。所以默认是异步的,但是在一些情况下是同步的。


setState 并不是单纯同步/异步的,它的表现会因调用场景的不同而不同。在源码中,通过 isBatchingUpdates 来判断 setState 是先存进 state 队列还是直接更新,如果值为 true 则执行异步操作,为 false 则直接更新。


  • 异步: 在 React 可以控制的地方,就为 true,比如在 React 生命周期事件和合成事件中,都会走合并操作,延迟更新的策略。

  • 同步: 在 React 无法控制的地方,比如原生事件,具体就是在 addEventListener 、setTimeout、setInterval 等事件中,就只能同步更新。


一般认为,做异步设计是为了性能优化、减少渲染次数:


  • setState设计为异步,可以显著的提升性能。如果每次调用 setState都进行一次更新,那么意味着render函数会被频繁调用,界面重新渲染,这样效率是很低的;最好的办法应该是获取到多个更新,之后进行批量更新;

  • 如果同步更新了state,但是还没有执行render函数,那么stateprops不能保持同步。stateprops不能保持一致性,会在开发中产生很多的问题;

react16 版本的 reconciliation 阶段和 commit 阶段是什么

  • reconciliation 阶段包含的主要工作是对 current tree 和 new tree 做 diff 计算,找出变化部分。进行遍历、对比等是可以中断,歇一会儿接着再来。

  • commit 阶段是对上一阶段获取到的变化部分应用到真实的 DOM 树中,是一系列的 DOM 操作。不仅要维护更复杂的 DOM 状态,而且中断后再继续,会对用户体验造成影响。在普遍的应用场景下,此阶段的耗时比 diff 计算等耗时相对短。

用户不同权限 可以查看不同的页面 如何实现?

  1. Js 方式

  2. 根据用户权限类型,把菜单配置成 json, 没有权限的直接不显示

  3. react-router 方式 在 route 标签上 添加 onEnter 事件,进入路由之前替换到首页


<Route path="/home" component={App} onEnter={(nexState,replace)=>{      if(nexState.location.pathname!=='/'){         var  sid = UtilsMoudle.getSidFromUrl(nexState);         if(!sid){            replace("/")         }else{            console.log(sid);         }      }    }}>
复制代码


  1. 自己封装一个 privateRouter 组件 里面判断是否有权限,有的话返回

  2. 没有权限的话 component 返回一个提示信息的组件。

  3. 扩展一下,如果是根据用权限来判断是否隐藏组件该怎么做呢?

  4. react 可以使用高阶组件,在高阶组件里面判断是否有权限,然后判断是否返回组件,无权限返回 null

  5. vue 可以使用自定义指令,如果没有权限移除组件


// 需要在入口处添加自定义权限指令v-auth,显示可操作组件Vue.directive('auth', {    bind: function (el, binding, vnode) {        // 用户权限表        const rules = auths        for (let i = 0; i < rules.length; i++) {            const item = rules[i]            if(!binding.value || (binding.value == item.auth)){                // 权限允许则显示组件                return true            }        }        // 移除组件        el.parentNode.removeChild(el)    }})// 使用<template>  <div>    <Button v-auth="admin_user_add">添加用户</Button>    <Button v-auth="admin_user_del">删除用户</Button>    <Button v-auth="admin_user_edit">编辑用户</Button>  </div></template>
复制代码

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 及其子组件的重新渲染

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

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


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

fetch 封装

npm install whatwg-fetch --save  // 适配其他浏览器npm install es6-promise
export const handleResponse = (response) => { if (response.status === 403 || response.status === 401) { const oauthurl = response.headers.get('locationUrl'); if (!_.isEmpty(oauthUrl)) { window.location.href = oauthurl; return; } } if (!response.ok) { return getErrorMessage(response).then(errorMessage => apiError(response.status, errorMessage)); } if (isJson(response)) { return response.json(); } if (isText(response)) { return response.text(); }
return response.blob();};
const httpRequest = { request: ({ method, headers, body, path, query, }) => { const options = {}; let url = path; if (method) { options.method = method; } if (headers) { options.headers = {...options.headers,...headers}; } if (body) { options.body = body; } if (query) { const params = Object.keys(query) .map(k => `${k}=${query[k]}`) .join('&'); url = url.concat(`?${params}`); } return fetch(url, Object.assign({}, options, { credentials: 'same-origin' })).then(handleResponse); },};
export default httpRequest;
复制代码

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

react 中的 Portal 是什么?

Portals 提供了一种很好的将子节点渲染到父组件以外的 DOM 节点的方式。


第一个参数(child)是任何可渲染的 React 子元素,例如一个元素,字符串或碎片。


第二个参数(container)则是一个 DOM 元素。


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

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

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


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

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

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

  • render:渲染组件。

  • componentDidMount:组件构建完成


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


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

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

  • componnent WillUpdate:组件即将更新。

  • render:渲染组件。

  • componentDidUpdate:组件更新完成。


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

在 Reducer 文件里,对于返回的结果,要注意哪些问题?

在 Reducer 文件里,对于返回的结果,必须要使用 Object.assign ( )来复制一份新的 state,否则页面不会跟着数据刷新。


return Object.assign({}, state, {  type: action.type,  shouldNotPaint: true,});
复制代码

什么是 React Context?

Context 通过组件树提供了一个传递数据的方法,从而避免了在每一个层级手动的传递 props 属性。

对虚拟 DOM 的理解?虚拟 DOM 主要做了什么?虚拟 DOM 本身是什么?

从本质上来说,Virtual Dom 是一个 JavaScript 对象,通过对象的方式来表示 DOM 结构。将页面的状态抽象为 JS 对象的形式,配合不同的渲染工具,使跨平台渲染成为可能。通过事务处理机制,将多次 DOM 修改的结果一次性的更新到页面上,从而有效的减少页面渲染的次数,减少修改 DOM 的重绘重排次数,提高渲染性能。


虚拟 DOM 是对 DOM 的抽象,这个对象是更加轻量级的对 DOM 的描述。它设计的最初目的,就是更好的跨平台,比如 node.js 就没有 DOM,如果想实现 SSR,那么一个方式就是借助虚拟 dom,因为虚拟 dom 本身是 js 对象。 在代码渲染到页面之前,vue 或者 react 会把代码转换成一个对象(虚拟 DOM)。以对象的形式来描述真实 dom 结构,最终渲染到页面。在每次数据发生变化前,虚拟 dom 都会缓存一份,变化之时,现在的虚拟 dom 会与缓存的虚拟 dom 进行比较。在 vue 或者 react 内部封装了 diff 算法,通过这个算法来进行比较,渲染时修改改变的变化,原先没有发生改变的通过原先的数据进行渲染。


另外现代前端框架的一个基本要求就是无须手动操作 DOM,一方面是因为手动操作 DOM 无法保证程序性能,多人协作的项目中如果 review 不严格,可能会有开发者写出性能较低的代码,另一方面更重要的是省略手动 DOM 操作可以大大提高开发效率。


为什么要用 Virtual DOM:


(1)保证性能下限,在不进行手动优化的情况下,提供过得去的性能


下面对比一下修改 DOM 时真实 DOM 操作和 Virtual DOM 的过程,来看一下它们重排重绘的性能消耗∶


  • 真实 DOM∶ 生成 HTML 字符串+ 重建所有的 DOM 元素

  • Virtual DOM∶ 生成 vNode+ DOMDiff+必要的 DOM 更新


Virtual DOM 的更新 DOM 的准备工作耗费更多的时间,也就是 JS 层面,相比于更多的 DOM 操作它的消费是极其便宜的。尤雨溪在社区论坛中说道∶ 框架给你的保证是,你不需要手动优化的情况下,我依然可以给你提供过得去的性能。 (2)跨平台 Virtual DOM 本质上是 JavaScript 的对象,它可以很方便的跨平台操作,比如服务端渲染、uniapp 等。

哪些方法会触发 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 组件添加属性?

对于某些属性,React 非常聪明,如果传递给它的值是虚值,可以省略该属性。例如:


var InputComponent = React.createClass({  render: function () {    var required = true;    var disabled = false;    return <input type="text" disabled={disabled} required={required} />;  },});
复制代码


渲染结果:


<input type="text" required>
复制代码


另一种可能的方法是:


var condition = true;var component = <div value="foo" {...(condition && { disabled: true })} />;
复制代码


用户头像

beifeng1996

关注

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

还未添加个人简介

评论

发布
暂无评论
前端二面经典react面试题_React_beifeng1996_InfoQ写作社区