React+Redux学习笔记

最近的实习过程中,学习了React+Redux,并使用React+Redux参与了一个完整项目的开发。这里对React和Redux中的关键概念做一下总结,并从实际使用感受上,与我个人熟悉的vue做一个简单的对比。

React

React是由facebook发布和维护的前端MVVM框架。

React中的几个概念

  • React Element(元素)

    • React Element是React的virtual DOM,本质上就是一个普通的对象,相较于浏览器的DOM更加轻量,它是Component的组成部分,是构建React应用的最小单元。
    • React Element通常由render函数返回的JSX创建,但其本质上只是React.createElement(component, props, …children)的语法糖。
    • React Element有类型之分,比如JSX 的标签名就决定了 React Element的类型,不同的JSX标签,就是不同类型的React Element。
    • React Element有内容(children)和属性(attribute),但是一旦React Element被创建之后,是无法改变其内容或属性的。即,React Element都是immutable 不可变的。
    • 更新界面的唯一办法是创建一个新的React Element,会由React DOM对比(diff)新旧React Element之后,只把改变了的部分更新到浏览器DOM上。

    React Element 示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    // JSX
    <h1 className="title">Hello, world!</h1>
    // JSX compile to
    React.createElement(
    "h1",
    {className: 'title'},
    'Hello, world!'
    )
    // React Element
    Object {
    $$typeof: Symbol(react.element),
    key: null,
    props: Object {
    children: "Hello, world!",
    className: "title"
    },
    ref: null,
    type: "h1",
    _owner: null,
    _store: Object {}
    }
  • Components(组件)

    React的主要特征就是由Components组成。Components可以将UI切分成一些的独立的、可复用的部件,这样你就只需专注于构建每一个单独的部件。
    所有的React组件必须像纯函数那样使用它们的props。

    • Functional && Class Component

    Functional Component 函数定义的组件需要是一个函数,接收单一的props对象作为参数,然后返回一个React Element。

    Class Component 使用ES6语法定义的组件,必须继承自React.Component(或PureComponent),实现render函数并返回React Element。Class Component可以有自己的state,用来实现局部状态(或封装)。

    Class Component有自身的生命周期钩子。组件的生命周期总体上可分成四个状态:

    1. Mounting:组件创建并插入DOM

      1
      2
      3
      4
      5
      6
      7
      constructor()
      static getDerivedStateFromProps()
      render()
      componentDidMount()
      // 以下不推荐使用
      UNSAFE_componentWillMount()
    2. Updating:props/state改变,造成update,进而re-render

      1
      2
      3
      4
      5
      6
      7
      8
      9
      static getDerivedStateFromProps()
      shouldComponentUpdate()
      render()
      getSnapshotBeforeUpdate()
      componentDidUpdate()
      // 以下不推荐使用
      UNSAFE_componentWillUpdate()
      UNSAFE_componentWillReceiveProps()
    3. Unmounting:移出 DOM

      1
      componentWillUnmount()
    4. Error Handling:渲染、生命周期方法、子组件constructor中的错误处理

      1
      componentDidCatch()()
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // Functional Component
    function Welcome(props) {
    return <h1>Hello, {props.name}</h1>;
    }
    // Class Component
    class Welcome extends React.Component {
    constructor(props) {
    super(props);
    this.state = {date: new Date()};
    }
    render() {
    return <h1>Hello, {this.props.name}</h1>;
    }
    }

    详细可查看官方文档React.Component API

    react-lifecycle示意图

  • Controlled Components && Uncontrolled Components

    Controlled Components受控组件通常用来实现表单(但并不是唯一的方式)。它有着可变的state,但state只能通过setState方法来更新,并且每个状态的改变都有一个与之相关的处理函数,这样就可以直接修改或验证用户输入。实现起来相对复杂。

    Uncontrolled Components非受控组件也可以用来实现表单,它可以使用ref从DOM中直接获取表单项的值,而不是为每个状态更新编写事件处理程序。实现方式更为简单。

  • Pure Components && Components

    Pure Components相对于Components有着更好的性能,源于它在对state和props的变化进行判断时,只做“浅比较(shallow comparison)”。也就是说原地对数组元素或对象属性的修改,而非生成新元素或对象,是无法触发Pure Components的更新行为的,可以使用Immutable.js来解决这个问题。

    Components也可以重写shouldComponentUpdate方法,来阻止Components更新。

Props VS State

