react常见面试题总结

react核心思想

简单来讲,就是virtual dom & react diff。
咱们都知道在前端开发中,js运行很快,dom操做很慢,而react充分利用了这个前提。在react中render的执行结果是树形结构的javascript对象,当数据(state || props)发生变化时,会生成一个新的树形结构的javascript对象,这两个javascript对象咱们能够称之为virtual dom。而后对比两个virtual dom,找出最小的有变化的点,这个对比的过程咱们称之为react diff,将这个变化的部分(patch)加入到一个队列中,最终批量更新这些patch到dom中。javascript

react执行render和setState进行渲染时主要有两个阶段

  • 调度阶段(Reconciler):React 会自顶向下经过递归, 用新数据生成一颗新树,遍历虚拟dom,diff新老virtual dom树,搜集具体的UI差别,找到须要更新的元素(Patch),放到更新队列中。
  • 渲染阶段(Renderer):遍历更新队列,经过调用宿主环境的API(好比 DOM、Native、WebGL)实际更新渲染对应元素。

引入虚拟dom的好处是什么?

  • js运行很快,dom操做很慢。配合react diff算法,经过对比virtual Dom,能够快速找出真实dom的最小变化,这样前端实际上是不须要去关注那个变化的点,把这个变化交给react来作就好,同时你也没必要本身去完成属性操做、事件处理、DOM更新,React会替你完成这一切,这让咱们更关注咱们的业务逻辑而非DOM操做,基于以上两点可大大提高咱们的开发效率。
  • 跨浏览器、跨平台兼容。react基于virtual dom本身实现了一套本身的事件机制,本身模拟了事件冒泡和捕获的过程,采用了事件代理,批量更新等方法,抹平了各个浏览器的事件兼容性问题。跨平台virtual dom为React带来了跨平台渲染的能力。以React Native为例子。React根据virtual dom画出相应平台的ui层,只不过不一样平台画的姿式不一样而已。

react对性能的提高

关于提高性能,不少人说virtual dom能够提高性能,这一说法其实是很片面的。由于咱们知道,直接操做dom是很是耗费性能的,可是即便咱们用了react,最终依然要去操做真实的dom。而react帮咱们作的事情就是尽可能用最佳的方式有操做dom。若是是首次渲染,virtual dom不具备任何优点,甚至它要进行更多的计算,消耗更多的内存。
react自己的优点在于react diff算法和批处理策略。react在页面更新以前,提早计算好了如何进行更新和渲染DOM,实际上,这个计算过程咱们在直接操做DOM时,也是能够本身判断和实现的,可是必定会耗费很是多的精力和时间,并且每每咱们本身作的是不如React好的。因此,在这个过程当中React帮助咱们"提高了性能"。
因此,我更倾向于说,virtual dom帮助咱们提升了开发效率,在重复渲染时它帮助咱们计算如何更高效的更新,而不是它比DOM操做更快。html

什么是jsx?

咱们在实现一个React组件时能够选择两种编码方式,第一种是使用JSX编写,第二种是直接使用React.createElement编写。实际上,上面两种写法是等价的,jsx只是为React.createElemen方法的语法糖,最终全部的jsx都会被babel转换成React.createElement。
可是请注意,babel在编译时会判断jsx中组件的首字母,当首字母为小写时,其被认定为原生dom标签,createElement的第一个变量被编译为字符串。当首字母为大写时,其被认定为自定义组件,createElement的第一个变量被编译为对象。前端

react的生命周期是怎样的?

在react16中,废弃了三个will属性componentWillMount,componentWillReceiveProps,comonentWillUpdate,可是目前还未删除,react17计划会删除,同时经过UNSAFF_前缀向前兼容。
在 React 中,咱们能够将其生命周期分为三个阶段。java

