前端面试的那些事儿(16)~ 掌握这些知识就不怕面试官问React - React框架基础

React 快速入门

React 是一个用于构建用户界面的 JavaScript 库。数据改变时 React 能有效地更新并正确地渲染组件。javascript

咱们写 React 就是建立拥有各自状态的组件,再由这些组件构成更加复杂的 UI 界面。java

第一个组件:react

import React from 'react';

class HelloReact extends React.Component {
  render() {
    return (
      <div> Hello {this.props.name} </div>
    );
  }
}

export default HelloReact;
复制代码

组件拥有动态渲染 name 的功能,name 字段是使用该组件的地方传入的。git

<HelloReact name={"第一个组件"}/>
复制代码

所以咱们就能够正确渲染出一个DOM结构github

image.png
就是经过这种不断去组合各种有状态的组件,构建一个复杂的工业项目。

本文代码托管地址>>>请点击面试

JSX

接触 React 想必 JSX 应该会是你接触的第一个新概念,由于你多是第一次在 js 文件中直接编写“HTML”。编程

return (
  <div> Hello {this.props.name} </div>
);
复制代码

JSX 是一个 JavaScript 的语法扩展,它能够生成 React “元素”。数组

它的写法彻底等价于:浏览器

import React from 'react';

class HelloJSX extends React.Component {
  render() {
    return React.createElement("div", null, "Hello ", this.props.name);
  }
}

export default HelloJSX;
复制代码

其实 React 最终须要的就是 React.createElement("div", null, "Hello ", this.props.name)  经过它,React 能够建立一系列数据结构来表示 React 中的元素,最终再把它转换成真实的 DOM 插入到页面中。缓存

所以 JSX 的结构与真实的 DOM 结构只能说是类似,它们之间还须要 React 去作一系列转换。

JSX的本质是什么?

JSX 自己在 React 项目中只是语法糖,不能直接被浏览器识别的。须要通过编译,那么这个编译后的就是它的本质了。

JSX 在 React 项目中被编译成了 React 元素,就是一个树状的数据结构。也就是咱们常说的虚拟 DOM。

组件传值

既然 Reac t是经过组件的组合来实现复杂工程的构建。那么组件之间的第一个问题就是它们之间如何通讯。

props

经过 props 向子组件传递数据,而且全部 React 组件都必须像纯函数同样保护它们的 props 不被更改。

image.png

import React from 'react';

class Child extends React.Component {
  render() {
    const { list } = this.props

    return <ul>{list.map((item, index) => { return <li key={item.id}> <span>{item.title}</span> </li> })}</ul>
  }
}

class Parent extends React.Component{
  constructor(props) {
    super(props)
    this.state = {
      list: [
        {
          id: 'id-1',
          title: '标题1'
        },
        {
          id: 'id-2',
          title: '标题2'
        },
        {
          id: 'id-3',
          title: '标题3'
        }
      ]
    }
  }
  render() {
    return <Child list={this.state.list} /> } } export default Parent; 复制代码

代码解释:

  1. Parent 父组件把自身的 list 状态传递给子组件 <Child list={this.state.list} />
  2. Child 子组件经过 props 获取到父组件传递的数据并成功渲染 const { list } = this.props

这个就是父组件向子组件传递数据。

思考一个问题,当咱们的组件层级嵌套很深,例如 Parent 组件须要向 Child 的 Child 也就是孙子组件,甚至乎更加深的层级去传递一些数据该如何作呢?能够经过context去实现。

Context

Context 提供了一种在组件之间共享此类值的方式,而没必要显式地经过组件树的逐层传递 props。

咱们经常用它来传递应用级别的配置数据,例如APP的主题、用户喜爱之类的。

image.png

咱们来实现一个切换主题的简单场景:

import React from 'react'

// 建立 Context 填入默认值(任何一个 js 变量)
const ThemeContext = React.createContext('light') // {1}

