前端一面经典react面试题(边面边更)

react 的虚拟dom是怎么实现的

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

如何配置 React-Router 实现路由切换

(1)使用<Route> 组件

路由匹配是通过比较 <Route> 的 path 属性和当前地址的 pathname 来实现的。当一个 <Route> 匹配成功时,它将渲染其内容,当它不匹配时就会渲染 null。没有路径的 <Route> 将始终被匹配。

// when location = { pathname: '/about' }
<Route path='/about' component={About}/> // renders <About/>
<Route path='/contact' component={Contact}/> // renders null
<Route component={Always}/> // renders <Always/>

(2)结合使用 <Switch> 组件和 <Route> 组件

<Switch> 用于将 <Route> 分组。

<Switch>
    <Route exact path="/" component={Home} />
    <Route path="/about" component={About} />
    <Route path="/contact" component={Contact} />
</Switch>

<Switch> 不是分组 <Route> 所必须的,但他通常很有用。 一个 <Switch> 会遍历其所有的子 <Route>元素,并仅渲染与当前地址匹配的第一个元素。

(3)使用 <Link>、 <NavLink>、<Redirect> 组件

<Link> 组件来在你的应用程序中创建链接。无论你在何处渲染一个<Link> ,都会在应用程序的 HTML 中渲染锚(<a>)。

<Link to="/">Home</Link>   
// <a href='/'>Home</a>

是一种特殊类型的 当它的 to属性与当前地址匹配时,可以将其定义为"活跃的"。

// location = { pathname: '/react' }
<NavLink to="/react" activeClassName="hurray">
    React
</NavLink>
// <a href='/react' className='hurray'>React</a>

当我们想强制导航时,可以渲染一个<Redirect>,当一个<Redirect>渲染时,它将使用它的to属性进行定向。

useEffect和useLayoutEffect的区别

useEffect
基本上90%的情况下,都应该用这个,这个是在render结束后,你的callback函数执行,但是不会block browser painting,算是某种异步的方式吧,但是class的componentDidMount 和componentDidUpdate是同步的,在render结束后就运行,useEffect在大部分场景下都比class的方式性能更好.
useLayoutEffect
这个是用在处理DOM的时候,当你的useEffect里面的操作需要处理DOM,并且会改变页面的样式,就需要用这个,否则可能会出现出现闪屏问题, useLayoutEffect里面的callback函数会在DOM更新完成后立即执行,但是会在浏览器进行任何绘制之前运行完成,阻塞了浏览器的绘制.

对虚拟 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中发起网络请求应该在哪个生命周期中进行?为什么?

对于异步请求,最好放在componentDidMount中去操作,对于同步的状态改变,可以放在componentWillMount中,一般用的比较少。

如果认为在componentWillMount里发起请求能提早获得结果,这种想法其实是错误的,通常componentWillMount比componentDidMount早不了多少微秒,网络上任何一点延迟,这一点差异都可忽略不计。

react的生命周期: constructor() -> componentWillMount() -> render() -> componentDidMount()

上面这些方法的调用是有次序的,由上而下依次调用。

  • constructor被调用是在组件准备要挂载的最开始,此时组件尚未挂载到网页上。
  • componentWillMount方法的调用在constructor之后,在render之前,在这方法里的代码调用setState方法不会触发重新render,所以它一般不会用来作加载数据之用。
  • componentDidMount方法中的代码,是在组件已经完全挂载到网页上才会调用被执行,所以可以保证数据的加载。此外,在这方法中调用setState方法,会触发重新渲染。所以,官方设计这个方法就是用来加载外部数据用的,或处理其他的副作用代码。与组件上的数据无关的加载,也可以在constructor里做,但constructor是做组件state初绐化工作,并不是做加载数据这工作的,constructor里也不能setState,还有加载的时间太长或者出错,页面就无法加载出来。所以有副作用的代码都会集中在componentDidMount方法里。