挂载阶段

  • constructor()
    组件在挂载前,会调用它的构造函数,在构造函数内部必须执行一次super(props),不然不能在constructor内部使用this,constructor一般用于给this.state初始化内部状态,为事件处理函数绑定this。
  • static getDerivedStateFromProps(newProps,prevState)
    是一个静态方法,父组件传入的newProps和当前组件的prevState进行比较,判断时须要更新state,返回值用做更新state,若是不须要则返回null。在render()方法以前调用,而且在初始挂载和后续更新时调用。
  • render()
    render()是组件中惟一必须实现的方法。须要返回如下类型,React元素、数组、fragments、Portals、字符串或者、值类型、布尔类型或null。同时render函数应该是纯函数。不可以调用setState。
  • componentDidMount()

更新阶段

  • static getDerivedStateFromProps(props,state)
  • shouldComponentUpate()
    当props或者state发生变化时,会在渲染前调用。根据父组件的props和当前的state进行对比,返回true/false。决定是否触发后续的 UNSAFE_componentWillUpdate(),render()和componentDidUpdate()。。
  • render()
  • getSnapshotBeforeUpdate(prevProps,prevSteate)
    在render()以后componentDidUpdate()以前调用。此方法的返回值(snaphot)可做为componentDidUpdate()的第三个参数使用。如不须要返回值则直接返回null。
  • componentDidUpdate(prevProps, prevState, snapshot)
    该方法会在更新完成后当即调用。首次渲染不会执行此方法,当组件更新后,能够在此处对dom进行操做。能够在此阶段使用setState,触发render()但必须包裹在一个条件语句里,以免死循环。

卸载阶段

  • componentWillUnmount()
    会在组件卸载和销毁以前直接调用。此方法主要用来执行一些清理工做,例如:定时器,清除事件绑定,取消网络请求。此阶段不能调用setState,由于组件永远不会从新渲染。

react diff解决什么问题?是怎样的实现思路?

react diff会帮助咱们计算出virtual dom中真正变化的部分,并只针对该部分进行实际dom操做,而非从新渲染整个页面,从而保证了每次操做更新后页面的高效渲染。传统diff算法经过循环递归对节点进行依次对比,效率低下,算法复杂度达到 O(n^3)。react diff基于一下三个策略实现了O(n)的算法复杂度。react

  • Web UI中dom节点跨层级的移动操做特别少,能够忽略不计。
  • 拥有相同类的两个组件将会生成类似的树形结构,拥有不一样类的两个组件将会生成不一样的树形结构。
  • 对于同一层级的一组子节点,它们能够经过惟一id进行区分。

基于以上三个前提策略,React分别对tree diff、component diff以及element diff 进行算法优化,事实也证实这三个前提策略是合理且准确的,它保证了总体界面构建的性能。git

react中key的做用,能不能用index做为Key。

首先说一下element diff的过程。好比有老的集合(A,B,C,D)和新的集合(B,A,D,C),咱们考虑在不增长空间复杂度的状况下如何以O(n)的时间复杂度找出老集合中须要移动的元素。
在react里的思路是这样的,遍历新集合,初始化lastIndex=0(表明访问过的老集合中最右侧的位置),表达式为max(prev.mountIndex, lastIndex),若是当前节点在老集合中的位置即(prev.mountIndex)比lastIndex大说明当前访问节点在老集合中就比上一个节点位置靠后则该节点不会影响其余节点的位置,所以不用添加到差别队列中,即不执行移动操做,只有当访问的节点比 lastIndex 小时,才须要进行移动操做。
部分源码为github

var lastIndex = 0;
var nextIndex = 0;
for (name in nextChildren) {
    var prevChild = prevChildren && prevChildren[name]; // 老节点
    var nextChild = nextChildren[name]; // 新节点
    if (prevChild === nextChild) { // 若是新节点存在老节点集合里
      // 移动节点
      this.moveChild(prevChild, nextIndex, lastIndex);
      lastIndex = Math.max(prevChild._mountIndex, lastIndex);
      prevChild._mountIndex = nextIndex;
    } else {
      if (prevChild) { // 若是不存在在
        lastIndex = Math.max(prevChild._mountIndex, lastIndex);
        // 删除节点
        this._unmountChild(prevChild);
      }
      // 初始化并建立节点
      this._mountChildAtIndex(
        nextChild, nextIndex, transaction, context
      );
    }
    nextIndex++;
}

