那些年,自己没回答上来的react面试题

29,911 阅读9分钟

1、React中元素与组件的区别

那个时候刚学react,不知道面试官说的元素是什么,现在知道了,就是虚拟dom嘛。。。

React 元素(React element)

它是 React 中最小基本单位,我们可以使用 JSX 语法轻松地创建一个 React 元素:

const element = <div className="element">I'm element</div>

React 元素不是真实的 DOM 元素,它仅仅是 js 的普通对象(plain objects),所以也没办法直接调用 DOM 原生的 API。上面的 JSX 转译后的对象大概是这样的:


{
    _context: Object,
    _owner: null,
    key: null,
    props: {
    className: 'element',
    children: 'I'm element'
  },
    ref: null,
    type: "div"
}

除了使用 JSX 语法,我们还可以使用 React.createElement() 和 React.cloneElement() 来构建 React 元素。

React 组件

React 中有三种构建组件的方式。React.createClass()、ES6 class和无状态函数。

1、React.createClass()

var Greeting = React.createClass({
  render: function() {
    return <h1>Hello, {this.props.name}</h1>;
  }
});

2、ES6 class

class Greeting extends React.Component{
  render: function() {
    return <h1>Hello, {this.props.name}</h1>;
  }
};

3、无状态函数

无状态函数是使用函数构建的无状态组件,无状态组件传入props和context两个参数,它没有state,除了render(),没有其它生命周期方法。

function Greeting (props) {
  return <h1>Hello, {props.name}</h1>;
}

4、PureComponent

除了为你提供了一个具有浅比较的shouldComponentUpdate方法,PureComponent和Component基本上完全相同。

元素与组件的区别

组件是由元素构成的。元素数据结构是普通对象,而组件数据结构是类或纯函数。

2、请说一说Forwarding Refs有什么用

是父组件用来获取子组件的dom元素的,为什么有这个API,原因如下

// 例如有一个子组件和父组件,代码如下
子组件为:
class Child extends React.Component{
    constructor(props){
      super(props);
    }
    render(){
      return <input />
    }
 }

// 父组件中,ref:
class Father extends React.Component{
  constructor(props){
    super(props);
    this.myRef=React.createRef();
  }
  componentDidMount(){
    console.log(this.myRef.current);
  }
  render(){
    return <Child ref={this.myRef}/>
  }
}

此时父组件的this.myRef.current的值是Child组件,也就是一个对象,如果用了React.forwardRef,也就是如下

// 子组件
const Child = React.forwardRef((props, ref) => (
    <input ref={ref} />
));
// 父组件
class Father extends React.Component {
    constructor(props) {
        super(props);
        this.myRef = React.createRef();
    }
    componentDidMount() {
        console.log(this.myRef.current);
    }
    render() {
        return <Child ref={this.myRef} />
    }
}

此时父组件的this.myRef.current的值是input这个DOM元素

3、简述一下virtual DOM 如何工作?

  • 当数据发生变化,比如setState时,会引起组件重新渲染,整个UI都会以virtual dom的形式重新渲染
  • 然后收集差异也就是diff新的virtual dom和老的virtual dom的差异
  • 最后把差异队列里的差异,比如增加节点、删除节点、移动节点更新到真实的DOM上

4、你对Fiber架构了解吗

注:答案参考司徒正美大神的文章

4.1、Fiber架构相对于以前的递归更新组件有有什么优势

  • 原因是递归更新组件会让JS调用栈占用很长时间。
  • 因为浏览器是单线程的,它将GUI渲染,事件处理,js执行等等放在了一起,只有将它做完才能做下一件事,如果有足够的时间,浏览器是会对我们的代码进行编译优化(JIT)及进行热代码优化。
  • Fiber架构正是利用这个原理将组件渲染分段执行,提高这样浏览器就有时间优化JS代码与修正reflow!

4.1、既然你说Fiber是将组件分段渲染,那第一段渲染之后,怎么知道下一段从哪个组件开始渲染呢?

  • Fiber节点拥有return, child, sibling三个属性,分别对应父节点, 第一个孩子, 它右边的兄弟, 有了它们就足够将一棵树变成一个链表, 实现深度优化遍历。