总结:

  • 跟服务器端渲染(同构)有关系,如果在componentWillMount里面获取数据,fetch data会执行两次,一次在服务器端一次在客户端。在componentDidMount中可以解决这个问题,componentWillMount同样也会render两次。
  • 在componentWillMount中fetch data,数据一定在render后才能到达,如果忘记了设置初始状态,用户体验不好。
  • react16.0以后,componentWillMount可能会被执行多次。

对 React-Intl 的理解,它的工作原理?

React-intl是雅虎的语言国际化开源项目FormatJS的一部分,通过其提供的组件和API可以与ReactJS绑定。

React-intl提供了两种使用方法,一种是引用React组件,另一种是直接调取API,官方更加推荐在React项目中使用前者,只有在无法使用React组件的地方,才应该调用框架提供的API。它提供了一系列的React组件,包括数字格式化、字符串格式化、日期格式化等。

在React-intl中,可以配置不同的语言包,他的工作原理就是根据需要,在语言包之间进行切换。

参考:前端react面试题详细解答

HOC相比 mixins 有什么优点?

HOC 和 Vue 中的 mixins 作用是一致的,并且在早期 React 也是使用 mixins 的方式。但是在使用 class 的方式创建组件以后,mixins 的方式就不能使用了,并且其实 mixins 也是存在一些问题的,比如:

  • 隐含了一些依赖,比如我在组件中写了某个 state 并且在 mixin 中使用了,就这存在了一个依赖关系。万一下次别人要移除它,就得去 mixin 中查找依赖
  • 多个 mixin 中可能存在相同命名的函数,同时代码组件中也不能出现相同命名的函数,否则就是重写了,其实我一直觉得命名真的是一件麻烦事。。
  • 雪球效应,虽然我一个组件还是使用着同一个 mixin,但是一个 mixin 会被多个组件使用,可能会存在需求使得 mixin 修改原本的函数或者新增更多的函数,这样可能就会产生一个维护成本

HOC 解决了这些问题,并且它们达成的效果也是一致的,同时也更加的政治正确(毕竟更加函数式了)。

讲讲什么是 JSX ?

Facebook 第一次发布 React 时,他们还引入了一种新的 JS 方言 JSX,将原始 HTML 模板嵌入到 JS 代码中。JSX 代码本身不能被浏览器读取,必须使用Babelwebpack等工具将其转换为传统的JS。很多开发人员就能无意识使用 JSX,因为它已经与 React 结合在一直了。

class MyComponent extends React.Component {
  render() {
    let props = this.props;
    return (
      <div className="my-component">
        <a href={props.url}>{props.name}</a>
      </div>
    );
  }
}

类组件(Class component)和函数式组件(Functional component)之间有何不同

  • 类组件不仅允许你使用更多额外的功能,如组件自身的状态和生命周期钩子,也能使组件直接访问 store 并维持状态
  • 当组件仅是接收 props,并将组件自身渲染到页面时,该组件就是一个 '无状态组件(stateless component)',可以使用一个纯函数来创建这样的组件。这种组件也被称为哑组件(dumb components)或展示组件

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

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

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

为何React事件要自己绑定this

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

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

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

React中setState的第二个参数作用是什么?

setState 的第二个参数是一个可选的回调函数。这个回调函数将在组件重新渲染后执行。等价于在 componentDidUpdate 生命周期内执行。通常建议使用 componentDidUpdate 来代替此方式。在这个回调函数中你可以拿到更新后 state 的值:

this.setState({
    key1: newState1,
    key2: newState2,
    ...
}, callback) // 第二个参数是 state 更新完成后的回调函数

对有状态组件和无状态组件的理解及使用场景

(1)有状态组件

特点:

  • 是类组件
  • 有继承
  • 可以使用this
  • 可以使用react的生命周期
  • 使用较多,容易频繁触发生命周期钩子函数,影响性能
  • 内部使用 state,维护自身状态的变化,有状态组件根据外部组件传入的 props 和自身的 state进行渲染。

使用场景:

  • 需要使用到状态的。
  • 需要使用状态操作组件的(无状态组件的也可以实现新版本react hooks也可实现)