Props和State变化的时候,都有可能会触发Components的更新,进而re-render,这取决于shouldComponentUpdate方法的返回值。其区别在于:

  • Props是组件外部传递进来的数据,是组件的输入;而State仅属于该组件自身,是组件的状态。
  • 对于组件而言,Props是不可变的,只能由外层组件或Redux等来改变(输入),而State仅可以通过组件自身的setState方法来改变。
  • State的变化如果想通知到外部组件,需要借助于“事件”,将state作为参数传递出去。

HOC高阶组件

高阶组件本质上就是一个没有副作用的纯函数,且该函数接受一个组件作为参数,并返回一个新的组件。高阶组件是通过将原组件 包裹(wrapping) 在容器组件(container component)里面的方式来 组合(composes) 使用原组件。

高阶组件和 容器组件(container component)的相似之处。容器组件是专注于在高层次和低层次关注点之间进行责任划分的策略的一部分。容器组件会处理诸如数据订阅和状态管理等事情,并传递props属性给展示组件。而展示组件则负责处理渲染UI等事情。高阶组件使用容器组件作为实现的一部分。你也可以认为高阶组件就是参数化的容器组件定义。

React中的diff算法

在React中,Component的render函数返回的是React Elements(tree)。由于React Element的不可变性,在下一个state或props更新的时候,render函数会返回一个新的React Elements(tree)。React需要对这两个tree做比对(diff),求出最小的更新操作过程,用来高效的更新DOM。

求解将一棵树转换成另一棵树的最小操作数的算法,最优算法的时间复杂度为o(n^3)。React中对React Elements的diff算法做了简化,将时间复杂度降到了o(n),极大得提升了性能。该算法的简化基于两点假设:

  1. 两个不同类型的元素将产生不同的树。
  2. 通过渲染器附带key属性,开发者可以示意哪些子元素可能是稳定的。

上述假设适用于大部分场景,但并不是所有的情况。所以React中的diff算法,求出的并不是最优解,而是较优解,它在特定的场景下会降低性能。

React分层diff

React中的diff算法是分层(depth)进行的,具体如下:

当对比两棵React Elements tree时,React会从两棵树的根元素(节点)开始,进行比对。针对同一层级节点的具体比对方式如下:

  1. 针对不同类型的元素
    React会销毁该元素及其所有的子元素,并重新构建新的元素及其所有的子元素;
    • DOM元素:直接销毁并重建;
    • Component元素:销毁前,该Component实例会收到componentWillUnmount();重建时,新Component实例会收到componentWillMount() 和 componentDidMount(),这会导致该Component的state丢失。
  2. 针对相同类型的元素
    • DOM元素:例如div、h1等,React会比较两者的属性,仅更新变化的属性,并递归其子元素;
    • Component元素:会保留该Component的实例,并在该实例上依次调用componentWillReceiveProps() 和 componentWillUpdate() 方法,该Component的state会保留。在组件元素的render方法被调用的时候,diff算法会继续以该Component的根元素递归处理;
  3. 在递归子节点(React Element children)
    • 没key:默认情况下,React会从前到后逐个按照上述方式对children的各个项进行比对,根据其元素类型做不同的改变(创建、销毁、更改)。此方法效率低下,并且在特定情况下可能会导致大量元素的销毁和重建,无法保持组件的实例及其状态;
    • 有key:为了提高效率并保证稳定性,可以给所有的children加“key”。该key值需要在同一兄弟元素之间应该是独一无二的,这样就能快速地通过该key值做对比,本质上是一种hash的思想。

React diff with keys

由于React diff算法对子节点(children)的对比方式的特殊性,在进行 类似将最后一个节点移动到列表首部的操作时,在一定程度上会影响 React 的渲染性能,尤其是在节点数量过大或更新操作过于频繁时。

其他知识点

  • 自React v15.5后,需要使用prop-types来对props做类型检查;
  • 与第三方库协同时,如涉及到DOM操作,可以使用ref;
  • 跨父子组件传递props,可以使用Context,借助Provider和Consumer实现;
  • 如果不想在DOM中增加额外的节点,可以使用Fragments;
  • 如果需要将子节点渲染到父组件以外的 DOM 节点,可以使用Portals。此时Virtual DOM(React Elements)的层级会仍然保留(主要体现在事件冒泡上);
  • React有自身的SyntheticEvent(合成事件),其接口和行为与原生事件的保持一致,是跨浏览器的一层封装。