// 函数式组件可使用 Consumer
function ThemeLink (props) {
  return <ThemeContext.Consumer> // {3}
    { value => <p>link's theme is {value}</p> }
  </ThemeContext.Consumer>
}

class ThemedButton extends React.Component {
  render() {
    const theme = this.context // React 会往上找到最近的 theme Provider,而后使用它的值。
    return <div>
      <p>button's theme is {theme}</p>
    </div>
  }
}
ThemedButton.contextType = ThemeContext // 指定 contextType 读取当前的 theme context。

// 中间的组件不再必指明往下传递 theme 了。
function Toolbar(props) {
  return (
    <div>
      <ThemedButton />
      <ThemeLink />
    </div>
  )
}

class App extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      theme: 'light'
    }
  }
  render() {
    return <ThemeContext.Provider value={this.state.theme}> // {2}
      <Toolbar />
      <hr/>
      <button onClick={this.changeTheme}>change theme</button>
    </ThemeContext.Provider>
  }
  changeTheme = () => {
    this.setState({
      theme: this.state.theme === 'light' ? 'dark' : 'light'
    })
  }
}

export default App
复制代码

代码解释:

  • {1} const ThemeContext = React.createContext('light') 建立一个context,返回一个ThemeContext 对象
  • {2} <ThemeContext.Provider value={this.state.theme}></ThemeContext.Provider>  ThemeContext 的提供者
  • {3} <ThemeContext.Consumer></ThemeContext.Consumer> ThemeContext 的消费者
React.createContext 源码
export function createContext( defaultValue, calculateChangedBits ){
  const context = {
    $$typeof: REACT_CONTEXT_TYPE,
    _calculateChangedBits: calculateChangedBits,
    _currentValue: defaultValue,
    _currentValue2: defaultValue,
    Provider:null,
    Consumer:null ,
  };

  context.Provider = {
    $$typeof: REACT_PROVIDER_TYPE,
    _context: context,
  };

  return context;
}
复制代码

这是通过删减的源码,调用 createContext 函数返回的就是一个context对象,咱们使用的 ThemeContext.Provider 也就是该对象返回的其中一个属性。

state

经过上面咱们已经知道组件之间是如何进行通讯的,那么组件如何维护自身的状态呢?就是 state 了,由于它的一些特性,也让它成为了面试必考。

import React from 'react'

class StateDemo extends React.Component{
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    }
  }

  add = ()=>{
    this.setState({
      count: this.state.count + 1
    })
  }

  render() {
    return <div> <p>{this.state.count}</p> <button onClick={this.add}>增长</button> </div>
  }
}

export default StateDemo
复制代码

代码解释:

  1. 在 constructor 中初始化组件的 state 对象
  2. 当点击按钮时经过 setState 改变 state 的状态

这就是组件的 state,以及如何改变 state,关于它的常见面试题有:

  1. setState 是同步仍是异步的?
  2. 为何有时连续两次 setState只有一次生效?
  3. 为何 state 必须为不可变值

setState 异步状况

1.1 React 生命周期中 setState

componentDidMount() {
  console.log('SetState调用setState');
  this.setState({
    index: this.state.index + 1
  })
  console.log('state', this.state.index); // 0

  console.log('SetState调用setState');
  this.setState({
    index: this.state.index + 1
  })
  console.log('state', this.state.index); // 0
}
复制代码

两次打印都是0,没有当即更新

1.2 React 合成事件中 setState

add = ()=>{
    console.log('合成事件中调用setState');

    this.setState({
      count: this.state.count + 1
    })

    console.log('count', this.state.count); // 0

    console.log('合成事件中调用setState');

    this.setState({
      count: this.state.count + 1
    })

    console.log('count', this.state.count); // 0

  }
复制代码
  • 两次输出都为0没有当即更新
  • 这里进行了两次设置,可是页面只增长了1,说明被合并了

setState 同步状况

1.1 异步函数中执行 setState

componentDidMount() {
    setTimeout(()=>{
      console.log('SetState调用setState');
      this.setState({
        index: this.state.index + 1
      })
      console.log('state', this.state.index); // 1

      console.log('SetState调用setState');
      this.setState({
        index: this.state.index + 1
      })
      console.log('state', this.state.index); // 2
    },0);
  }
复制代码

咱们使用 setTimeout 异步函数包装下,发现,setState 同步执行了。

1.2 原生事件中执行 setState

componentDidMount(){
    document.body.addEventListener('click', this.bodyClickHandler); // 在生命周期中绑定原生事件
}