总结: 类组件可以维护自身的状态变量,即组件的 state ,类组件还有不同的生命周期方法,可以让开发者能够在组件的不同阶段(挂载、更新、卸载),对组件做更多的控制。类组件则既可以充当无状态组件,也可以充当有状态组件。当一个类组件不需要管理自身状态时,也可称为无状态组件。

(2)无状态组件 特点:

  • 不依赖自身的状态state
  • 可以是类组件或者函数组件。
  • 可以完全避免使用 this 关键字。(由于使用的是箭头函数事件无需绑定)
  • 有更高的性能。当不需要使用生命周期钩子时,应该首先使用无状态函数组件
  • 组件内部不维护 state ,只根据外部组件传入的 props 进行渲染的组件,当 props 改变时,组件重新渲染。

使用场景:

  • 组件不需要管理 state,纯展示

优点:

  • 简化代码、专注于 render
  • 组件不需要被实例化,无生命周期,提升性能。 输出(渲染)只取决于输入(属性),无副作用
  • 视图和数据的解耦分离

缺点:

  • 无法使用 ref
  • 无生命周期方法
  • 无法控制组件的重渲染,因为无法使用shouldComponentUpdate 方法,当组件接受到新的属性时则会重渲染

总结: 组件内部状态且与外部无关的组件,可以考虑用状态组件,这样状态树就不会过于复杂,易于理解和管理。当一个组件不需要管理自身状态时,也就是无状态组件,应该优先设计为函数组件。比如自定义的 <Button/><Input /> 等组件。

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

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

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

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

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

子类:

//子类jsx
import 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);
}

如果创建了类似于下面的 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组件都未渲染。

Diff 的瓶颈以及 React 的应对

由于 diff 操作本身会带来性能上的损耗,在 React 文档中提到过,即使最先进的算法中,将前后两棵树完全比对的算法复杂度为O(n3),其中 n 为树中元素的数量。

如果 React 使用了该算法,那么仅仅一千个元素的页面所需要执行的计算量就是十亿的量级,这无疑是无法接受的。

为了降低算法的复杂度,React 的 diff 会预设三个限制:

  1. 只对同级元素进行 diff 比对。如果一个元素节点在前后两次更新中跨越了层级,那么 React 不会尝试复用它
  2. 两个不同类型的元素会产生出不同的树。如果元素由 div 变成 p,React 会销毁 div 及其子孙节点,并新建 p 及其子孙节点
  3. 开发者可以通过 key 来暗示哪些子元素在不同的渲染下能保持稳定

react hooks,它带来了那些便利

  • 代码逻辑聚合,逻辑复用
  • HOC嵌套地狱
  • 代替class

React 中通常使用 类定义 或者 函数定义 创建组件:

在类定义中,我们可以使用到许多 React 特性,例如 state、 各种组件生命周期钩子等,但是在函数定义中,我们却无能为力,因此 React 16.8 版本推出了一个新功能 (React Hooks),通过它,可以更好的在函数定义组件中使用 React 特性。

好处:

  1. 跨组件复用: 其实 render props / HOC 也是为了复用,相比于它们,Hooks 作为官方的底层 API,最为轻量,而且改造成本小,不会影响原来的组件层次结构和传说中的嵌套地狱;

  2. 类定义更为复杂

  • 不同的生命周期会使逻辑变得分散且混乱,不易维护和管理;

  • 时刻需要关注this的指向问题;

  • 代码复用代价高,高阶组件的使用经常会使整个组件树变得臃肿;

  1. 状态与UI隔离: 正是由于 Hooks 的特性,状态逻辑会变成更小的粒度,并且极容易被抽象成一个自定义 Hooks,组件中的状态和 UI 变得更为清晰和隔离。

注意:

  • 避免在 循环/条件判断/嵌套函数 中调用 hooks,保证调用顺序的稳定;
  • 只有 函数定义组件 和 hooks 可以调用 hooks,避免在 类组件 或者 普通函数 中调用;
  • 不能在useEffect中使用useState,React 会报错提示;
  • 类组件不会被替换或废弃,不需要强制改造类组件,两种方式能并存;