// 移动节点
moveChild: function(child, toIndex, lastIndex) {
  if (child._mountIndex < lastIndex) {
    this.prepareToManageChildren();
    enqueueMove(this, child._mountIndex, toIndex);
  }
}

React 16有哪些新特性?

  • render支持返回数组和字符串
  • Error Boundaries
  • createPortal
  • rollup减少文件体积
  • fiber
  • Fragment
  • createRef
  • Strict Mode

React Fiber是什么?解决什么问题?

React Fiber是React对核心算法的一次从新实现。
在协调阶段阶段,之前因为是采用的递归的遍历方式,这种也被称为Stack Reconciler,主要是为了区别Fiber Reconciler取的一个名字。这种方式有一个特色: 一旦任务开始进行,就没法中断,那么js将一直占用主线程,一直要等到整棵virtual dom树计算完成以后,才能把执行权交给渲染引擎,那么这就会致使一些用户交互、动画等任务没法当即获得处理,就会有卡顿,很是的影响用户体验。
页面是一帧一帧绘制出来的,当每秒绘制的帧数(FPS)达到60时,页面是流畅的,小于这个值时,用户会感受到卡顿。1秒60帧,因此每一帧分到的时间是1000/60 ≈ 16ms。因此咱们书写代码时力求不让一帧的工做量超过 16ms。若是任意一个步骤所占用的时间过长,超过16ms了以后,用户就能看到卡顿。web

Fiber如何实现

简单来讲就是时间分片 + 链表结构。而fiber就是维护每个分片的数据结构。
Fiber利用分片的思想,把一个耗时长的任务分红不少小片,每个小片的运行时间很短,在每一个小片执行完以后,就把控制权交还给React负责任务协调的模块,若是有紧急任务就去优先处理,若是没有就继续更新,这样就给其余任务一个执行的机会,惟一的线程就不会一直被独占。
所以,在组件更新时有可能一个更新任务尚未完成,就被另外一个更高优先级的更新过程打断,优先级高的更新任务会优先处理完,而低优先级更新任务所作的工做则会彻底做废,而后等待机会重头再来。因此 React Fiber把一个更新过程分为两个阶段:算法

  • 第一个阶段 Reconciliation Phase,Fiber会找出须要更新的DOM,这个阶段是能够被打断的。
  • 第二个阶段 Commit Phase,是没法别打断,完成dom的更新并展现。

什么是高阶组件

高阶组件(HOC)是React中用于复用组件逻辑的一种高级技巧。HOC自身不是React API的一部分,它是一种基于 React 的组合特性而造成的设计模式。具体而言,高阶组件是参数为组件,返回值为新组件的函数。
请注意,HOC 不会修改传入的组件,也不会使用继承来复制其行为。相反,HOC 经过将组件包装在容器组件中来组成新组件。HOC 是纯函数,没有反作用。
我理解的高阶组件是,将组件以参数的方式传递给另一个函数,在该函数中,对组件进行包装,封装了一些公用的组件逻辑,实现组件的逻辑复用,该函数被称为高阶组件。可是请注意,高阶组件不该修改传入的组件行为。
属性代理segmentfault

function ppHOC(WrappedComponent) {
  return class PP extends React.Component {
    render() {
      const newProps = {
        user: currentLoggedInUser
      }
      return <WrappedComponent {...this.props} {...newProps}/>
    }
  }
}

反向继承

function hoc(ComponentClass) {
    return class HOC extends ComponentClass {
        render() {
            if (this.state.success) {
                return super.render()
            }
            return <div>Loading...</div>
        }
    }
}

export default class ComponentClass extends Component {
    state = {
        success: false,
        data: null
    };
    async componentDidMount() {
        const result = await fetch(...请求);          
     this.setState({
            success: true,
            data: result.data
        });
    }
    render() {
        return <div>主要内容</div>
    }
}

什么是渲染属性