bodyClickHandler = ()=>{
  console.log('原生事件中调用setState');

  this.setState({
    count: this.state.count + 1
  })

  console.log('count', this.state.count); // 1

  console.log('原生事件中调用setState');

  this.setState({
    count: this.state.count + 1
  })

  console.log('count', this.state.count); // 2
}
复制代码

在原生事件中执行 setState 也同步执行了。

异步状况:

  1. React 生命周期中 setState
  2. React 合成事件中 setState

缘由:

React的生命周期和合成事件中,React仍然处于他的更新机制中,这时isBatchingUpdates 为 true 。 这时不管调用多少次setState,都会不会执行更新,而是将要更新的state存入_pendingStateQueue,将要更新的组件存入dirtyComponent

当上一次更新机制执行完毕,以生命周期为例,全部组件,即最顶层组件didmount后会将 isBatchingUpdates 设置为 false 。这时将执行以前累积的setState

同步状况:

  1. 异步函数中执行 setState
  2. 原生事件中执行 setState

缘由:

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

在生命周期,根据JS的异步机制,会将异步函数先暂存,等全部同步代码执行完毕后再执行,这时上一次更新过程已经执行完毕,isBatchingUpdates被设置为 false ,根据上面的流程,这时再调用setState便可当即执行更新,拿到更新结果。

简单理解:当isBatchingUpdatestrue 时,加入异步队列,下一轮执行;当isBatchingUpdatesfalse 时,不加入异步队列,当即执行。能够理解成相似 JavaScript EventLoop  机制。

image.png
这是 React-16.6.0 的源码

if (!isBatchingUpdates && !isRendering) {
  performSyncWork();
}
复制代码

这句话的大概意思是,isBatchingUpdates变量为 false 时当即执行performSyncWork方法。从该方法名的意思是“执行同步任务”。

Batch模式下React不会马上修改state,而是把这个对象放到一个更新队列中,稍后才会从队列中把新的状态提取出来合并到state中,而后再触发组件更新,这样的设计主要目的是为了提升 UI 更新的性能。

setState 合并

从 React 源码中也能够看出 setState 第一个参数其实能够接受两种类型的,一种是对象,一种是函数。对象型是会进行合并处理 Object.assagin ,而函数型是不会的。

Component.prototype.setState = function(partialState, callback) {
  invariant(
    typeof partialState === 'object' ||
      typeof partialState === 'function' ||
      partialState == null,
    'setState(...): takes an object of state variables to update or a ' +
      'function which returns an object of state variables.',
  );
  this.updater.enqueueSetState(this, partialState, callback, 'setState');
};
复制代码

咱们再来看看源码中 enqueueSetState 是如何进行处理的:

for (let i = oldReplace ? 1 : 0; i < oldQueue.length; i++) {
  let partial = oldQueue[i];
  let partialState =
      typeof partial === 'function'
  ? partial.call(inst, nextState, element.props, publicContext)
  : partial;
  if (partialState != null) {
    if (dontMutate) {
      dontMutate = false;
      nextState = Object.assign({}, nextState, partialState);
    } else {
      Object.assign(nextState, partialState);
    }
  }
}
复制代码

这是其中处理 state queue 的源码片断,咱们关注重点便可

if(partial === 'function'){
  partial.call(inst, nextState, element.props, publicContext)
}
if (partialState != null) {
  Object.assign(nextState, partialState);
}

复制代码

若是 queue 中的 state 是函数则直接执行,若是是非函数,且不是null,则先进行对象合并操做。

咱们来测试下传入函数是否真的不会合并

componentDidMount() {
    console.log('SetState传入函数');

    this.setState((state, props) => ({
      index: state.index + 1
    }));

    console.log('state', this.state.index); // 0

    console.log('SetState传入函数');

    this.setState((state, props) => ({
      index: state.index + 1
    }));

    console.log('state', this.state.index); // 0

    setTimeout(()=>{
      console.log('state', this.state.index); // 2
    },0);
  }
复制代码

从结果能够看出来,咱们同时更新了两次index,说明传入函数并不会被合并。

state必须为不可变值

咱们那上面的 add 方法作例子,改造下:

add = ()=>{
    this.state.count++;
    this.setState({
      count: this.state.count
    })
  }