4.2 怎么决定每次更新的数量

  • React16则是需要将虚拟DOM转换为Fiber节点,首先它规定一个时间段内,然后在这个时间段能转换多少个FiberNode,就更新多少个。
  • 因此我们需要将我们的更新逻辑分成两个阶段,第一个阶段是将虚拟DOM转换成Fiber, Fiber转换成组件实例或真实DOM(不插入DOM树,插入DOM树会reflow)。Fiber转换成后两者明显会耗时,需要计算还剩下多少时间。
  • 比如,可以记录开始更新视图的时间var now = new Date - 0,假如我们更新试图自定义需要100毫秒,那么定义结束时间是var deadline = new Date + 100 ,所以每次更新一部分视图,就去拿当前时间new Date<deadline做判断,如果没有超过deadline就更新视图,超过了,就进入下一个更新阶段

4.3 如何调度时间才能保证流畅

  • 使用浏览器自带的api - requestIdleCallback,
  • 它的第一个参数是一个回调,回调有一个参数对象,对象有一个timeRemaining方法,就相当于new Date - deadline,并且它是一个高精度数据, 比毫秒更准确
  • 这个因为浏览器兼容性问题,react团队自己实现了requestIdleCallback

4.4 fiber带来的新的生命周期是什么

创建时

  • constructor ->
  • getDerivedStateFromProps(参数nextProps, prevState,注意里面this不指向组件的实例)->
  • render ->
  • componentDidMount

更新时

  • getDerivedStateFromProps(这个时props更新才调用,setState时不调用这个生命周期, 参数nextProps, prevState) ->
  • shouldComponentUpdate(setState时调用参数nextProps, nextState)->
  • render->
  • getSnapsshotBeforeUpdate(替换 componentWillUpdate)
  • componentDidUpdate(参数prevProps, prevState, snapshot)

5、什么是受控组件和非受控组件

  • 受状态控制的组件,必须要有onChange方法,否则不能使用 受控组件可以赋予默认值(官方推荐使用 受控组件) 实现双向数据绑定
class Input extends Component{
    constructor(){
        super();
        this.state = {val:'100'}
    }
    handleChange = (e) =>{ //e是事件源
        let val = e.target.value;
        this.setState({val});
    };
    render(){
        return (<div>
            <input type="text" value={this.state.val} onChange={this.handleChange}/>
            {this.state.val}
        </div>)
    }
}
  • 非受控也就意味着我可以不需要设置它的state属性,而通过ref来操作真实的DOM
class Sum extends Component{
    constructor(){
        super();
        this.state =  {result:''}
    }
    //通过ref设置的属性 可以通过this.refs获取到对应的dom元素
    handleChange = () =>{
        let result = this.refs.a.value + this.b.value;
        this.setState({result});
    };
    render(){
        return (
            <div onChange={this.handleChange}>
                <input type="number" ref="a"/>
                {/*x代表的真实的dom,把元素挂载在了当前实例上*/}
                <input type="number" ref={(x)=>{
                    this.b = x;
                }}/>
                {this.state.result}
            </div>
        )
    }
}

6、什么是状态提升

使用 react 经常会遇到几个组件需要共用状态数据的情况。这种情况下,我们最好将这部分共享的状态提升至他们最近的父组件当中进行管理。我们来看一下具体如何操作吧。

import React from 'react'
class Child_1 extends React.Component{
    constructor(props){
        super(props)
    }
    render(){
        return (
            <div>
                <h1>{this.props.value+2}</h1>
            </div> 
        )
    }
}
class Child_2 extends React.Component{
    constructor(props){
        super(props)
    }
    render(){
        return (
            <div>
                <h1>{this.props.value+1}</h1>
            </div> 
        )
    }
}
class Three extends React.Component {
    constructor(props){
        super(props)
        this.state = {
            txt:"牛逼"
        }
        this.handleChange = this.handleChange.bind(this)
    }
    handleChange(e){
        this.setState({
            txt:e.target.value
        })
    }
    render(){
       return (
            <div>
                <input type="text" value={this.state.txt} onChange={this.handleChange}/>
                <p>{this.state.txt}</p>
                <Child_1 value={this.state.txt}/>
                <Child_2 value={this.state.txt}/>
            </div>
       )
    }
}
export default Three

7、什么是高阶组件

高阶组件不是组件,是 增强函数,可以输入一个元组件,返回出一个新的增强组件

  • 属性代理 (Props Proxy) 在我看来属性代理就是提取公共的数据和方法到父组件,子组件只负责渲染数据,相当于设计模式里的模板模式,这样组件的重用性就更高了
function proxyHoc(WrappedComponent) {
	return class extends React.Component {
		render() {
			const newProps = {
				count: 1
			}
			return <WrappedComponent {...this.props} {...newProps} />
		}
	}
}
  • 反向继承
const MyContainer = (WrappedComponent)=>{
    return class extends WrappedComponent {
        render(){
            return super.render();
        }
    }
}

