抱着侥幸心理都逃不过的React面试题

1,027 阅读14分钟

什么是Immutable?优点?

Immutable是一旦创建,就不能被更改的数据。对Immutable对象的任何修改或添加删除操作都会返回一个新的Immutable对象。

Immutable实现原理:是持久化数据结构,也就是是永久数据创建新数据时,要保证旧数据同时可用且不变。同时为了避免deepCopy把所有节点都复制一遍带来的性能损耗,Immutable使用了Structural Sharing(结构共享),即如果对象树结点发生变化,只修改这个结点和受它影响的父节点,其他结点进行共享。

实例对比:

// 原来的写法
let foo = {a: {b: 1}};
let bar = foo;
bar.a.b = 2;
console.log(foo.a.b);  // 打印 2
console.log(foo === bar);  //  打印 true

// 使用 immutable.js 后
import Immutable from 'immutable';
foo = Immutable.fromJS({a: {b: 1}});
bar = foo.setIn(['a', 'b'], 2);   // 使用 setIn 赋值
console.log(foo.getIn(['a', 'b']));  // 使用 getIn 取值,打印 1
console.log(foo === bar);  //  打印 false

优点:

  1. Immutable 降低了 Mutable 带来的复杂度
    可变(Mutable)数据耦合了 Time 和 Value 的概念,造成了数据很难被回溯。
  2. 节省内存:Immutable使用了 Structure Sharing 会尽量复用内存。没有被引用的对象会被垃圾回收
import { Map} from 'immutable';
let a = Map({
  select: 'users',
  filter: Map({ name: 'Cam' })
})
let b = a.set('select', 'people');

a === b; // false

a.get('filter') === b.get('filter'); // true
上面 ab 共享了没有变化的 `filter` 节点。
  1. Undo/Redo,Copy/Paste,想回退到哪里就拿出对应数据即可,很容易开发出撤销重做这种功能(时间旅行这些功能做起来小菜一碟)
  2. 并发安全:数据天生是不可变的,并发锁就不需要了
  3. 函数式编程

缺点:

  1. 需要学习新的 API
  2. 增加了资源文件大小
  3. 容易与原生对象混淆

这点是我们使用 Immutable.js 过程中遇到最大的问题。写代码要做思维上的转变。

虽然 Immutable.js 尽量尝试把 API 设计的原生对象类似,有的时候还是很难区别到底是 Immutable 对象还是原生对象,容易混淆操作。

Immutable 中的 Map 和 List 虽对应原生 Object 和 Array,但操作非常不同,比如你要用 map.get('key') 而不是 map.keyarray.get(0) 而不是 array[0]。另外 Immutable 每次修改都会返回新对象,也很容易忘记赋值。

当使用外部库的时候,一般需要使用原生对象,也很容易忘记转换。

下面给出一些办法来避免类似问题发生:

- 使用 Flow 或 TypeScript 这类有静态类型检查的工具
- 约定变量命名规则:如所有 Immutable 类型对象以 `$$` 开头。
- 使用 `Immutable.fromJS` 而不是 `Immutable.Map``Immutable.List` 来创建对象,这样可以避免 Immutable 和原生对象间的混用。

Immutable与 Object.freeze、const 区别

Object.freeze 和 ES6 中新加入的 const 都可以达到防止对象被篡改的功能,但它们是 shallowCopy 的。对象层级一深就要特殊处理了。

受控组件和非控组件是什么?

  • 受控组件:值受到react控制的表单元素(<input>、<textarea>、<select>),这也意味着表单组件可以立即响应输入更改
class Demo extends React.Component {
  constructor(props) {
    super(props);
    this.state = {val: ''};
    this.handleChange = this.handleChange.bind(this);
  }
  handleChange(event) {
   //当注释此行代码,文本框再次输入时,页面不会重新渲染,所产生效果即是文本框输入不了值,即文本框值的改变受到setState()方法的控制。
    this.setState({ val: event.target.value });
  };
  render() {
    return (
      <div>
          <input type="text" value={this.state.name} onChange={this.handleChange}/>
      </div>
    );
  }
}

