关于react.js:前端React面试题总结

4次阅读

共计 29455 个字符,预计需要花费 74 分钟才能阅读完成。

一、简介介绍下 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()          // 输入 : jack
jack.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}

// 创立 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 创立 store
const store = redux.createStore(reducer);

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

// 批改 store 中的 state
store.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 以反对 JSX
import {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 节点,整个流程完结。

正文完
 0