React面试汇总

275 阅读9分钟

React的设计初衷

声明式的写法

React 使创建交互式 UI 变得轻而易举。为你应用的每一个状态设计简洁的视图,当数据改变时 React 能有效地更新并正确地渲染组件。

以声明式编写 UI,可以让你的代码更加可靠,且方便调试。

组件化的思维

创建拥有各自状态的组件,再由这些组件构成更加复杂的 UI。

组件逻辑使用 JavaScript 编写而非模版,因此你可以轻松地在应用中传递数据,并使得状态与 DOM 分离。

面试考点

虚拟DOM是什么

虚拟DOM作为React的骨架,面试必问题,首先虚拟DOM在React的代码中其实就是一个树的结构,里面存着和真实dom之间的映射关系,例如:

['div', {id: 'a'}, ['span', {}, 'hello']]

对应的真实dom:

<div id = 'a'><span>hello</span></div>

有了这份结构,实现跨平台的RN也就是水到渠成了。

虚拟dom是何时创建的呢?

在某一时间节点调用 React 的 render() 方法,会创建一棵由 React 元素组成的树。在下一次 state 或 props 更新时,相同的 render() 方法会返回一棵不同的树。

render() {
  return React.createElement('div', {id: 'a'}, React.createElement('span', {}, 'hello'))
}

那么React是如何把这个虚拟dom的优势发挥它的作用呢?React 需要基于这两棵树之间的差别来判断如何有效率的更新 UI 以保证当前 UI 与最新的树保持同步。这就要说到它的另一个配套diff算法了。

Diff算法做了哪些事情

当对比两棵不同的树的时候会从根节点开始,有几个策略:

1、对比不同类型的元素

当根节点为不同的类型的元素的时候,React会之间废弃掉原有的虚拟DOM,卸载所有的真实DOM,触发一次完整的重建流程,不再往下比较。举个例子,当一个元素从 <a> 变成 <img>,从 <Article> 变成 <Comment>,或从 <Button> 变成 <div> 都会触发一个完整的重建流程。

当拆卸一棵树时,对应的 DOM 节点也会被销毁。组件实例将执行 componentWillUnmount() 方法。当建立一棵新的树时,对应的 DOM 节点会被创建以及插入到 DOM 中。组件实例将执行 componentWillMount() 方法,紧接着 componentDidMount() 方法。所有跟之前的树所关联的 state 也会被销毁。

2、对比相同类型的元素

React只会对比属性的变化,更新对应的属性值,例如className,style等。

处理完之后需要继续递归子元素处理。

3、对比相同的React组件

React 将更新该组件实例的 props 以跟最新的元素保持一致,并且调用该实例的 componentWillReceiveProps()componentWillUpdate() 方法。

下一步,调用 render() 方法,diff 算法将在之前的结果以及新的结果中进行递归,找出具体的差异。

4、遍历子元素需要依赖key来提升性能

当遍历子元素的时候,需要借助key的概念了,因为React并不知道子元素是否做了位置移动,比如在前面插入了一个节点,又或者做重新排序的场景,这些都会可能导致React性能底下。

如果有了key,React就只需要对比相同key的节点差异即可,同时也能分辨出新增或者删除的节点有哪些。

那么这个key的值如何设置呢?大部分情况下我们可以使用数组的下标作为key的值,比如:

lists.map(item, index => <li key={index}>aa</li>)

但是当<li>里面包含非受控组件的时候,用下标作为key可能会出现无法预期的结果,比如:

lists.map(item, index => {
    return (
        <li key={index}>
            <input />
        </li>
    )
})

当对列表元素进行排序的时候,元素的key会跟着数组下标被改变,就会导致input中的内容并不会跟着元素移动,从而导致功能出现异常。


所以建议第一选择用每一条数据的唯一id作为key。


setState原理

1、为什么setState是个异步的过程?


因为React是个处处都考虑性能优化的框架,如果每一次setState都进行一次render的话,这会导致很多没必要的渲染,特别是组件层级嵌套深的时候,每一次父级结点的render都会带动所有子节点的render。

回答这题的时候,需要知道提到一个概念batchUpdate,批量合并更新,也就是说你在一次事件函数内触发多次setState并不会实时触发更新,而是合并成一次更新,这样可以避免不必要的渲染重构。所以正常情况下setState是个异步的过程,但是在setTimeout等宏任务中React还无法做到批量更新,如果需要在宏任务里做到批量更新需要借用React的一个方法unstable_batchedUpdates

setTimeout(() => {
    unstable_batchedUpdates(() => {
        this.setState({a: 1});
        this.setState({a: 2});
    })
}, 100)

ps:这个方法是极其不优雅的一种做法,但是React又没有更好的办法来解决,所以也是没办法的办法,最新版引进了Fiber,这个或许在未来的版本中能够完美的解决,而不需要开发者去处理,所以unstable_batchedUpdates是会逐渐在新版本中淘汰掉的。

HOC(higherOrderComponent)

HOC又叫高阶组件,具体而言,高阶组件是参数为组件,返回值为新组件的函数。

代表有Redux的connect方法,Mobx的@observer,我们来看看Redux的connect简单实现:

function connect(mapStateToProps, mapDispatchToProps, ...){
    return function(Wrapper) {
        return class extent React.Component{
            constructor(){
                this.state = {
                    nextStates: {}
                }
            }
            componentWillMount(){
                const { store } = this.context;
                store.subscribe(() => {
                   this.updateProps();
                }
            }
            updateProps() {
                const { store } = this.context;
                this.setState(state => {
                    nextStates: {                        
                        ...mapStateToProps(store.getState()),
                        ...mapDispatchToProps(),
                        ...this.props
                    }
                });
            }            render() {
                return <Wrapper {...this.nextStates} />
            }
        }   
    }
}

以上为简单的实现,只为了说明一下高阶组件的原理,但是有几点需要注意:

1.HOC不能改变原有组件的任何结构,只做props的透传
2.不要在render中对任何一个组件使用高阶函数,这样会导致React渲染效率大打折扣

性能优化

React中的性能优化无非就是减少不必要的渲染,特别是嵌套层次比较多的时候,很容易一个不小心引起一连串的反应。

1、善于使用工具

React开发了一个基于Chrome的插件,这个插件可以查看到React的dom树,还有每次渲染的情况。

2、有没有用过shouldComponentUpdate

这个生命周期函数是控制组件是否渲染,可以跳过render及之后的所有流程,默认情况下是直接返回true渲染的。如果你能够知道组件什么时候才需要渲染的话,就可以重写这个函数,只让符合条件的情况返回true,默认都返回false即可。例如:

shouldComponentUpdate(nextProps, nextState) {
    if (this.props.color !== nextProps.color) {
      return true;
    }
    if (this.state.count !== nextState.count) {
      return true;
    }
    return false;
  }

这种写法适合功能比较单一,用到的属性也比较少的情况,如果复杂化就不优雅了,一个个列出来也不适合。

那如果数据很多的情况下怎么办呢?(面试官开始装逼了,因为他堵你不知道,哈哈哈)

对于这种情况React提供了一个封装好的组件PureComponent,我们只要继承这个组件即可

class CounterButton extends React.PureComponent {
  constructor(props) {
    super(props);
    this.state = {count: 1};
  }

  render() {
    return (
      <button
        color={this.props.color}
        onClick={() => this.setState(state => ({count: state.count + 1}))}>
        Count: {this.state.count}
      </button>
    );
  }
}

什么是PureComponent?它和普通组件的区别是啥?(你竟然答上来了,他不服了,你惨了,他要深挖你:))))

PureComponent只是对shouldComponentUpdate做了一次封装,但是内部用的是浅比较

shouldComponentUpdate(nextProps, nextState) {
    if (this.props !== nextProps) {
      return true;
    }
   if (this.state !== nextState) {
     return true;
   }
    return false;
  }

那么设计好的坑又来了,等着你跳下去,我下面的代码到底会不会导致更新?

class ListOfWords extends React.PureComponent {
  render() {
    return <div>{this.props.words.join(',')}</div>;
  }
}

class WordAdder extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      words: ['marklar']
    };
    this.handleClick = this.handleClick.bind(this);
  }

  handleClick() {
    // 这部分代码很糟,而且还有 bug
    const words = this.state.words;
    words.push('marklar');
    this.setState({words: words}); // 坑在着呢
  }

  render() {
    return (
      <div>
        <button onClick={this.handleClick} />
        <ListOfWords words={this.state.words} />
      </div>
    );
  }
}

如果你没有发现坑的话,你的回答肯定是会渲染,哈哈哈。

这里还涉及到this.setState,如果按照上面words的写法的话,是基于原来的state对象去更新的,就像:

var xiaoming = {
    girlFrends: ['lily', 'sandy', 'xiaomei']
}
// 这么多女朋友要同时约会咋办,小明发明了高科技给自己制作了好多替身,但是他们公用的是一个脑子
var xiaoming2 = xiaoming;

// 当小明2去和lily约会的时候,lily要验明正身,问了好多隐私问题,但是奈何xiaoming2都答上来了
// 因为啊
xiaoming2 === xiaoming; // true

// xiaoming2这时候又发展了一个女朋友
xiaoming2.girlFrends.push('lisa');

// 这下小明爽歪歪了,lisa也没办法分辨出来小明
xiaoming === xiaoming2; // true 互相共享大脑啊

所以按照上面的情况,PureComponent肯定发现不了小明换了一个替身,而且还多了一个女朋友lisa,哈哈哈

那要定义什么样的规则来约束小明这样的行为呢,这就要提到不可变数据了(immutable)。

handleClick() {
  this.setState(state => ({
    words: state.words.concat(['marklar'])
  }));
}

function updateColorMap(colormap) {
  return Object.assign({}, colormap, {right: 'blue'});
}

以上两种方式就是从复制的角度做了约束,对象生成了就不能改变内部的任何一个属性,可以通过生成一个新的对象来实现,这个新的对象可以把小明的女朋友们都复制过来,但是以后他们两个就没有关系了,再多几个女朋友也和小明没有关系,哈哈哈

但是一个对象里面可能嵌套很深,你需要一层层的去拷贝那得累死


所以这里又可以隐身出immutability-helper,如果数据非常复杂的情况下,可以引入这个插件,帮你解决这种深层嵌套的问题,这里就不展开更多了。

import update from 'immutability-helper';

const newData = update(myData, {
  x: {y: {z: {$set: 7}}},
  a: {b: {$push: [9]}}
});