术语 “render prop” 是指一种技术,用于使用一个值为函数的 prop 在 React 组件之间的代码共享。
带有渲染属性(Render Props)的组件须要一个返回 React 元素并调用它的函数,而不是实现本身的渲染逻辑。
我理解的渲染属性是,提供渲染页面的props给子组件,共享能够共享子组件的状态,复用子组件的状态,并告诉子组件如何进行渲染。

import React from 'react'
import ReactDOM from 'react-dom'
import PropTypes from 'prop-types'
// 与 HOC 不一样,咱们可使用具备 render prop 的普通组件来共享代码
class Mouse extends React.Component {
  static propTypes = {
    render: PropTypes.func.isRequired
  }
  state = { x: 0, y: 0 }
  handleMouseMove = (event) => {
    this.setState({
      x: event.clientX,
      y: event.clientY
    })
  }
  render() {
    return (
      <div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}>
        {this.props.render(this.state)}
      </div>
    )
  }
}
const App = React.createClass({
  render() {
    return (
      <div style={{ height: '100%' }}>
        <Mouse render={({ x, y }) => (
          // render prop 给了咱们所须要的 state 来渲染咱们想要的
          <h1>The mouse position is ({x}, {y})</h1>
        )}/>
      </div>
    )
  }
})
ReactDOM.render(<App/>, document.getElementById('app'))

什么是React Hooks,它是为了解决什么问题?说一下它的实现原理!

React Hooks 是 React 16.7.0-alpha 版本推出的新特性,它可让你在不编写class的状况下使用state以及其余的 React特性。React Hooks要解决的问题是状态共享,是继render-props和hoc以后的第三种状态共享方案,不会产生JSX嵌套地狱问题。这个状态指的是状态逻辑,因此称为状态逻辑复用会更恰当,由于只共享数据处理逻辑,不会共享数据自己。

简单实现

let memoizedState = []; // hooks 存放在这个数组
let cursor = 0; // 当前 memoizedState 下标

function useState(initialValue) {
  memoizedState[cursor] = memoizedState[cursor] || initialValue;
  const currentCursor = cursor;
  function setState(newState) {
    memoizedState[currentCursor] = newState;
    render();
  }
  return [memoizedState[cursor++], setState]; // 返回当前 state,并把 cursor 加 1
}

function useEffect(callback, depArray) {
  const hasNoDeps = !depArray;
  const deps = memoizedState[cursor];
  const hasChangedDeps = deps
    ? !depArray.every((el, i) => el === deps[i])
    : true;
  if (hasNoDeps || hasChangedDeps) {
    callback();
    memoizedState[cursor] = depArray;
  }
  cursor++;
}

React为何要在构造函数中调用super(props),为何要bind(this)?

super表明父类的构造函数,javascript规定若是子类不调用super是不容许在子类中使用this的,这不是React的限制,而是javaScript的限制,同时你也必须给super传入props,不然React.Component就无法初始化this.props
在 React 的类组件中,当咱们把事件处理函数引用做为回调传递过去,事件处理程序方法会丢失其隐式绑定的上下文。当事件被触发而且处理程序被调用时,this的值会回退到默认绑定,即值为 undefined,这是由于类声明和原型方法是以严格模式运行。

说一下react事件机制?

react为何要用本身的事件机制

  • 减小内存消耗,提高性能,不须要注册那么多的事件了,一种事件类型只在document上注册一次。
  • 统一规范,解决 ie 事件兼容问题,简化事件逻辑。
  • 对开发者友好。

react的合成事件

SyntheticEvent是react合成事件的基类,定义了合成事件的基础公共属性和方法。react会根据当前的事件类型来使用不一样的合成事件对象,好比鼠标单机事件 - SyntheticMouseEvent,焦点事件-SyntheticFocusEvent等,可是都是继承自SyntheticEvent。在合成事件中主要作了如下三件事情。

  • 对原生事件的封装
  • 对某些原生事件的升级和改造
  • 不一样浏览器事件兼容的处理

事件注册