比如其他标签:textarea和onchange select和onchange

  • 非受控组件:不受setState()的控制,与传统的HTML表单输入相似,input输入值即显示最新值
class Demo extends React.Component {
  constructor(props) {
    super(props);
    this.handleSubmit = this.handleSubmit.bind(this);
  }
  handleSubmit(event) {
    console.log(this.input.value);
    event.preventDefault();
  }
  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <label>
          Name:
          <input type="text" ref={(input) => this.input = input} />
        </label>
        <input type="submit" value="Submit" />
      </form>
    );
  }
}

受控组件和非受控组件的特点

特性非受控受控
一次性检索(例如表单提交)
及时验证
有条件的禁用提交按钮
执行输入格式
一个数据的几个输入
动态输入
表单数据由DOM本身处理

什么是状态提升

概括来说,就是将多个组件需要共享的状态提升到它们最近的父组件上.在父组件上改变这个状态然后通过props分发给子组件。

具体实现方式是:子组件更改父组件传递的数据,可以通过父组件通过传入一个方法,子组件触发该方法

使用场景:提供两个输入框(分别属于两个组件),保证输入框里面的内容同步

class Son extends React.Component {
    constructor(props) {
        super(props)
        this.handleChange = this.handleChange.bind(this)
    }

    handleChange(e) {
        this.props.onFatherMethod(e.target.value)
    }

    render() {
        return (
            <input type='text' value={ this.props.content } onChange={ this.handleChange } />
        )
    }
}
class Father extends React.Component {
   ...
    handleContentChange(newContent) {
        this.setState({ content: newContent })
    }
    render() {
        return (
            <div>
            	<Input content={ this.state.content } onFatherMethod={ this.handleContentChange }/>
                <br />
                <Input content={ this.state.content } onFatherMethod={ this.handleContentChange }/>
            </div>
        )
    }
}

什么是无状态组件

无状态组件就是指组件内部不存在state

什么是高阶组件(HOC)?有什么作用?特点?

高阶函数:接受函数作为参数的函数,其实map函数就是一个高阶函数

高阶组件:不是React提供的某种API,而是使用React的一种模式,类似于高阶函数,即指接受React组件作为参数,输出一个新的组件的函数。在这个函数中,我们可以修饰组件的props与state,所以在一些特定情况下高阶组件可以让我们的代码看起来更优美。

注:返回的新组件拥有了输入组件所不具备的功能。这里提到的组件并不是组件实例,而是组件类,也可以是一个无状态组件的函数

作用:

  • 更具有复用性:有时候很多React组件都需要公用同样一个逻辑,比如React-Redux中容器组件的部分,没有必要让每个组件都实现一遍shouldComponentUpdate这些生命周期函数,把这部分逻辑提取出来,利用高阶组件的方式应用出去,就可以减少很多组件的重复代码。
  • 修改现有React组件的行为:有些现成的React组件并不是开发者自己开发的,来自于第三方,或者即便是我们自己开发的,但是我们不想去触碰这些组件的内部逻辑,这时候可以用高阶组件。通过一个独立于原有组件的函数,可以产生新的组件,对原有组件没有任何侵害。

目前,高阶组件的实现方式主要有两种:

  • 代理方式的高阶组件:高阶组件通过被包裹的React组件来操作props
    • 高阶组件中可以修饰props于state的,在高阶组价中定义state,作为props传给参数组件(InnerComponent)
    • 高阶组件也可以传方法到参数组件(InnerComponent)中
    • 高阶组件的生命周期不会影响传入的组件
    • 并不是高阶组件的所有生命周期都会先执行
    • 由同一个高阶组件生成的新的组件间其实是相互不会影响的
  • 继承方式的高阶组件:高阶组件继承于被包裹的React组件
    • 操纵props
    • 操纵生命周期