复制代码
  1. 咱们先直接改变 state 对象中的 count 值
  2. 而后再去 setState

咱们发现效果是同样的,是否是表示咱们这样作也能够呢?答案是否认的。

必须得这样写:

add = ()=>{
    this.setState({
      count: this.state.count + 1
    })
  }
复制代码

不可变值在对象和数组中的应用:

import React from 'react'

class StateDemo1 extends React.Component{
  constructor(props) {
    super(props);
    this.state = {
      list:[
        {
          id:1,
          name:"a"
        },
        {
          id:2,
          name:"b"
        },
        {
          id:3,
          name:"C"
        },
      ]
    }
  }

render() {
    return (
      <div> <ul> { this.state.list.map((item)=>{ return <li key={item.id}> <span>{item.name}</span> </li> }) } </ul> <button onClick={this.deletePop}>删除最后一条</button> </div>
    )
  }
}

export default StateDemo1
复制代码

image.png

当咱们须要对这个列表进行删除时, this.state.list  就必须遵循不可变值。

deletePop = ()=>{
    const newList = [...this.state.list];
    newList.pop();
    this.setState({
      list: newList
    })
  }
复制代码
  • 首先对 this.state.list  进行浅复制
  • 再操做新的 list 数组
  • 在 setState 中进行赋值操做

在这里咱们没有去改变原 state.list 值,而是浅复制了一个副本出来再去操做的。这就是在 React 编程中要很是注意的“不可变值”。

至于为何必需要不可变值呢,这个其实跟React性能优化有很是大的关系,在下一篇文章中会详细讲述其中缘由。

React 事件

合成事件

Virtual DOM 在内存中是以对象的形式存在的,若是想要在这些对象上添加事件,就会很是简单。React 基于 Virtual DOM 实现了一个 SyntheticEvent (合成事件)层,咱们所定义的事件处理器会接收到一个 SyntheticEvent 对象的实例,它彻底符合 W3C 标准,不会存在任何 IE 标准的兼容性问题。而且与原生的浏览器事件同样拥有一样的接口,一样支持事件的冒泡机制,咱们可使用 stopPropagation() 和 preventDefault() 来中断它。

合成事件的实现机制

在 React 底层,主要对合成事件作了两件事:事件委派和自动绑定。

1. 事件委派

在使用 React 事件前,必定要熟悉它的事件代理机制。它并不会把事件处理函数直接绑定到真实的节点上,而是把全部事件绑定到结构的最外层,使用一个统一的事件监听器,这个事件监听器上维持了一个映射来保存全部组件内部的事件监听和处理函数。当组件挂载或卸载时,只是在这个统一的事件监听器上插入或删除一些对象;当事件发生时,首先被这个统一的事件监听器处理,而后在映射里找到真正的事件处理函数并调用。这样作简化了事件处理和回收机制,效率也有很大提高。

2. 自动绑定

在 React 组件中,每一个方法的上下文都会指向该组件的实例,即自动绑定 this 为当前组件。并且 React 还会对这种引用进行缓存,以达到 CPU 和内存的最优化。 实际上,React 的合成事件系统只是原生 DOM 事件系统的一个子集。它仅仅实现了 DOM Level 3 的事件接口,而且统一了浏览器间的兼容问题。有些事件 React 并无实现,或者受某些限制没办法去实现,好比 windowresize 事件。

对于没法使用 React 合成事件的场景,咱们还须要使用原生事件来完成。

为何要手动绑定this

ES6类的方法内部若是含有this,它默认指向类的实例。可是,必须很是当心,一旦单独使用该方法,极可能报错。而React中执行事件回调方法放入一个队列中,当事件被触发时执行相应的回调,所以该事件回调方法并未与React组件实例绑定在一块儿,因此咱们须要进行手动绑定上下文。

bind this 绑定

import React from 'react'

class EventDemo extends React.Component{
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    }
  }

  add(){
    this.setState({
    	count: this.state.count + 1
    })   
  }

  render() {
    return <div> <p>{this.state.count}</p> <button onClick={this.add.bind(this)}>增长</button> </div>
  }
}
复制代码

或者在 constructor 中 bind this

constructor(props) {
    super(props);
    this.state = {
      count: 0
    }
    this.add = this.add.bind(this);
  }
复制代码

