一、简介介绍下React,说说他们都有哪些个性

1.1 简介

React是一个构建用户界面的 JavaScript 库,是一个UI 层面的解决方案。React遵循组件设计模式、申明式编程范式和函数式编程概念,以使前端利用程序开发更高效。同时,React应用虚构DOM来无效地操作DOM,遵循从高阶组件到低阶组件的单向数据流。同时,React能够帮忙咱们将界面拆分成各个独立的小块,每一个块就是组件,这些组件之间能够组合、嵌套,形成一个整体页面。

语法上,React 类组件应用一个名为 render() 的办法或者函数组件return,接管输出的数据并返回须要展现的内容,比方:

class HelloMessage extends React.Component {  render() {    return (      <div>        Hello {this.props.name}      </div>    );  }}ReactDOM.render(  <HelloMessage name="Taylor" />,  document.getElementById('hello-example'));

上述这种相似 XML模式就是 JSX,最终会被babel编译为非法的JS语句调用。被传入的数据可在组件中通过 this.props 在 render() 拜访。

1.2 个性

React个性有很多,上面列举几个有个性的:

  • JSX语法
  • 单向数据绑定
  • 虚构DOM
  • 申明式编程
  • Component

1.2.1 申明式编程

申明式编程是一种编程范式,它关注的是你要做什么,而不是如何做。它表白逻辑而不显式地定义步骤。这意味着咱们须要依据逻辑的计算来申明要显示的组件,如实现一个标记的地图:通过命令式创立地图、创立标记、以及在地图上增加的标记的步骤如下。

// 创立地图const map = new Map.map(document.getElementById('map'), {    zoom: 4,    center: {lat,lng}});// 创立标记const marker = new Map.marker({    position: {lat, lng},    title: 'Hello Marker'});// 地图上增加标记marker.setMap(map);

而用React实现上述性能,则如下:

<Map zoom={4} center={lat, lng}>    <Marker position={lat, lng} title={'Hello Marker'}/></Map>

申明式编程形式使得React组件很容易应用,最终的代码也更加简略易于保护。

1.2.2 Component

在React 中,所有皆为组件。通常将应用程序的整个逻辑合成为小的单个局部。 咱们将每个独自的局部称为组件。组件能够是一个函数或者是一个类,承受数据输出,解决它并返回在UI中出现的React元素,函数式组件如下:

const Header = () => {    return(        <Jumbotron style={{backgroundColor:'orange'}}>            <h1>TODO App</h1>        </Jumbotron>    )}

而对于须要扭转状态来说,有状态组件的定义如下:

class Dashboard extends React.Component {    constructor(props){        super(props);        this.state = {        }    }    render() {        return (            <div className="dashboard">                 <ToDoForm />                <ToDolist />            </div>        );    }}

能够看到,React的组件有如下的一些个性:

  • 可组合:个组件易于和其它组件一起应用,或者嵌套在另一个组件外部。
  • 可重用:每个组件都是具备独立性能的,它能够被应用在多个UI场景。
  • 可保护:每个小的组件仅仅蕴含本身的逻辑,更容易被了解和保护。

二、Real DOM和 Virtual DOM 的区别

2.1 Real DOM

Real DOM是绝对Virtual DOM 来说的, Real DOM指的是文档对象模型,是一个结构化文本的形象,在页面渲染出的每一个结点都是一个实在DOM构造,咱们能够应用浏览器的DevTool来查看,如下。

Virtual Dom,实质上是以 JavaScript 对象模式存在的对 DOM 的形容。创立虚构DOM目标就是为了更好将虚构的节点渲染到页面视图中,虚构DOM对象的节点与实在DOM的属性一一呼应。在React中,JSX是其一大个性,能够让你在JS中通过应用XML的形式去间接申明界面的DOM构造。

const vDom = <h1>Hello World</h1> // 创立h1标签,左边千万不能加引号const root = document.getElementById('root') // 找到<div id="root"></div>节点ReactDOM.render(vDom, root) // 把创立的h1标签渲染到root节点上

在上述代码中,ReactDOM.render()用于将创立好的虚构DOM节点插入到某个实在节点上,并渲染到页面上。实际上,JSX是一种语法糖,在应用过程中会被babel进行编译转化成JS代码,上述VDOM转化为如下。

const vDom = React.createElement(  'h1',   { className: 'hClass', id: 'hId' },  'hello world')

能够看到,JSX就是为了简化间接调用React.createElement() 办法:

  • 第一个参数是标签名,例如h1、span、table...。
  • 第二个参数是个对象,外面存着标签的一些属性,例如id、class等。
  • 第三个参数是节点中的文本。

通过console.log(VDOM),则可能失去虚构DOM的相干信息。

所以,从下面的例子可知,JSX通过babel的形式转化成React.createElement执行,返回值是一个对象,也就是虚构DOM。、

2.2 区别

Real DOM和 Virtual DOM的区别如下:

  • 虚构DOM不会进行排版与重绘操作,而实在DOM会频繁重排与重绘。
  • 虚构DOM的总损耗是“虚构DOM增删改+实在DOM差别增删改+排版与重绘”,实在DOM的总损耗是“实在DOM齐全增删改+排版与重绘”。

2.3 优缺点

实在DOM的劣势:

  • 易用

毛病:

  • 效率低,解析速度慢,内存占用量过高。
  • 性能差:频繁操作实在DOM,易于导致重绘与回流。

应用虚构DOM的劣势如下:

  • 简略不便:如果应用手动操作实在DOM来实现页面,繁琐又容易出错,在大规模利用下保护起来也很艰难。
  • 性能好:应用Virtual DOM,可能无效防止实在DOM数频繁更新,缩小屡次引起重绘与回流,进步性能。
    -跨平台:React借助虚构DOM, 带来了跨平台的能力,一套代码多端运行。

毛病如下:

  • 在一些性能要求极高的利用中虚构 DOM 无奈进行针对性的极致优化。
  • 首次渲染大量DOM时,因为多了一层虚构DOM的计算,速度比失常稍慢。

三、super()和super(props)有什么区别

3.1 ES6类

在ES6中,通过extends关键字实现类的继承,形式如下:

class sup {    constructor(name) {        this.name = name    }    printName() {        console.log(this.name)    }}class sub extends sup{    constructor(name,age) {        super(name) // super代表的事父类的构造函数        this.age = age    }    printAge() {        console.log(this.age)    }}let jack = new sub('jack',20)jack.printName()          //输入 : jackjack.printAge()           //输入 : 20

在下面的例子中,能够看到通过super关键字实现调用父类,super代替的是父类的构建函数,应用super(name)相当于调用sup.prototype.constructor.call(this,name)。如果在子类中不应用super,关键字,则会引发报错,如下:

报错的起因是 子类是没有本人的this对象的,它只能继承父类的this对象,而后对其进行加工。而super()就是将父类中的this对象继承给子类的,没有super() 子类就得不到this对象。如果先调用this,再初始化super(),同样是禁止的行为。

class sub extends sup{    constructor(name,age) {        this.age = age        super(name) // super代表的事父类的构造函数    }}

所以,在子类constructor中,必须先代用super能力援用this。

3.2 类组件

在React中,类组件是基于es6的标准实现的,继承React.Component,因而如果用到constructor就必须写super()才初始化this。这时候,在调用super()的时候,咱们个别都须要传入props作为参数,如果不传进去,React外部也会将其定义在组件实例中。

// React 外部const instance = new YourComponent(props);instance.props = props;

所以无论有没有constructor,在render中this.props都是能够应用的,这是React主动附带的,是能够不写的。

class HelloMessage extends React.Component{    render (){        return (            <div>nice to meet you! {this.props.name}</div>        );    }}

然而也不倡议应用super()代替super(props)。因为在React会在类组件构造函数生成实例后再给this.props赋值,所以在不传递props在super的状况下,调用this.props会返回undefined,如下。

class Button extends React.Component {  constructor(props) {    super();                             // 没传入 props    console.log(props);          // {}    console.log(this.props);  // undefined   // ...}

而传入props的则都能失常拜访,确保了 this.props 在构造函数执行结束之前已被赋值,更合乎逻辑。

class Button extends React.Component {  constructor(props) {    super(props); // 没传入 props    console.log(props);      //  {}    console.log(this.props); //  {}  // ...}

从下面的例子,咱们能够得出:

  • 在React中,类组件基于ES6,所以在constructor中必须应用super。
  • 在调用super过程,无论是否传入props,React外部都会将porps赋值给组件实例porps属性中。
  • 如果只调用了super(),那么this.props在super()和构造函数完结之间仍是undefined。

四、谈谈setState执行机制

4.1 什么是setState机制

在React中,一个组件的显示状态能够由数据状态和内部参数所决定,而数据状态就是state。当须要批改外面的值的状态时,就须要通过调用setState来扭转,从而达到更新组件外部数据的作用。

比方,上面的例子:

import React, { Component } from 'react'export default class App extends Component {    constructor(props) {        super(props);        this.state = {            message: "Hello World"        }    }    render() {        return (            <div>                <h2>{this.state.message}</h2>                <button onClick={e => this.changeText()}>面试官系列</button>            </div>        )    }    changeText() {        this.setState({            message: "JS每日一题"        })    }}

通过点击按钮触发onclick事件,执行this.setState办法更新state状态,而后从新执行render函数,从而导致页面的视图更新。如果想要间接批改state的状态,那么只须要调用setState即可。

changeText() {    this.state.message = "你好啊,世界";}

咱们会发现页面并不会有任何反馈,然而state的状态是曾经产生了扭转。这是因为React并不像vue2中调用Object.defineProperty数据响应式或者Vue3调用Proxy监听数据的变动,必须通过setState办法来告知react组件state曾经产生了扭转。
对于state办法的定义是从React.Component中继承,定义的源码如下:

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');};

4.2 更新形式

在应用setState更新数据的时候,setState的更新类型分成异步更新、同步更新。

4.2.1 异步更新

举个例子,有上面一段代码。

changeText() {  this.setState({    message: "你好啊"  })  console.log(this.state.message);         // Hello World}

下面的代码最终打印后果为Hello world,并不能在执行完setState之后立马拿到最新的state的后果。如果想要立即获取更新后的值,在第二个参数的回调中更新后会执行。

changeText() {  this.setState({    message: "你好啊"  }, () => {    console.log(this.state.message);   // 你好啊  });}

4.2.2 同步更新

上面是应用setTimeout同步更新的例子。

changeText() {  setTimeout(() => {    this.setState({      message: "你好啊    });    console.log(this.state.message); // 你好啊  }, 0);}

4.2.3 批量更新

有时候,咱们须要解决批量更新的状况,先给出一个例子:

handleClick = () => {    this.setState({        count: this.state.count + 1,    })    console.log(this.state.count) // 1    this.setState({        count: this.state.count + 1,    })    console.log(this.state.count) // 1    this.setState({        count: this.state.count + 1,    })    console.log(this.state.count) // 1}

当咱们点击按钮触发事件,打印的都是 1,页面显示 count 的值为 2。对同一个值进行屡次 setState , setState 的批量更新策略会对其进行笼罩,取最初一次的执行后果。因而,下面的代码等价于上面的代码:

Object.assign(  previousState,  {index: state.count+ 1},  {index: state.count+ 1},  ...)

因为前面的数据会笼罩后面的更改,所以最终只加了一次。如果是下一个state依赖前一个state的话,举荐给setState一个参数传入一个function,如下:

onClick = () => {    this.setState((prevState, props) => {      return {count: prevState.count + 1};    });    this.setState((prevState, props) => {      return {count: prevState.count + 1};    });}

而在setTimeout或者原生dom事件中,因为是同步的操作,所以并不会进行笼罩景象。

五、React事件绑定

5.1 事件绑定

当咱们须要解决点击事件时,几句须要给事件增加一些绑定操作,即所谓的事件绑定。上面是一个最常见的事件绑定:

class ShowAlert extends React.Component {  showAlert() {    console.log("Hi");  }  render() {    return <button onClick={this.showAlert}>show</button>;  }}

能够看到,事件绑定的办法须要应用{}包住。上述的代码看似没有问题,然而当将处理函数输入代码换成console.log(this)的时候,点击按钮,则会发现控制台输入undefined。

5.2 常见绑定形式

React常见的事件绑定形式有如下几种:

  • render办法中应用bind
  • render办法中应用箭头函数
  • constructor中bind
  • 定义阶段应用箭头函数绑定

5.2.1 render办法中应用bind

如果应用一个类组件,在其中给某个组件/元素一个onClick属性,它当初并会自定绑定其this到以后组件,解决这个问题的办法是在事件函数后应用.bind(this)将this绑定到以后组件中。

class App extends React.Component {  handleClick() {    console.log('this > ', this);  }  render() {    return (      <div onClick={this.handleClick.bind(this)}>test</div>    )  }}

这种形式在组件每次render渲染的时候,都会从新进行bind的操作,影响性能。

5.2.2 render办法中应用箭头函数

通过ES6的上下文来将this的指向绑定给以后组件,同样再每一次render的时候都会生成新的办法,影响性能。

class App extends React.Component {  handleClick() {    console.log('this > ', this);  }  render() {    return (      <div onClick={e => this.handleClick(e)}>test</div>    )  }}

5.2.3 constructor中bind

在constructor中事后bind以后组件,能够防止在render操作中反复绑定。

class App extends React.Component {  constructor(props) {    super(props);    this.handleClick = this.handleClick.bind(this);  }  handleClick() {    console.log('this > ', this);  }  render() {    return (      <div onClick={this.handleClick}>test</div>    )  }}

5.2.4 定义阶段应用箭头函数绑定

跟上述形式三一样,可能防止在render操作中反复绑定,实现也十分的简略。

class App extends React.Component {  constructor(props) {    super(props);  }  handleClick = () => {    console.log('this > ', this);  }  render() {    return (      <div onClick={this.handleClick}>test</div>    )  }}

5.3 区别

上述四种办法的形式,区别次要如下:

  • 编写方面:形式一、形式二写法简略,形式三的编写过于繁杂
  • 性能方面:形式一和形式二在每次组件render的时候都会生成新的办法实例,性能问题欠缺。若该函数作为属性值传给子组件的时候,都会导致额定的渲染。而形式三、形式四只会生成一个办法实例。

综合上述,形式四是最优的事件绑定形式。

六、React中组件通信

6.1 组件通信

组件是Vue中和React前端框架最外围的根底思维,也是区别其余js框架最显著的特色之一。通常,一个实现的简单业务页面就是由许多的根底组件形成的。而组件之间须要传递音讯,就会波及到通信。通信指的是发送者通过某种媒体以某种格局来传递信息到收信者以达到某个目标,狭义上,任何信息的交通都是通信。

6.2 通信的几种形式

组件传递的形式有很多种,依据传送者和接收者能够分为如下几种:

  • 父组件向子组件传递
  • 子组件向父组件传递
  • 兄弟组件之间的通信
  • 父组件向后辈组件传递
  • 非关系组件传递

6.2.1 父组件向子组件传递音讯

因为React的数据流动为单向的,父组件向子组件传递是最常见的形式。父组件在调用子组件的时候,只须要在子组件标签内传递参数,子组件通过props属性就能接管父组件传递过去的参数即可。

function EmailInput(props) {  return (    <label>      Email: <input value={props.email} />    </label>  );}const element = <EmailInput email="123124132@163.com" />;

6.2.2 子组件向父组件传递音讯

子组件向父组件通信的基本思路是,父组件向子组件传一个函数,而后通过这个函数的回调,拿到子组件传过来的值。父组件对应代码如下:

class Parents extends Component {  constructor() {    super();    this.state = {      price: 0    };  }  getItemPrice(e) {    this.setState({      price: e    });  }  render() {    return (      <div>        <div>price: {this.state.price}</div>        {/* 向子组件中传入一个函数  */}        <Child getPrice={this.getItemPrice.bind(this)} />      </div>    );  }}

子组件对应代码如下:

class Child extends Component {  clickGoods(e) {    // 在此函数中传入值    this.props.getPrice(e);  }  render() {    return (      <div>        <button onClick={this.clickGoods.bind(this, 100)}>goods1</button>        <button onClick={this.clickGoods.bind(this, 1000)}>goods2</button>      </div>    );  }}

6.2.3 兄弟组件之间的通信

如果是兄弟组件之间的传递,则父组件作为中间层来实现数据的互通,通过应用父组件传递。

class Parent extends React.Component {  constructor(props) {    super(props)    this.state = {count: 0}  }  setCount = () => {    this.setState({count: this.state.count + 1})  }  render() {    return (      <div>        <SiblingA          count={this.state.count}        />        <SiblingB          onClick={this.setCount}        />      </div>    );  }}

6.2.4 隔代组传递音讯

父组件向后辈组件传递数据是一件最一般的事件,就像全局数据一样。应用context提供了组件之间通信的一种形式,能够共享数据,其余数据都能读取对应的数据。通过应用React.createContext创立一个context。

 const PriceContext = React.createContext('price')

context创立胜利后,其下存在Provider组件用于创立数据源,Consumer组件用于接收数据,应用实例如下:Provider组件通过value属性用于给后辈组件传递数据。

<PriceContext.Provider value={100}></PriceContext.Provider>

如果想要获取Provider传递的数据,能够通过Consumer组件或者或者应用contextType属性接管,对应别离如下:

class MyClass extends React.Component {  static contextType = PriceContext;  render() {    let price = this.context;    /* 基于这个值进行渲染工作 */  }}

Consumer组件代码如下:

<PriceContext.Consumer>    { /*这里是一个函数*/ }    {        price => <div>price:{price}</div>    }</PriceContext.Consumer>

6.2.5 非关系组件传递音讯

如果组件之间关系类型比较复杂的状况,倡议将数据进行一个全局资源管理,从而实现通信,例如redux,mobx等。

七、React Hooks

7.1 Hook

Hook 是 React 16.8 的新增个性。它能够让你在不编写 class 的状况下应用 state 以及其余的 React 个性。至于为什么引入hook,官网给出的动机是解决长时间应用和保护React过程中常遇到的问题,例如:

  • 难以重用和共享组件中的与状态相干的逻辑
  • 逻辑简单的组件难以开发与保护,当咱们的组件须要解决多个互不相干的 local state 时,每个生命周期函数中可能会蕴含着各种互不相干的逻辑在外面
  • 类组件中的this减少学习老本,类组件在基于现有工具的优化上存在些许问题
  • 因为业务变动,函数组件不得不改为类组件等等

函数组件也被称为无状态的组件,在刚开始只负责渲染的一些工作。因而,应用Hook技术当初的函数组件也能够是有状态的组件,外部也能够保护本身的状态以及做一些逻辑方面的解决。

7.2 Hooks函数

Hooks让咱们的函数组件领有了类组件的个性,例如组件内的状态、生命周期等。为了实现状态治理,Hook提供了很多有用的Hooks函数,常见的有:

  • useState
  • useEffect
  • 其余

useState

首先给出一个例子,如下:

import React, { useState } from 'react';function Example() {  // 申明一个叫 "count" 的 state 变量  const [count, setCount] = useState(0);  return (    <div>      <p>You clicked {count} times</p >      <button onClick={() => setCount(count + 1)}>        Click me      </button>    </div>  );}

在函数组件中通过useState实现函数外部保护state,参数为state默认的值,返回值是一个数组,第一个值为以后的state,第二个值为更新state的函数。该函数组件如果用类组件实现,代码如下:

class Example extends React.Component {  constructor(props) {    super(props);    this.state = {      count: 0    };  }  render() {    return (      <div>        <p>You clicked {this.state.count} times</p >        <button onClick={() => this.setState({ count: this.state.count + 1 })}>          Click me        </button>      </div>    );  }}

从上述两种代码剖析,能够看出两者区别:

  • state申明形式:在函数组件中通过 useState 间接获取,类组件通过constructor 构造函数中设置
  • state读取形式:在函数组件中间接应用变量,类组件通过this.state.count的形式获取
  • state更新形式:在函数组件中通过 setCount 更新,类组件通过this.setState()

总的来讲,useState 应用起来更为简洁,缩小了this指向不明确的状况。

useEffect

useEffect能够让咱们在函数组件中进行一些带有副作用的操作。比方,上面是一个计时器的例子:

class Example extends React.Component {  constructor(props) {    super(props);    this.state = {      count: 0    };  }  componentDidMount() {    document.title = `You clicked ${this.state.count} times`;  }  componentDidUpdate() {    document.title = `You clicked ${this.state.count} times`;  }  render() {    return (      <div>        <p>You clicked {this.state.count} times</p >        <button onClick={() => this.setState({ count: this.state.count + 1 })}>          Click me        </button>      </div>    );  }}

从下面能够看见,组件在加载和更新阶段都执行同样操作。而如果应用useEffect后,则可能将雷同的逻辑抽离进去,这是类组件不具备的办法。

import React, { useState, useEffect } from 'react';function Example() {  const [count, setCount] = useState(0);   useEffect(() => {    document.title = `You clicked ${count} times`;  });  return (    <div>      <p>You clicked {count} times</p >      <button onClick={() => setCount(count + 1)}>        Click me      </button>    </div>  );}

useEffect的呃第一个参数承受一个回调函数。默认状况下,useEffect会在第一次渲染和更新之后都会执行,相当于在componentDidMount和componentDidUpdate两个生命周期函数中执行回调。

如果某些特定值在两次重渲染之间没有发生变化,你能够跳过对 effect 的调用,这时候只须要传入第二个参数,如下:

useEffect(() => {  document.title = `You clicked ${count} times`;}, [count]);      // 仅在 count 更改时更新

上述传入第二个参数后,如果 count 的值是 5,而且咱们的组件重渲染的时候 count 还是等于 5,React 将对前一次渲染的 [5] 和后一次渲染的 [5] 进行比拟,如果是相等则跳过effects执行。

回调函数中能够返回一个革除函数,这是effect可选的革除机制,相当于类组件中componentwillUnmount生命周期函数,可做一些革除副作用的操作,如下:

useEffect(() => {    function handleStatusChange(status) {        setIsOnline(status.isOnline);    }    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);    return () => {        ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);    };});

能够发现,useEffect相当于componentDidMount,componentDidUpdate 和 componentWillUnmount 这三个生命周期函数的组合。

其它 hooks

除了下面两个比拟常见的外,React还有很多额定的hooks。

  • useReducer
  • useCallback
  • useMemo
  • useRef

7.3 总结

通过对下面的初步意识,能够看到hooks可能更容易解决状态相干的重用的问题:

  • 每调用useHook一次都会生成一份独立的状态
  • 通过自定义hook可能更好的封装咱们的性能

编写hooks为函数式编程,每个性能都包裹在函数中,整体格调更清新,更优雅。

八、谈谈你对Redux的了解

8.1 概念

React是用于构建用户界面的,帮忙咱们解决渲染DOM的过程。而在整个利用中会存在很多个组件,每个组件的state是由本身进行治理,包含组件定义本身的state、组件之间的通信通过props传递、应用Context实现数据共享。

如果让每个组件都存储本身相干的状态,实践上来讲不会影响利用的运行,但在开发及后续维护阶段,咱们将破费大量精力去查问状态的变动过程。这种状况下,如果将所有的状态进行集中管理,当须要更新状态的时候,仅须要对这个治理集中处理,而不必去关怀状态是如何散发到每一个组件外部的。

Redux实现了状态的集中管理,应用时须要遵循三大根本准则:

  • 繁多数据源
  • state 是只读的
  • 应用纯函数来执行批改

须要阐明的是,Redux并不是只利用在React中,还与其余界面库一起应用,如Vue。

8.2 工作原理

redux状态治理次要分为三个局部: Action Creactor、Store和Reducer。其中, store 是用于数据的公共存储空间。一个组件扭转了 store 里的数据内容,其余组件就能感知到 store 的变动,再来取数据,从而间接的实现了这些数据传递的性能。

工作流程示意图如下图所示。

具体的介绍能够查看:Redux 三大外围概念

8.3 应用

首先,须要创立一个store的公共数据区域。

import { createStore } from 'redux' // 引入一个第三方的办法const store = createStore() // 创立数据的公共存储区域(管理员)

而后,再创立一个记录本去辅助治理数据,也就是reduecer,实质就是一个函数,接管两个参数state和action,并返回state。

// 设置默认值const initialState = {  counter: 0}const reducer = (state = initialState, action) => {}

接着,应用createStore函数将state和action建设连贯,如下。

const store = createStore(reducer)

如果想要获取store外面的数据,则通过store.getState()来获取以后state,如下。

console.log(store.getState());

上面再看看如何更改store外面数据。是通过dispatch来派发action,通常action中都会有type属性,也能够携带其余的数据。

store.dispatch({  type: "INCREMENT"})store.dispath({  type: "DECREMENT"})store.dispatch({  type: "ADD_NUMBER",  number: 5})

接着,咱们再来看看批改reducer中的解决逻辑。

const reducer = (state = initialState, action) => {  switch (action.type) {    case "INCREMENT":      return {...state, counter: state.counter + 1};    case "DECREMENT":      return {...state, counter: state.counter - 1};    case "ADD_NUMBER":      return {...state, counter: state.counter + action.number}    default:       return state;  }}

留神,reducer是一个纯函数,不须要间接批改state。接着,当派发action之后,既能够通过store.subscribe监听store的变动。

store.subscribe(() => {  console.log(store.getState());})

在React我的项目中,会搭配react-redux进行应用。

const redux = require('redux');const initialState = {  counter: 0}// 创立reducerconst reducer = (state = initialState, action) => {  switch (action.type) {    case "INCREMENT":      return {...state, counter: state.counter + 1};    case "DECREMENT":      return {...state, counter: state.counter - 1};    case "ADD_NUMBER":      return {...state, counter: state.counter + action.number}    default:       return state;  }}// 依据reducer创立storeconst store = redux.createStore(reducer);store.subscribe(() => {  console.log(store.getState());})// 批改store中的statestore.dispatch({  type: "INCREMENT"})// console.log(store.getState());store.dispatch({  type: "DECREMENT"})// console.log(store.getState());store.dispatch({  type: "ADD_NUMBER",  number: 5})// console.log(store.getState());
  • createStore能够帮忙创立 store。
  • store.dispatch 帮忙派发 action , action 会传递给 store。
  • store.getState 这个办法能够帮忙获取 store 里边所有的数据内容。
  • store.subscrible 办法订阅 store 的扭转,只有 store 产生扭转, store.subscrible 这个函数接管的这个回调函数就会被执行。

九、Redux中间件

9.1 什么是中间件

中间件(Middleware)是介于利用零碎和系统软件之间的一类软件,它应用系统软件所提供的根底服务(性能),连接网络上利用零碎的各个局部或不同的利用,可能达到资源共享、性能共享的目标。

后面,咱们理解到了Redux整个工作流程,当action收回之后,reducer立刻算出state,整个过程是一个同步的操作。那么如果须要反对异步操作,或者反对错误处理、日志监控,这个过程就能够用上中间件。

Redux中,中间件就是放在就是在dispatch过程,在散发action进行拦挡解决,如下图:

其本质上一个函数,对store.dispatch办法进行了革新,在收回 Action 和执行 Reducer 这两步之间,增加了其余性能。

9.2 罕用中间件

优良的redux中间件有很多,比方:

  • redux-thunk:用于异步操作
  • redux-logger:用于日志记录

上述的中间件都须要通过applyMiddlewares进行注册,作用是将所有的中间件组成一个数组,顺次执行,作为第二个参数传入到createStore中。

const store = createStore(  reducer,  applyMiddleware(thunk, logger));

9.2.1 redux-thunk

redux-thunk是官网举荐的异步解决中间件。默认状况下的dispatch(action),action须要是一个JavaScript的对象。

redux-thunk中间件会判断你以后传进来的数据类型,如果是一个函数,将会给函数传入参数值(dispatch,getState)。

  • dispatch函数用于咱们之后再次派发action。
  • getState函数思考到咱们之后的一些操作须要依赖原来的状态,用于让咱们能够获取之前的一些状态。

所以,dispatch能够写成下述函数的模式。

const getHomeMultidataAction = () => {  return (dispatch) => {    axios.get("http://xxx.xx.xx.xx/test").then(res => {      const data = res.data.data;      dispatch(changeBannersAction(data.banner.list));      dispatch(changeRecommendsAction(data.recommend.list));    })  }}

9.2.2 redux-logger

如果想要实现一个日志性能,则能够应用现成的redux-logger,如下。

import { applyMiddleware, createStore } from 'redux';import createLogger from 'redux-logger';const logger = createLogger();const store = createStore(  reducer,  applyMiddleware(logger));

9.3 Redux源码剖析

首先,咱们来看看applyMiddlewares的源码:

export default function applyMiddleware(...middlewares) {  return (createStore) => (reducer, preloadedState, enhancer) => {    var store = createStore(reducer, preloadedState, enhancer);    var dispatch = store.dispatch;    var chain = [];    var middlewareAPI = {      getState: store.getState,      dispatch: (action) => dispatch(action)    };    chain = middlewares.map(middleware => middleware(middlewareAPI));    dispatch = compose(...chain)(store.dispatch);    return {...store, dispatch}  }}

能够看到,所有中间件被放进了一个数组chain,而后嵌套执行,最初执行store.dispatch,而中间件外部(middlewareAPI)能够拿到getState和dispatch这两个办法

通过下面的剖析,咱们理解到了redux-thunk的根本应用。同时,外部会将dispatch进行一个判断,而后执行对应操作,原理如下:

function patchThunk(store) {    let next = store.dispatch;    function dispatchAndThunk(action) {        if (typeof action === "function") {            action(store.dispatch, store.getState);        } else {            next(action);        }    }    store.dispatch = dispatchAndThunk;}

上面,咱们本人实现一个日志输入的拦挡。

let next = store.dispatch;function dispatchAndLog(action) {  console.log("dispatching:", addAction(10));  next(addAction(5));  console.log("新的state:", store.getState());}store.dispatch = dispatchAndLog;

十、如何进步组件的渲染效率

咱们晓得,React 基于虚构 DOM 和高效 Diff 算法的完满配合,实现了对 DOM 最小粒度的更新,大多数状况下,React 对 DOM 的渲染效率足以咱们的业务日常。不过,对于简单业务场景,性能问题仍然会困扰咱们。此时须要采取一些措施来晋升运行性能,防止不必要的渲染则是业务中常见的优化伎俩之一。

10.1 实现计划

咱们理解到,render的触发机会简略来讲就是类组件通过调用setState办法, 就会导致render,父组件一旦产生render渲染,子组件肯定也会执行render渲染。父组件渲染导致子组件渲染,子组件并没有产生任何扭转,这时候就能够从防止无谓的渲染,具体实现的形式有如下:

  • shouldComponentUpdate
  • PureComponent
  • React.memo

10.2 波及生命周期函数

102.1 shouldComponentUpdate

通过shouldComponentUpdate生命周期函数来比对 state 和 props,确定是否要从新渲染。默认状况下返回true示意从新渲染,如果不心愿组件从新渲染,返回 false 即可。

10.2.2 PureComponent

跟shouldComponentUpdate 原理基本一致,通过对 props 和 state的浅比拟后果来实现 shouldComponentUpdate,源码大抵如下:

if (this._compositeType === CompositeTypes.PureClass) {    shouldUpdate = !shallowEqual(prevProps, nextProps) || ! shallowEqual(inst.state, nextState);}

shallowEqual对应办法源码如下:

const hasOwnProperty = Object.prototype.hasOwnProperty;/** * is 办法来判断两个值是否是相等的值,为何这么写能够移步 MDN 的文档 * https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/is */function is(x: mixed, y: mixed): boolean {  if (x === y) {    return x !== 0 || y !== 0 || 1 / x === 1 / y;  } else {    return x !== x && y !== y;  }}function shallowEqual(objA: mixed, objB: mixed): boolean {  // 首先对根本类型进行比拟  if (is(objA, objB)) {    return true;  }  if (typeof objA !== 'object' || objA === null ||      typeof objB !== 'object' || objB === null) {    return false;  }  const keysA = Object.keys(objA);  const keysB = Object.keys(objB);  // 长度不相等间接返回false  if (keysA.length !== keysB.length) {    return false;  }  // key相等的状况下,再去循环比拟  for (let i = 0; i < keysA.length; i++) {    if (      !hasOwnProperty.call(objB, keysA[i]) ||      !is(objA[keysA[i]], objB[keysA[i]])    ) {      return false;    }  }  return true;}

10.2.3 React.memo

React.memo用来缓存组件的渲染,防止不必要的更新,其实也是一个高阶组件,与 PureComponent 非常相似。但不同的是, React.memo 只能用于函数组件。

import { memo } from 'react';function Button(props) {  // Component code}export default memo(Button);

如果须要深层次比拟,这时候能够给memo第二个参数传递比拟函数。

function arePropsEqual(prevProps, nextProps) {  // your code  return prevProps === nextProps;}export default memo(Button, arePropsEqual);

10.3 总结

在理论开发过程中,前端性能问题是一个必须思考的问题,随着业务的简单,遇到性能问题的概率也在增高。

除此之外,倡议将页面进行更小的颗粒化,如果一个过大,当状态产生批改的时候,就会导致整个大组件的渲染,而对组件进行拆分后,粒度变小了,也可能缩小子组件不必要的渲染。

十一、对Fiber架构的了解

11.1 背景

JavaScript 引擎和页面渲染引擎两个线程是互斥的,当其中一个线程执行时,另一个线程只能挂起期待。如果 JavaScript 线程长时间地占用了主线程,那么渲染层面的更新就不得不长时间地期待,界面长时间不更新,会导致页面响应度变差,用户可能会感觉到卡顿。

而这也正是 React 15 的 Stack Reconciler 所面临的问题,当 React 在渲染组件时,从开始到渲染实现整个过程是零打碎敲的,无奈中断。如果组件较大,那么js线程会始终执行,而后等到整棵VDOM树计算实现后,才会交给渲染的线程。这就会导致一些用户交互、动画等工作无奈立刻失去解决,导致卡顿的状况。

11.2 React Fiber

eact Fiber 是 Facebook 破费两年余工夫对 React 做出的一个重大扭转与优化,是对 React 外围算法的一次从新实现。从Facebook在 React Conf 2017 会议上确认,React Fiber 在React 16 版本公布。

在React中,次要做了以下的操作:

  • 为每个减少了优先级,优先级高的工作能够中断低优先级的工作。而后再从新,留神是从新执行优先级低的工作。
  • 减少了异步工作,调用requestIdleCallback api,浏览器闲暇的时候执行。
  • dom diff树变成了链表,一个dom对应两个fiber(一个链表),对应两个队列,这都是为找到被中断的工作,从新执行。

从架构角度来看,Fiber 是对 React 外围算法(即和谐过程)的重写。从编码角度来看,Fiber 是 React 外部所定义的一种数据结构,它是 Fiber 树结构的节点单位,也就是 React 16 新架构下的虚构DOM。

一个 fiber 就是一个 JavaScript 对象,蕴含了元素的信息、该元素的更新操作队列、类型,其数据结构如下:

type Fiber = {  // 用于标记fiber的WorkTag类型,次要示意以后fiber代表的组件类型如FunctionComponent、ClassComponent等  tag: WorkTag,  // ReactElement外面的key  key: null | string,  // ReactElement.type,调用`createElement`的第一个参数  elementType: any,  // The resolved function/class/ associated with this fiber.  // 示意以后代表的节点类型  type: any,  // 示意以后FiberNode对应的element组件实例  stateNode: any,  // 指向他在Fiber节点树中的`parent`,用来在解决完这个节点之后向上返回  return: Fiber | null,  // 指向本人的第一个子节点  child: Fiber | null,  // 指向本人的兄弟构造,兄弟节点的return指向同一个父节点  sibling: Fiber | null,  index: number,  ref: null | (((handle: mixed) => void) & { _stringRef: ?string }) | RefObject,  // 以后处理过程中的组件props对象  pendingProps: any,  // 上一次渲染实现之后的props  memoizedProps: any,  // 该Fiber对应的组件产生的Update会寄存在这个队列外面  updateQueue: UpdateQueue<any> | null,  // 上一次渲染的时候的state  memoizedState: any,  // 一个列表,寄存这个Fiber依赖的context  firstContextDependency: ContextDependency<mixed> | null,  mode: TypeOfMode,  // Effect  // 用来记录Side Effect  effectTag: SideEffectTag,  // 单链表用来疾速查找下一个side effect  nextEffect: Fiber | null,  // 子树中第一个side effect  firstEffect: Fiber | null,  // 子树中最初一个side effect  lastEffect: Fiber | null,  // 代表工作在将来的哪个工夫点应该被实现,之后版本改名为 lanes  expirationTime: ExpirationTime,  // 疾速确定子树中是否有不在期待的变动  childExpirationTime: ExpirationTime,  // fiber的版本池,即记录fiber更新过程,便于复原  alternate: Fiber | null,}

11.3 解决方案

Fiber把渲染更新过程拆分成多个子工作,每次只做一小部分,做完看是否还有剩余时间,如果有持续下一个工作;如果没有,挂起当前任务,将工夫控制权交给主线程,等主线程不忙的时候在继续执行。

即能够中断与复原,复原后也能够复用之前的中间状态,并给不同的工作赋予不同的优先级,其中每个工作更新单元为 React Element 对应的 Fiber 节点。

实现的上述形式的是requestIdleCallback办法,window.requestIdleCallback()办法将在浏览器的闲暇时段内调用的函数排队。这使开发者可能在主事件循环上执行后盾和低优先级工作,而不会影响提早要害事件,如动画和输出响应。

首先 ,React 中工作切割为多个步骤,分批实现。在实现一部分工作之后,将控制权交回给浏览器,让浏览器有工夫再进行页面的渲染。等浏览器忙完之后有剩余时间,再持续之前 React 未实现的工作,是一种单干式调度。

该实现过程是基于 Fiber 节点实现,作为动态的数据结构来说,每个 Fiber 节点对应一个 React element,保留了该组件的类型(函数组件/类组件/原生组件等等)、对应的 DOM 节点等信息。作为动静的工作单元来说,每个 Fiber 节点保留了本次更新中该组件扭转的状态、要执行的工作。

每个 Fiber 节点有个对应的 React element,多个 Fiber 节点依据如下三个属性构建一颗树。

// 指向父级Fiber节点this.return = null// 指向子Fiber节点this.child = null// 指向左边第一个兄弟Fiber节点this.sibling = null

十二、 React 性能优化的伎俩有哪些

12.1 render渲染

React凭借virtual DOM和diff算法领有高效的性能,然而某些状况下,性能显著能够进一步提高。咱们晓得,类组件通过调用setState办法, 就会导致render,父组件一旦产生render渲染,子组件肯定也会执行render渲染。当咱们想要更新一个子组件的时候,如更新的绿色局部的内容:

现实状态下,咱们只调用该门路下的组件render,执行对应组件的渲染即可。

不过,React的默认做法是调用所有组件的render,再对生成的虚构DOM进行比照。

因而,默认的做法是十分节约性能的。

12.2 优化计划

蔚来防止不必要的render,咱们后面介绍了能够通过shouldComponentUpdate、PureComponent、React.memo来进行优化。除此之外,性能优化常见的还有如下一些:

  • 防止应用内联函数
  • 应用 React Fragments 防止额定标记
  • 应用 Immutable
  • 懒加载组件
  • 事件绑定形式
  • 服务端渲染

12.2.1 防止应用内联函数

如果咱们应用内联函数,则每次调用render函数时都会创立一个新的函数实例,比方:

import React from "react";export default class InlineFunctionComponent extends React.Component {  render() {    return (      <div>        <h1>Welcome Guest</h1>        <input type="button" onClick={(e) => { this.setState({inputValue: e.target.value}) }} value="Click For Inline Function" />      </div>    )  }}

正确的做法是,应该在组件外部创立一个函数,并将事件绑定到该函数自身。这样每次调用 render 时就不会创立独自的函数实例。

import React from "react";export default class InlineFunctionComponent extends React.Component {    setNewStateData = (event) => {    this.setState({      inputValue: e.target.value    })  }    render() {    return (      <div>        <h1>Welcome Guest</h1>        <input type="button" onClick={this.setNewStateData} value="Click For Inline Function" />      </div>    )  }}

12.2.2 应用 React Fragments 防止额定标记

用户创立新组件时,每个组件应具备单个父标签。父级不能有两个标签,所以顶部要有一个公共标签,所以咱们常常在组件顶部增加额定标签div。

这个额定标签除了充当父标签之外,并没有其余作用,这时候则能够应用fragement。其不会向组件引入任何额定标记,但它能够作为父级标签的作用。

export default class NestedRoutingComponent extends React.Component {    render() {        return (            <>                <h1>This is the Header Component</h1>                <h2>Welcome To Demo Page</h2>            </>        )    }}

12.2.3 懒加载组件

从工程方面思考,webpack存在代码拆分能力,能够为利用创立多个包,并在运行时动静加载,缩小初始包的大小。而在react中应用到了Suspense 和 lazy组件实现代码拆分性能,根本应用如下:

const johanComponent = React.lazy(() => import(/* webpackChunkName: "johanComponent" */ './myAwesome.component')); export const johanAsyncComponent = props => (  <React.Suspense fallback={<Spinner />}>    <johanComponent {...props} />  </React.Suspense>);

12.2.4 服务端渲染

采纳服务端渲染端形式,能够使用户更快的看到渲染实现的页面。服务端渲染,须要起一个node服务,能够应用express、koa等,调用react的renderToString办法,将根组件渲染成字符串,再输入到响应中:

import { renderToString } from "react-dom/server";import MyPage from "./MyPage";app.get("/", (req, res) => {  res.write("<!DOCTYPE html><html><head><title>My Page</title></head><body>");  res.write("<div id='content'>");    res.write(renderToString(<MyPage/>));  res.write("</div></body></html>");  res.end();});

而后,客户端应用render办法来生成HTML即可。

import ReactDOM from 'react-dom';import MyPage from "./MyPage";ReactDOM.render(<MyPage />, document.getElementById('app'));

十三、React服务端渲染

13.1 什么是服务端渲染

服务器渲染指的是由服务侧实现页面的 HTML 构造拼接的页面解决技术,发送到浏览器,而后为其绑定状态与事件,成为齐全可交互页面的过程。其解决的问题次要有两个:

  • SEO,因为搜索引擎爬虫抓取工具能够间接查看齐全渲染的页面
  • 减速首屏加载,解决首屏白屏问题

13.2 怎么做

在React中,实现SSR次要有两种模式:

  • 手动搭建一个 SSR 框架
  • 应用成熟的SSR 框架,如 Next.JS

上面以手动搭建一个 SSR 框架来阐明怎么实现SSR。首先,通过express启动一个app.js文件,用于监听3000端口的申请,当申请根目录时,返回HTML,如下:

const express = require('express')const app = express()app.get('/', (req,res) => res.send(`<html>   <head>       <title>ssr demo</title>   </head>   <body>       Hello world   </body></html>`))app.listen(3000, () => console.log('Exampleapp listening on port 3000!'))

而后,在服务器中编写React代码,在app.js中进行应援用:

import React from 'react'const Home = () =>{    return <div>home</div>}export default Home

为了让服务器可能辨认JSX,这里须要应用webpakc对我的项目进行打包转换,创立一个配置文件webpack.server.js并进行相干配置,如下所示。

const path = require('path')    //node的path模块const nodeExternals = require('webpack-node-externals')module.exports = {    target:'node',    mode:'development',           //开发模式    entry:'./app.js',             //入口    output: {                     //打包进口        filename:'bundle.js',     //打包后的文件名        path:path.resolve(__dirname,'build')    //寄存到根目录的build文件夹    },    externals: [nodeExternals()],  //放弃node中require的援用形式    module: {        rules: [{                  //打包规定           test:   /\.js?$/,       //对所有js文件进行打包           loader:'babel-loader',  //应用babel-loader进行打包           exclude: /node_modules/,//不打包node_modules中的js文件           options: {               presets: ['react','stage-0',['env', {                                   //loader时额定的打包规定,对react,JSX,ES6进行转换                    targets: {                        browsers: ['last 2versions']   //对支流浏览器最近两个版本进行兼容                    }               }]]           }       }]    }}

接着,借助react-dom提供了服务端渲染的 renderToString办法,负责把React组件解析成Html。

import express from 'express'import React from 'react'//引入React以反对JSX的语法import { renderToString } from 'react-dom/server'//引入renderToString办法import Home from'./src/containers/Home'const app= express()const content = renderToString(<Home/>)app.get('/',(req,res) => res.send(`<html>   <head>       <title>ssr demo</title>   </head>   <body>        ${content}   </body></html>`))app.listen(3001, () => console.log('Exampleapp listening on port 3001!'))

下面的过程中,曾经可能胜利将组件渲染到了页面上。然而,像一些事件处理的办法,是无奈在服务端实现,因而须要将组件代码在浏览器中再执行一遍,这种服务器端和客户端共用一套代码的形式就称之为同构。重构艰深讲就是一套React代码在服务器上运行一遍,达到浏览器又运行一遍:

  • 服务端渲染实现页面构造
  • 浏览器端渲染实现事件绑定

浏览器实现事件绑定的形式为让浏览器去拉取JS文件执行,让JS代码来管制,因而须要引入script标签。通过script标签为页面引入客户端执行的react代码,并通过express的static中间件为js文件配置路由,批改如下:

import express from 'express'import React from 'react'//引入React以反对JSX的语法import { renderToString } from'react-dom/server'//引入renderToString办法import Home from './src/containers/Home' const app = express()app.use(express.static('public'));//应用express提供的static中间件,中间件会将所有动态文件的路由指向public文件夹 const content = renderToString(<Home/>) app.get('/',(req,res)=>res.send(`<html>   <head>       <title>ssr demo</title>   </head>   <body>        ${content}   <script src="/index.js"></script>   </body></html>`)) app.listen(3001, () =>console.log('Example app listening on port 3001!'))

而后,在客户端执行以下react代码,新建webpack.client.js作为客户端React代码的webpack配置文件如下:

const path = require('path')                    //node的path模块module.exports = {    mode:'development',                         //开发模式    entry:'./src/client/index.js',              //入口    output: {                                   //打包进口        filename:'index.js',                    //打包后的文件名        path:path.resolve(__dirname,'public')   //寄存到根目录的build文件夹    },    module: {        rules: [{                               //打包规定           test:   /\.js?$/,                    //对所有js文件进行打包           loader:'babel-loader',               //应用babel-loader进行打包           exclude: /node_modules/,             //不打包node_modules中的js文件           options: {               presets: ['react','stage-0',['env', {                         //loader时额定的打包规定,这里对react,JSX进行转换                    targets: {                        browsers: ['last 2versions']   //对支流浏览器最近两个版本进行兼容                    }               }]]           }       }]    }}

这种办法就可能简略实现首页的React服务端渲染,过程如下图所示。

通常,一个利用会存在路由的状况,配置信息如下:

import React from 'react'                   //引入React以反对JSXimport { Route } from 'react-router-dom'    //引入路由import Home from './containers/Home'        //引入Home组件export default (    <div>        <Route path="/" exact component={Home}></Route>    </div>)

而后,能够通过index.js援用路由信息,如下:

import React from 'react'import ReactDom from 'react-dom'import { BrowserRouter } from'react-router-dom'import Router from'../Routers'const App= () => {    return (        <BrowserRouter>           {Router}        </BrowserRouter>    )}ReactDom.hydrate(<App/>, document.getElementById('root'))

这时候,控制台会存在报错信息,起因在于每个Route组件里面包裹着一层div,但服务端返回的代码中并没有这个div。解决办法只须要将路由信息在服务端执行一遍,应用应用StaticRouter来代替BrowserRouter,通过context进行参数传递。

import express from 'express'import React from 'react'//引入React以反对JSX的语法import { renderToString } from 'react-dom/server'//引入renderToString办法import { StaticRouter } from 'react-router-dom'import Router from '../Routers' const app = express()app.use(express.static('public'));//应用express提供的static中间件,中间件会将所有动态文件的路由指向public文件夹app.get('/',(req,res)=>{    const content  = renderToString((        //传入以后path        //context为必填参数,用于服务端渲染参数传递        <StaticRouter location={req.path} context={{}}>           {Router}        </StaticRouter>    ))    res.send(`   <html>       <head>           <title>ssr demo</title>       </head>       <body>       <div id="root">${content}</div>       <script src="/index.js"></script>       </body>   </html>    `)})app.listen(3001, () => console.log('Exampleapp listening on port 3001!'))

13.3 总结

整体React服务端渲染原理并不简单,具体如下:
Node server 接管客户端申请,失去以后的申请url 门路,而后在已有的路由表内查找到对应的组件,拿到须要申请的数据,将数据作为 props、context或者store 模式传入组件。

而后,基于 React 内置的服务端渲染办法 renderToString()把组件渲染为 html字符串在把最终的 html 进行输入前须要将数据注入到浏览器端.

浏览器开始进行渲染和节点比照,而后执行实现组件内事件绑定和一些交互,浏览器重用了服务端输入的 html 节点,整个流程完结。