代理方式实例

const HOC = (InnerComponent) => class extends React.Component{
    static defaultProps = {
        n:'HOC'
    }
    constructor(){
        super();
        this.state = {
            count:0
        }
    }
    componentWillMount(){
        console.log('HOC will mount')
    }
    componentDidMount(){
        console.log('HOC did mount')
    }
    update = () =>{
        const {count} = this.state
        this.setState({
            count:count+1
        })
    }
    render(){
        const newProps = this.state;
        return(
            <InnerComponent
                {...this.props}
                {...newProps}   //传state
                update = {this.update}  //传方法
            />
        )
    }

}

//无状态组件
const Button = HOC((props) => <button onClick={props.update}>{props.children}-{props.count}</button>) //无状态组件

//Button中改变count,不会影响Label组件中的count
class Label extends React.Component{//传统组件
    static defaultProps = {
        n:'C'
    }
    componentWillMount(){
        console.log('C will mount')
    }
    componentDidMount(){
        console.log('C did mount')
    }
    render(){
        return(
            <label>{this.props.children}-{this.props.count}</label> 
        )
    }
}
const LabelHoc = HOC(Label)

class App extends React.Component{//根组件
    render(){
        return(
            <div>
            	//随着点击渲染出label-0、1、2.....
                <Button>button</Button>
                <br/>
                //渲染出:label-0
                <LabelHoc>label</LabelHoc>
            </div>
        )
    }
}

继承方式实例

const HOC = (WrappedComponent) => 
  class NewComp extends WrappedComponent {
    componentWillMount() {
      console.log('我是继承高阶组件中的生命周期')
    }
    render() {
      // 获取继承而来的传入组件的元素内容
      const element = super.render()
      const newStyle = {
        color:element.type === 'div' ? 'red' : 'yellow'
      }
      const newProps = {...this.props, style: newStyle}
      // React.cloneElement方法生成新的React对象
      return React.cloneElement(element, newProps, element.props.children)
  }
}
export default HOC

普通组件传入高级组件

@HOC
class A extends Component {
  componentWillMount() {
    console.log('我是普通组件中生命周期')
  }
  render() {
    return (
      <div>我是普通组件</div>
    )
  }
}

export default A

最终效果:div是红色 ———— 这是操纵props实例

输出:我是继承高阶组件中的生命周期(A中生命周期内容不见了,被继承高阶组件给劫持了) ———— 
这就是继承高阶组件操纵生命周期

什么是上下文Context?怎么样使用Context?

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

React.createContext:创建一个上下文的容器(组件), defaultValue可以设置共享的默认数据

const {Provider, Consumer} = React.createContext(defaultValue);

Provider(生产者): 用于生产共享数据的地方。生产什么,就看value定义的是什么了。value:放置共享的数据。

<Provider value={/*共享的数据*/}>
    /*里面可以渲染对应的内容*/
</Provider>

Consumer(消费者): 专门消费供应商(Provider 上面提到的)产生数据。Consumer需要嵌套在生产者下面。才能通过回调的方式拿到共享的数据源。当然也可以单独使用,那就只能消费到上文提到的defaultValue

<Consumer>
  {value => /*根据上下文  进行渲染相应内容*/}
</Consumer>

实例:

export const {Provider,Consumer} = React.createContext("默认名称");

export default class App extends React.Component {
    render() {
        let name ="hannie"
        return (
            //Provider共享容器 接收一个name属性
            <Provider value={name}>
                <div style={{border:'1px solid red'}}>
                    <p>父组件定义的值:{name}</p>
                    <Son />
                </div>
            </Provider>
        );
    }
}


function Son(props) {
    return (
        //Consumer容器,可以拿到上文传递下来的name属性,并可以展示对应的值
        <Consumer>
            {( name ) =>
                <div style={{ border: '1px solid blue'}}>
                    <p>子组件。获取父组件的值:{name}</p>
                    <Grandson />
               </div>
            }
        </Consumer>
    );
}