箭头函数

使用箭头函数就不须要使用 bind this 进行绑定了

add = ()=>{
    this.setState({
    	count: this.state.count + 1
    })   
  }
复制代码

合成事件与原生事件的区别

  1. React 事件使用驼峰命名,而不是所有小写;
  2. 阻止原生事件传播须要使用 e.preventDefault(),不过对于不支持该方法的浏览器(IE9 如下),只能使用 e.cancelBubble = true 来阻止。而在 React 合成事件中,只须要使用 e.preventDefault() 便可;
  3. React本身实现了一套事件机制,本身模拟了事件冒泡和捕获的过程,采用了事件代理,批量更新等方法,而且抹平了各个浏览器的兼容性问题。

React事件和原生事件的执行顺序

import React from 'react'

class EventDemo extends React.Component {
  constructor(props) {
    super(props);
    this.parent = React.createRef();
    this.child = React.createRef();
  }
  componentDidMount() {
    this.parent.current.addEventListener('click', (e) => {
      console.log('dom parent');
    })
    this.child.current.addEventListener('click', (e) => {
      console.log('dom child');
    })
    document.addEventListener('click', (e) => {
      console.log('document');
    })
  }

  childClick = (e) => {
    console.log('react child');
  }

  parentClick = (e) => {
    console.log('react parent');
  }

  render() {
    return (
      <div onClick={this.parentClick} ref={this.parent}> <div onClick={this.childClick} ref={this.child}> test Event </div> </div>)
  }
}

export default EventDemo
复制代码

执行结果:

dom child
dom parent
react child
react parent
document
复制代码

由上面的流程咱们能够理解:

  • React的全部事件都挂载在document
  • 当真实dom触发后冒泡到document后才会对React事件进行处理
  • 因此原生的事件会先执行
  • 而后执行React合成事件
  • 最后执行真正在document上挂载的事件

React事件和原生事件能够混用吗?

React事件和原生事件最好不要混用。

原生事件中若是执行了stopPropagation方法,则会致使其余React事件失效。由于全部元素的事件将没法冒泡到document上。

受控组件与非受控组件

受控组件与非受控组件主要是针对表单元素

受控组件

咱们先来看一段React处理表单的代码:

import React from 'react'

class FormDemo extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      name: 'frank'
    }
  }
  render() {
    return <div> <p>{this.state.name}</p> <input id="inputName" value={this.state.name} onChange={this.onInputChange}/> </div> } onInputChange = (e) => { this.setState({ name: e.target.value }) } } export default FormDemo 复制代码

你心中必定会有疑问,为什么 <input>  要绑定一个 change 事件呢?

在 HTML 中,表单元素(如<input><textarea><select>)之类的表单元素一般本身维护 state,并根据用户输入进行更新。而在 React 中,可变状态一般保存在组件的 state 属性中,而且只能经过使用 setState()来更新。被 React 以这种方式控制取值的表单输入元素就叫作“受控组件”。

总结下 React 受控组件更新 state 的流程: (1) 能够经过在初始 state 中设置表单的默认值。 (2) 每当表单的值发生变化时,调用 onChange 事件处理器。 (3) 事件处理器经过合成事件对象 e 拿到改变后的状态,并更新应用的 state。 (4) setState 触发视图的从新渲染,完成表单组件值的更新。

非受控组件

先来看一段代码:

class FormDemo1 extends React.Component {
  constructor(props) {
    super(props)
    this.content = React.createRef();
  }
  handleSubmit=(e)=>{
    e.preventDefault();
    const { value } = this.content.current;
    console.log(value);
  }

  render() {
    return <form onSubmit={this.handleSubmit}> <input ref={this.content} type="text" defaultValue="frank" /> <button type="submit">Submit</button> </form> } } 复制代码

在 React 中,非受控组件是一种反模式,它的值不受组件自身的 state 或 props 控制。一般,须要经过为其添加 ref 来访问渲染后的底层 DOM 元素。

受控组件和非受控组件的最大区别是:非受控组件的状态并不会受应用状态的控制,而受控组件的值来自于组件的 state。

小结

因为React在面试中占有很是的比重,所以关于React技术栈的面试文章将分解为多篇进行讲解

相关文章
相关标签/搜索