8、什么是上下文Context

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

  • 用法:在父组件上定义getChildContext方法,返回一个对象,然后它的子组件就可以通过this.context属性来获取
import React,{Component} from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
class Header extends Component{
    render() {
        return (
            <div>
                <Title/>
            </div>
        )
    }
}
class Title extends Component{
    static contextTypes={
        color:PropTypes.string
    }
    render() {
        return (
            <div style={{color:this.context.color}}>
                Title
            </div>
        )
    }
}
class Main extends Component{
    render() {
        return (
            <div>
                <Content>
                </Content>
            </div>
        )
    }
}
class Content extends Component{
    static contextTypes={
        color: PropTypes.string,
        changeColor:PropTypes.func
    }
    render() {
        return (
            <div style={{color:this.context.color}}>
                Content
                <button onClick={()=>this.context.changeColor('green')}>绿色</button>
                <button onClick={()=>this.context.changeColor('orange')}>橙色</button>
            </div>
        )
    }
}
class Page extends Component{
    constructor() {
        super();
        this.state={color:'red'};
    }
    static childContextTypes={
        color: PropTypes.string,
        changeColor:PropTypes.func
    }
    getChildContext() {
        return {
            color: this.state.color,
            changeColor:(color)=>{
                this.setState({color})
            }
        }
    }
    render() {
        return (
            <div>
                <Header/>
                <Main/>
            </div>
        )
    }
}
ReactDOM.render(<Page/>,document.querySelector('#root'));

9、react中的Portal是什么?

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

ReactDOM.createPortal(child, container)

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

10、react16的错误边界(Error Boundaries)是什么

部分 UI 中的 JavaScript 错误不应该破坏整个应用程序。 为了解决 React 用户的这个问题,React 16引入了一个 “错误边界(Error Boundaries)” 的新概念。

import React from 'react';
import ReactDOM from 'react-dom';
class ErrorBoundary extends React.Component{
    constructor(props) {
        super(props);
        this.state={hasError:false};
    }
    componentDidCatch(err,info) {
        this.setState({hasError: true});
    }
    render() {
        if (this.state.hasError) {
            return <h1>Something Went Wrong</h1>
        }
        return this.props.children;
    }
}

class Page extends React.Component{
    render() {
        return (
            <ErrorBoundary>
                <Clock/>
            </ErrorBoundary>
        )
    }
}
class Clock extends React.Component{
    render() {
        return (
            <div>hello{null.toString()}</div>
        )
    }
}

ReactDOM.render(<Page/>,document.querySelector('#root'));

11、如何在React中使用innerHTML

增加dangerouslySetInnerHTML属性,并且传入对象的属性名叫_html

function Component(props){
    return <div dangerouslySetInnerHTML={{_html:'<span>你好</span>'}}>
            </div>
}

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

  • reconciliation阶段包含的主要工作是对current tree 和 new tree 做diff计算,找出变化部分。进行遍历、对比等是可以中断,歇一会儿接着再来。
  • commit阶段是对上一阶段获取到的变化部分应用到真实的DOM树中,是一系列的DOM操作。不仅要维护更复杂的DOM状态,而且中断后再继续,会对用户体验造成影响。在普遍的应用场景下,此阶段的耗时比diff计算等耗时相对短。

13、请简单谈一下react的事件机制

  • 当用户在为onClick添加函数时,React并没有将Click时间绑定在DOM上面。
  • 而是在document处监听所有支持的事件,当事件发生并冒泡至document处时,React将事件内容封装交给中间层SyntheticEvent(负责所有事件合成)
  • 所以当事件触发的时候,对使用统一的分发函数dispatchEvent将指定函数执行。

14、为什么列表循环渲染的key最好不要用index

举例说明

变化前数组的值是[1,2,3,4],key就是对应的下标:0,1,2,3
变化后数组的值是[4,3,2,1],key对应的下标也是:0,1,2,3
  • 那么diff算法在变化前的数组找到key =0的值是1,在变化后数组里找到的key=0的值是4
  • 因为子元素不一样就重新删除并更新
  • 但是如果加了唯一的key,如下
变化前数组的值是[1,2,3,4],key就是对应的下标:id0,id1,id2,id3
变化后数组的值是[4,3,2,1],key对应的下标也是:id3,id2,id1,id0
  • 那么diff算法在变化前的数组找到key =id0的值是1,在变化后数组里找到的key=id0的值也是1
  • 因为子元素相同,就不删除并更新,只做移动操作,这就提升了性能