function Grandson(props) {
    return (
         //Consumer容器,可以拿到上文传递下来的name属性,并可以展示对应的值
        <Consumer>
            {(name ) =>
               <div>
                   <p>孙组件。获取传递下来的值:{name}</p>
               </div>
            }
        </Consumer>
    );
}

什么是纯函数

  1. 函数的返回结果只依赖它的参数
  2. 函数执行时不会对外部变量产生影响(对外部变量产生影响简称副作用)
let a = 1
let func = (a) => a+1
fun(a)

依赖其他参数反例:返回结果还依赖于a 
let a =1
let func = (b) => a+b
fun(2)
const counter = {x:1}
let func = (counter) => (
  counter.x+1
) 
console.log(func(counter))  //2

副作用反例
let counter = { x:1 }
let func = (counter) => {
  counter.x=2
  return counter.x+1 
}
console.log(func(counter)) //3
console.log(counter.x) //2   受func(counter)执行影响

副作用实例:Ajax请求后端数据,添加登录监听和取消登录,手动修改DOM、调用 window.reload 刷新浏览器等都是对外部变量产生了影响

在react中,纯函数的状态只能在函数内部的生命周期存活

reducer 就是一个纯函数,接受旧的 state 和 action,返回新的 state (previousState, action) => newState

React Portal 是什么?有哪些使用场景

在以前, react 中所有的组件都会位于 #app 下,而使用 Portals 提供了一种脱离 #app 的组件。

因此 Portals 适合脱离文档流(out of flow) 的组件,特别是 position: absolute 与 position: fixed 的组件。比如模态框,通知,警告,goTop 等。

<html>
  <body>
    <div id="app"></div>
    <div id="modal"></div>
  </body>
</html>
const modalRoot = document.getElementById('modal');

class Modal extends React.Component {
  constructor(props) {
    super(props);
    this.el = document.createElement('div');
  }

  componentDidMount() {
    modalRoot.appendChild(this.el);
  }

  componentWillUnmount() {
    modalRoot.removeChild(this.el);
  }

  render() {
    return ReactDOM.createPortal(
      this.props.children,
      this.el,
    );
  }
}

React16错误边界Error Boundary是什么?

在推出之前报错会直接白屏,总是需要我们前端进行手动try catch,react16新增了两个生命周期componentdidcatch和static getDerivedStateFromError从框架级别让我们更方便捕捉异常并显示备用ui。其实就是在整个workloop外面包一层try catch,报错时候遍历父组件找到这两个生命周期并把堆栈信息塞给生命周期进行判断。

使用场景:在发生 js 报错的时候不至于说白屏,可以转去别的页面提示用户这里报了错;

其实就是用于在组件内发生了 js 错误时候的错误处理

React中createElement和cloneElement分别是什么?有什么区别?

createElement 和 cloneElement都能够生成可以直接在jsx页面组件中使用的标签语言,但是在生成的过程中是存在细微差别的

  • createElement
  1. type是一个标签名。如 div、span,或者 React 组件
  2. props为传入的属性
  3. 第三个以及之后的参数children,皆作为组件的子组件
React.createElement(
  type,
  [props],
  [...children]
)
  • cloneElement
  1. 传入的第一个参数element是一个 React 元素,而不是标签名或组件
  2. props为传入的属性
  3. 第三个以及之后的参数children,皆作为组件的子组件
React.cloneElement(
  element,
  [props],
  [...children]
)

特点:新添加的属性会并入原有的属性,传入到返回的新元素中,而旧的子元素将被替换,原始元素的键和引用将保留。

getChildren(){
    const  that = this;
    let { children } = that.props;
    return React.Children.map(children, child => {
        return React.cloneElement(child, {
            count: that.state.count
        });
    });
}

总之:
createElement 函数是 JSX 编译之后使⽤的创建 React Element 的函数,