重要钩子

  1. 状态钩子 (useState): 用于定义组件的 State,其到类定义中this.state的功能;
// useState 只接受一个参数: 初始状态
// 返回的是组件名和更改该组件对应的函数
const [flag, setFlag] = useState(true);
// 修改状态
setFlag(false)

// 上面的代码映射到类定义中:
this.state = {
    flag: true    
}
const flag = this.state.flag
const setFlag = (bool) => {
    this.setState({
        flag: bool,
    })
}
  1. 生命周期钩子 (useEffect):

类定义中有许多生命周期函数,而在 React Hooks 中也提供了一个相应的函数 (useEffect),这里可以看做componentDidMount、componentDidUpdate和componentWillUnmount的结合。

useEffect(callback, [source])接受两个参数

  • callback: 钩子回调函数;
  • source: 设置触发条件,仅当 source 发生改变时才会触发;
  • useEffect钩子在没有传入[source]参数时,默认在每次 render 时都会优先调用上次保存的回调中返回的函数,后再重新调用回调;
useEffect(() => {
    // 组件挂载后执行事件绑定
    console.log('on')
    addEventListener()

    // 组件 update 时会执行事件解绑
    return () => {
        console.log('off')
        removeEventListener()
    }
}, [source]);


// 每次 source 发生改变时,执行结果(以类定义的生命周期,便于大家理解):
// --- DidMount ---
// 'on'
// --- DidUpdate ---
// 'off'
// 'on'
// --- DidUpdate ---
// 'off'
// 'on'
// --- WillUnmount --- 
// 'off'

通过第二个参数,我们便可模拟出几个常用的生命周期:

  • componentDidMount: 传入[]时,就只会在初始化时调用一次
const useMount = (fn) => useEffect(fn, [])
  • componentWillUnmount: 传入[],回调中的返回的函数也只会被最终执行一次
const useUnmount = (fn) => useEffect(() => fn, [])
  • mounted: 可以使用 useState 封装成一个高度可复用的 mounted 状态;
const useMounted = () => {
    const [mounted, setMounted] = useState(false);
    useEffect(() => {
        !mounted && setMounted(true);
        return () => setMounted(false);
    }, []);
    return mounted;
}
  • componentDidUpdate: useEffect每次均会执行,其实就是排除了 DidMount 后即可;
const mounted = useMounted() 
useEffect(() => {
    mounted && fn()
})
  1. 其它内置钩子:
  • useContext: 获取 context 对象

  • useReducer: 类似于 Redux 思想的实现,但其并不足以替代 Redux,可以理解成一个组件内部的 redux:

    • 并不是持久化存储,会随着组件被销毁而销毁;
    • 属于组件内部,各个组件是相互隔离的,单纯用它并无法共享数据;
    • 配合useContext`的全局性,可以完成一个轻量级的 Redux;(easy-peasy)
  • useCallback: 缓存回调函数,避免传入的回调每次都是新的函数实例而导致依赖组件重新渲染,具有性能优化的效果;

  • useMemo: 用于缓存传入的 props,避免依赖的组件每次都重新渲染;

  • useRef: 获取组件的真实节点;

  • useLayoutEffect

    • DOM更新同步钩子。用法与useEffect类似,只是区别于执行时间点的不同
    • useEffect属于异步执行,并不会等待 DOM 真正渲染后执行,而useLayoutEffect则会真正渲染后才触发;
    • 可以获取更新后的 state;
  1. 自定义钩子(useXxxxx): 基于 Hooks 可以引用其它 Hooks 这个特性,我们可以编写自定义钩子,如上面的useMounted。又例如,我们需要每个页面自定义标题:
function useTitle(title) {
  useEffect(
    () => {
      document.title = title;
    });
}

// 使用:
function Home() {
    const title = '我是首页'
    useTitle(title)

    return (
        <div>{title}</div>
    )
}

createElement和 cloneElement有什么区别?

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

posted @ 2022-11-18 09:18  beifeng11996  阅读(98)  评论(0编辑  收藏  举报