组件挂载阶段,根据组件内的声明的事件类型-onclick,onchange等,给document上添加事件addEventListener,并指定统一的事件处理程序dispatchEvent。
经过virtual dom的props属性拿到要注册的事件名,回调函数,经过listenTo方法使用原生的addEventListener进行事件绑定。

事件存储

事件存储,就是把react组件内的全部事件统一的存放到一个二级map对象里,缓存起来,为了在触发事件的时候能够查找到对应的方法去执行。先查找事件名,而后找对对应的组件id相对应的事件。以下图:
8081b073fb2c06f047538b75cc97fc6f.png

setState是异步的?为何要这么作?setState执行机制?

由执行机制看,setState自己并非异步的,而是在调用setState时,若是react正处于更新过程,当前更新会被暂存,等上一次更新执行后再执行,这个过程给人一种异步的假象。

ReactComponent.prototype.setState = function(partialState, callback) {
  //  将setState事务放进队列中
  this.updater.enqueueSetState(this, partialState);
  if (callback) {
    this.updater.enqueueCallback(this, callback, 'setState');
  }
};
enqueueSetState: function (publicInstance, partialState) {
     // 获取当前组件的instance
    var internalInstance = getInternalInstanceReadyForUpdate(publicInstance, 'setState');

     // 将要更新的state放入一个数组里
     var queue = internalInstance._pendingStateQueue || (internalInstance._pendingStateQueue = []);
    queue.push(partialState);

     //  将要更新的component instance也放在一个队列里
    enqueueUpdate(internalInstance);
}
function enqueueUpdate(component) {
  // 若是没有处于批量建立/更新组件的阶段,则处理update state事务
  if (!batchingStrategy.isBatchingUpdates) {
    batchingStrategy.batchedUpdates(enqueueUpdate, component);
    return;
  }
  // 若是正处于批量建立/更新组件的过程,将当前的组件放在dirtyComponents数组中
  dirtyComponents.push(component);
}

这里的partialState能够传object,也能够传function,它会产生新的state以一种Object.assgine()的方式跟旧的state进行合并。
由这段代码能够看到,当前若是正处于建立/更新组件的过程,就不会马上去更新组件,而是先把当前的组件放在dirtyComponent里,因此不是每一次的setState都会更新组件。这段代码就解释了咱们常据说的:setState是一个异步的过程,它会集齐一批须要更新的组件而后一块儿更新。而batchingStrategy 又是个什么东西呢?
ReactDefaultBatchingStrategy.js

var ReactDefaultBatchingStrategy = {
  // 用于标记当前是否出于批量更新
  isBatchingUpdates: false,
  // 当调用这个方法时,正式开始批量更新
  batchedUpdates: function (callback, a, b, c, d, e) {
    var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates;

    ReactDefaultBatchingStrategy.isBatchingUpdates = true;

    // 若是当前事务正在更新过程在中,则调用callback,既enqueueUpdate
    if (alreadyBatchingUpdates) {
      return callback(a, b, c, d, e);
    } else {
    // 不然执行更新事务
      return transaction.perform(callback, null, a, b, c, d, e);
    }
  }
};

react-router原理

前端路由的原理思路大体上都是相同的,即实如今无刷新页面的条件下切换显示不一样的页面。而前端路由的本质就是页面的URL发生改变时,页面的显示结果能够根据URL的变化而变化,可是页面不会刷新。目前实现前端路由有两种方式:

经过Hash实现前端路由

路径中hash值改变,并不会引发页面刷新,同时咱们能够经过hashchange事件,监听hash的变化,从而实现咱们根据不一样的hash值展现和隐藏不一样UI显示的功能,进而实现前端路由。

经过H5的history实现前端路由

HTML5的History接口,History对象是一个底层接口,不继承于任何的接口。History接口容许咱们操做浏览器会话历史记录。
而history的pushState和repalce方法能够实现改变当前页面显示的url,但都不会刷新页面。

未完待续~
参考文档:

react生命周期详解
React diff
react 16新特性
react fiber1
react fiber2
react hooks
react 事件机制
setState机制1
setState机制2
react-router原理
集合

相关文章
相关标签/搜索