参考文章:

Redux

Redux 是 JavaScript 状态容器,提供可预测化的状态管理。Redux 由 Flux 演变而来,但受 Elm 的启发,避开了 Flux 的复杂性。

Flux
React使用Flux的单向数据流

React与flux区别在于:

  • Redux 并没有 dispatcher 的概念;
  • Redux 假设数据是不可变的;

React是严格的单向数据流。

Redux的三大原则

  1. 单一数据源;
  2. State 是只读的;
  3. 使用纯函数来执行修改。

Redux流程图

基础

  • Action 本质上是一个对象,由Action Creator产生,可以通过store.dispatch(action)传递到store;
  • Reducers 本质上是一个纯函数,接收旧的 state 和 action,返回新的 state;
  • Store 本质上是一个对象,用来保存应用的状态;

Reducers指定了应用状态的变化如何响应 actions 并发送到 store 的,actions 只是描述了有事情发生了这一事实,并没有描述应用如何更新 state。

Redux 应用中数据的生命周期遵循下面 4 个步骤:

  1. 调用 store.dispatch(action);
  2. Redux store 调用传入的 reducer 函数;
  3. 根 reducer 应该把多个子 reducer 输出合并成一个单一的 state 树;
  4. Redux store 保存了根 reducer 返回的完整 state 树;

高级

  • 异步Action

处理异步Action,需要dispatch 至少三种 action:

  1. 一种通知 reducer 异步Action开始的 action;
  2. 一种通知 reducer 异步Action成功的 action;
  3. 一种通知 reducer 异步Action失败的 action;

React处理异步Action,通常会使用middleware。常用的有redux-thunk、redux-saga、redux-promise、redux-observable、
redux-pack等

  • 异步数据流

默认情况下,createStore() 所创建的 Redux store 没有使用 middleware,所以只支持 同步数据流。

当使用了middleware时,middleware 链中的最后一个 middleware 开始 dispatch action,这个 action 必须是一个普通对象,middleware 链之前可以根据 middleware 对 dispatch 的封装,来 dispatch 一些除了 action 以外的其他内容,例如:函数或者 Promise。从而实现了异步数据流。

  • Middleware

Redux middleware可以用来链式组合,它提供的是位于 action 被发起之后,到达 reducer 之前的扩展点。 你可以利用 Redux middleware 来进行日志记录、创建崩溃报告、调用异步接口或者路由等等。

React vs Vue 简单对比

React和Vue都是现如今非常流行的前端MVVM框架。其中Vue的渐进式,更适合于中小型项目,React在大型项目有比Vue更严格的规则,在大型项目中表现更为出色。

JSX vs Template

在视图层面,React中通常会使用JSX,而Vue可以使用模板(Template)或render函数,但其本质上都是用来生成Virtual DOM的函数。JSX会被编译为React.createElement(),而Vue中的template会被编译为render函数。

JSX很强大,中间可以在{}内部使用任意JS代码。Vue中的template语法上近似原生HTML,上手难度极低,很多特性的写法上非常灵活,同时还提供了强大的render函数,很多复杂的组件,可以直接用render函数来实现。

单向数据流

React中是严格的单向数据流,组件要想将自身的state反馈回去,需要借助于“事件”(受控组件)。而Vue中也是单向数据流,虽然可以通过v-model实现双向绑定的,但其本质上只是onchange事件的语法糖。

组件间通信

  • 父子组件之间通信,React和Vue是相同的,都是采用“props down, event up”的形式;
  • 祖孙组件(跨层级的组件)之间通信,React需要借助context,Vue是使用message,本质上是一样的;
  • 兄弟组件之间通信,React和Vue都是通过共同祖先组件传递的同一个props实现。

生命周期

React的生命周期示意图
React lifecycle
Vue的生命周期示意图
Vue lifecycle

总结

React有强大的JSX,自定义的diff算法,提倡纯函数,支持服务器端渲染;再加上成熟的生态,丰富的组件库,以及各种周边(Redux、React-Router等),有着非常强的扩展性。结合Webpack等构建工具,可以使得用React开发的项目有着很强的可扩展性和可维护性,工程化得到了极大的保障。

本文只是React新手的学习笔记,是学习React和Redux概念方面的总结。我个人对React的了解仍比较浅,有待从实践中学习,从原理方面思考,并进一步从源码中学习。前方的路还很长,且行且成长。