cloneElement 则是⽤于克隆某个元素并传⼊新的 Props

React中Elemnet和Component有什么区别?

Element 是描述屏幕上所见内容的数据结构,是对于 UI 的对象表述。典型的 React Element 就是利用 JSX 构建的声明式代码片然后被转化为 createElement 的调用组合。

React Component 是一个函数或一个类,可以接收参数输入,并且返回某个React Element

React中state和props分别是什么?之间有什么不同

定义:

  • props是一个从外部传进组件的参数,主要作为就是从父组件向子组件传递数据,它具有可读性和不变性,只能通过外部组件主动传入新的props来重新渲染子组件,否则子组件的props以及展现形式不会改变。

  • state的主要作用是用于组件保存、控制以及修改自己的状态,它只能在constructor中初始化,它算是组件的私有属性,不可通过外部访问和修改,只能通过组件内部的this.setState来修改,修改state属性会导致组件的重新渲染。

区别:

  1. state是组件自己管理数据,控制自己的状态,可变;

  2. props是外部传入的数据参数,不可变;

  3. 没有state的叫做stateless无状态组件,有state的叫做有状态组件;

  4. 多用props,少用state。也就是多写无状态组件。

React中展示组件Presentational component和Container component之间有什么不同

  • Containers
  1. 更关心组件是如何运作的,负责和后台交互,负责和Redux进行连接,会调用actions,负责传递数据给component

  2. 有状态的,因为他们是(其它组件的)数据源

  3. 为了可读性,可复用性,可维护性

  • Presentational
  1. 展示组件关心组件看起来是什么,负责渲染
  2. 几乎不会有自身状态,但当展示组件拥有自身的状态时,通常也只关心 UI 状态而不是数据的状态。

useEffect 中如何使用 async/await

useEffect 的回调参数无法返回 Promise,因此无法使用 async/await,此时可以选择再包装一层函数

async function fetchApi() {
  let response = await fetch(url)
  response = await res.json()
  handleData(response)
}

useEffect(() => {
  fetchApi();
}, []);

怎么通讯?

看我的另一篇【框架对比】React和Vue的通讯方式

生命周期是什么?

看我的另一篇【框架对比】React和Vue的生命周期

React hooks

看我的另两篇【框架对比】React和Vue的hook函数[React基础知识]几个use+'...'常用hook

react hooks 中如何模拟 componentDidMount

在 useEffect,把第二个参数即依赖的状态,设置为 []

useEffect(callback, [])

为什么不能在表达式里面定义 react hooks

因为react hook底层是基于链表实现,调用的条件是每次组件被render的时候都会顺序执行所有的hooks。

如果使用 SSR,可以在 created/componentWillMount 中访问 localStorage 吗

不可以,created/componentWillMount 时,还未挂载,代码仍然在服务器中执行,此时没有浏览器环境,因此此时访问 localStorage 将会报错

React 中 fiber 是用来做什么的

因为JavaScript单线程的特点,每个同步任务不能耗时太长,不然就会让程序不会对其他输入作出相应,React的更新过程就是犯了这个禁忌,而React Fiber就是要改变现状。 而可以通过分片来破解JavaScript中同步操作时间过长的问题。

把一个耗时长的任务分成很多小片,每一个小片的运行时间很短,虽然总时间依然很长,但是在每个小片执行完之后,都给其他任务一个执行的机会,这样唯一的线程就不会被独占,其他任务依然有运行的机会。

React Fiber把更新过程碎片化,每执行完一段更新过程,就把控制权交还给React负责任务协调的模块,看看有没有其他紧急任务要做,如果没有就继续去更新,如果有紧急任务,那就去做紧急任务。

维护每一个分片的数据结构,就是Fiber。

参考链接:

www.cnblogs.com/qiangxia/p/…

q.shanyue.tech/fe/react/69…

www.520mg.com/a/inter/rea…