资讯专栏INFORMATION COLUMN

走近 Redux

fevin / 3074人阅读

摘要:的核心思想就是维护一个单向数据流,数据的流向永远是单向的,所以每个步骤便是可预测的,程序的健壮性得到了保证。另外,还有一点比较重要的是,因为没有了一个一直保存更新的状态对象,所以在中的也就没有意义了,通过可以完全实现一个顺畅的数据流。

1 Redux
Redux is a predictable state container for JavaScript apps

简单来说,Redux是一个管理应用状态的框架

2 解决的问题 2.1 前端开发中的普遍问题

前端开发中,本质的问题就是将 server -> client 的输入,变成 client -> user 输入;再将 user -> client 的输入,变成 client -> server 的输入。

在 client 中,前端的角色其实大概可以当做一个"转换器"。

举个简单的例子,后端传过来的是一个 json 格式的数据,这个 json 格式,其实是在计算机范畴内的,真正的终端用户并不知道什么是json,更不知道要如何修改json,保存自己的信息。所以,这个时候就需要像上面说的把 json 转换为页面上的内容和元素;另外,随着用户的一系列操作,数据需要随时更新保存到服务端。整个的这个过程,可能会很复杂,数据和数据之间会存在联动关系。

这个时候,就需要有一个东西,从更高的层面来管理所有的这些状态,于是有了 mvc,状态保存在model,数据展示在viewcontroller来串联用户的输入和数据的更新。但是这个时候就会有个问题,理想情况下,我们默认所有的状态更新都是由用户的操作(也可以理解为用户的输入)来触发的,但实际情况中,会触发状态更新的不仅仅是单纯的用户操作,还有可能是用户操作带来的后果,在举个例子:

页面上有个异步获取信息的按钮,用户可以点击这个按钮,那么用户点击这个按钮之后,会发生:

按钮状态变为 pending --> 获取成功,按钮状态变成 success
                   |
                   |--> 获取失败,按钮状态变成 error

这里改变success/error状态的并不是用户输入,而是服务端的返回,这个时候,就需要在 controller里面 handle 服务端的返回。这只是个简单的例子,如果类似的情况发生了很多之后,每次输入和输出将变得难以预测,难以预测的后果就是很容易出现 bug,程序的健壮性下降。

让每一步输入和输出可预测,可预测才能可测试,可测试才能保证健壮性。

2.2 React 和 Flux

于是,这个时候出现了ReactFlux

Flux的核心思想就是维护一个单向数据流,数据的流向永远是单向的,所以每个步骤便是可预测的,程序的健壮性得到了保证。

Reactjsx 可以将前端的 UI 部分变成了一层层套用的方法,再举个例子,之前写 html 是这样的

foo

如果状态改变之后,大部分情况下我们是将某个片段的 html 用改变的状态重新拼一遍,然后替换到原有的 dom 结构里。

但是,用了 jsx 之后,你的代码将变成这样:

div(span("foo"))

变成了一个函数,这个函数的输出就是上面的那段 html,所以整个 UI 变成了一个可输入输出的结构,有了输入和输出,就是一个完整的可预测的结构了,可预测,也就是代表可测试了。

2.3 使用 Redux

在使用Flux的过程里,当应用的结构变得复杂之后,会显得力不从心,虽然数据流还是单向,但是Flux的整体流程有两个比较关键的点:

store 更新完数据之后,需要emit

component 中需要 handle emit

当数据结构和输入输出变得复杂的时候,往往会定义很多个 store,但是往往 store 之间还是会有依赖和关联。

这个时候,handle 的过程会变得很臃肿,难以理解。

然后,Redux就出场了。

Flux的思路可以理解为多个store组成了一个完整的 App;Redux的思路则是一个完整的store对应一个完整的 App。

Redux相比Flux,多抽象出了一个reducer的概念。这个reducer只负责状态的更新,并且会返回一个新的状态对象,整个 App 从结构上看起来,没有一个一直保存/更新的状态(使用Flux每个store都是一直保存住的,然后在此基础上进行更新),Redux中的数据更像是一个流程。

另外,还有一点比较重要的是,因为没有了一个一直保存/更新的状态对象,所以在 component 中的 handle 也就没有意义了,通过react-redux可以完全实现一个顺畅的数据流。

这里举个简单的例子,如果我们更新一个订单,订单里有这么几项:

地址

运费

商品数量

总价

其中地址影响运费,运费影响总价;另外,商品数量也会影响总价

使用Flux的话,我们通常会分解成这样几个store:

address

items

deal

其中 addressitems的更新会触发deal.amount的更新,完整的交易信息会同步到deal中。

component里,我们会handel所有这些storeemit,然后再进行setState以更新 UI 部分。

使用Redux的话,我们会分解成这样几个reducer:

address

items

deal

其中address只负责address的更新,item只负责items的更新,deal会响应addressitem中跟交易相关的更新,实现改变价格和订单地址的操作。

但是并不需要在component中再hanle每个部分更新之后的emit。数据更新了,页面就会自己变化。

接下来,我们看看Redux是如何实现的。

3 实现原理

查看Redux的github,会发现Redux的代码异常的精简,仅仅包含了这几个部分:

utils/

applyMiddlewares.js

bindActionCreators.js

combineReducers.js

compose.js

createStore.js

index

其中的utils/index.js我们并不需要关心,只要看接下来的几部分就可以。

另外,因为我们的大部分场景还是搭配React来使用Redux,所以这里我们顺便搭配 react-redux来看下

react-redux/src

react-redux中,我们关心的更少,只有:

Provider.js

connect.js

这两部分而已。

3.1 一个真实世界中的例子

拿一个真正的实例来看,我们要做一个简单的订单,目录结构是这样的:

|- dealReducer.js
|- dealActions.js
|- dealStore.js
|- dealApp.js
|- main.js
3.1.1 main.js

先看代码:

import React from "react"
import ReactDom from "react-dom"
import { Provider } from "react-redux"
import configureStore from "./dealStore"
import DealApp from "./dealApp"

let store = configureStore()

ReactDom.render(
  (
    
      
    
  ), document.getElementById("app"))

这个部分比较简单,首先是调用了dealStore中的方法,生成了一个store,然后调用了react-redux中的Provider把这个store绑定到了Provider上。

我们先看 Provider 的代码:

3.1.2 react-redux.provider

完整代码看这里

我们只看下核心的部分:

export default class Provider extends Component {
  getChildContext() {
    return { store: this.store }
  }

  constructor(props, context) {
    super(props, context)
    this.store = props.store
  }

  render() {
    return Children.only(this.props.children)
  }
}

其实最核心就是getChildContext方法,这个方法在每次propsstate被调用时会被触发,这里更新了store

3.1.3 dealApp.js

还是先看代码:

import React, { Component, PropTypes } from "react"
import { bindActionCreators } from "redux"
import { connect } from "react-redux"
import * as dealActions from "deal/actions/dealActions"
import * as addressActions from "deal/actions/addressActions"

class DealApp extends Component {
    // some code
}

function mapStateToProps(state) {
  return {
    "deal": state.dealReducer,
    "address": state.addressReducer,
  }
}

function mapDispatchToProps(dispatch) {
  return {
    "dealActions": bindActionCreators(dealActions, dispatch),
    "addressActions": bindActionCreators(addressActions, dispatch),
  }
}

export default connect(mapStateToProps, mapDispatchToProps)(DealApp)

从代码可以看到,比一般的 react component 多了对connect的调用,以及mapStateToPropsmapDispatchToProps两个方法。

所以,接下来看下这个connect是什么

3.1.3 react-redux.connect

完整代码见

来看下核心部分的代码:

    // some code
    componentDidMount() {
        this.trySubscribe()
    },
    
    trySubscribe() {
        if (shouldSubscribe && !this.unsubscribe) {
            this.unsubscribe = this.store.subscribe(this.handleChange.bind(this))
          this.handleChange()
        }
      },
    
    handleChange() {
        if (!this.unsubscribe) {
            return
        }

        const storeState = this.store.getState()
        const prevStoreState = this.state.storeState
        if (pure && prevStoreState === storeState) {
            return
        }

        if (pure && !this.doStatePropsDependOnOwnProps) {
            const haveStatePropsChanged = tryCatch(this.updateStatePropsIfNeeded, this)
          if (!haveStatePropsChanged) {
            return
          }
          if (haveStatePropsChanged === errorObject) {
            this.statePropsPrecalculationError = errorObject.value
          }
          this.haveStatePropsBeenPrecalculated = true
        }

        this.hasStoreStateChanged = true
        this.setState({ storeState })
      }    

可以看到,这几个方法用到了store中的getStatesubscribe这几个方法。并且在handleChange中,实现了在Flux中需要人肉实现的setState方法。

3.1.4 dealStore.js

既然在上面的connect中,用到了store,那么就来看看dealStore的内容:

import { createStore, applyMiddleware, compose } from "redux"
import thunk from "redux-thunk"
import dealReducers from "deal/reducers/dealReducer"

let creator = compose(
    applyMiddleware(thunk),
    applyMiddleware(address),
  )(createStore)

export default function configureStore(initState) {
  const store = creator(dealReducers, initState)
  return store
}

这个文件里用到了redux中的createStore , composeapplyMiddleware方法。
通过调用可以看到,先是通过applyMiddleware方法调用了一些middleware,然后再用compose将对middleware的调用串联起来,返回一个方法,先简单列为f(createStore),然后这个调用再次返回了一个方法,这里被定义为creator。通过调用creator方法,最终生成了 store

下面逐个看一下createStore,compose,applyMiddleware这几个方法。

3.1.5 applyMiddleware

直接看源码:

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
    }
  }
}

这里直接返回了一个接收createStore作为参数的方法,这个方法中会遍历传入的middleware,并使用compose 调用store.dispatch,接下来看一下compose方法的具体实现。

3.1.6 compose

还是直接贴源码:

export default function compose(...funcs) {
  if (funcs.length === 0) {
    return arg => arg
  }

  if (funcs.length === 1) {
    return funcs[0]
  }

  const last = funcs[funcs.length - 1]
  const rest = funcs.slice(0, -1)
  return (...args) => rest.reduceRight((composed, f) => f(composed), last(...args))
}

可以看到 compose的源码十分精简,整个compose的作用就是传入一串funcs,然后返回一个方法,先暂定这个方法名为cc将传入的funcs按照从右到左的顺序,逐个执行c传入的参数。

为什么要按照从右到左的顺序执行,我们先按下不表,接下来看 createStore 的源码。

3.1.7 createStore

createStore的源码比较长,这里就不贴了,详情可以见这里。

我们这里只看下这个方法的输入和输出既可:

export default function createStore(reducer, preloadedState, enhancer) {

    // code
    
    return {
           dispatch,
           subscribe,
           getState,
           replaceReducer,
          [$$observable]: observable
  }
}

输入有三个,reducerpreloadState我们都属性,但是这个enhancer是什么呢?

再来看下相关代码:

if (typeof enhancer !== "undefined") {
    if (typeof enhancer !== "function") {
      throw new Error("Expected the enhancer to be a function.")
    }

    return enhancer(createStore)(reducer, preloadedState)
}

enhancer可以当做是预先设定的,对createStore返回对象执行的方法,比如可以给返回的对象添加一些新的属性或者方法之类的操作,就可以放到enhancer中做。

看到这里,我们再来看下compose中为什么调用reducerRight,将方法从右至左执行。

首先,是applyMiddleware方法获取到传入的createStore,返回了:

{
    ...store,
    dispatch
}

但是这里的dispatch已经不是creatStore中返回的store.dispatch了。这个dispatch是通过调用composestore.dispatch传入middlewares中执行的结果。

再回到主线上来,applyMiddleware返回了一个增强store,如果有多个applyMiddleware的调用,如下所示:

compose(
    applyMiddleware(A),
    applyMiddleware(B),
    applyMiddleware(C)
)

我们的期望的执行顺序当然是A,B,C这样,所以转换成方法的话,应该是这样

C(B(A()))

使用reducerRight的话,最先被调用的方法(也就是上面的C)就会是执行链的最外层的方法,所以要按照从右到左的顺序执行。

至此,Redux的介绍就先到这里,之后会再写一些关于Redux周边组件的使用。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/86724.html

相关文章

  • 走近正则:仿Nodejs的Url模块到前端

    摘要:正则学起来说真的,不去正儿八经的学正则,对付一般的工作是没啥问题的,虽然我们可能会经常用到,但毕竟度娘能提供大多时候你想要的可当我看一些框架的源码,总会被里面一长串一长串的正则给吓到之前一篇博客里有关于简单的爬虫实践,其实离达到我预期的效果 正则学起来 说真的,不去正儿八经的学正则,对付一般的工作是没啥问题的,虽然我们可能会经常用到replace,但毕竟度娘能提供大多时候你想要的;可当...

    HitenDev 评论0 收藏0
  • C++之vector:灵活的数组

    摘要:走近可以肤浅地理解成为灵活的数组,我们在定义数组的时候,是要确定数组的大小的。在内部,向量使用一个动态分配的数组来存储它们的元素。当插入新元素时,为了增加数组的大小,可能需要重新分配数组,这意味着分配一个新数组并将所有元素移动到该数组中。 ...

    mj 评论0 收藏0
  • 走近 Python (类比 JS)

    摘要:作为一名前端开发者,也了解中的很多特性借鉴自比如默认参数解构赋值等,同时本文会对的一些用法与进行类比。函数接收一个函数和一个,这个函数的作用是对每个元素进行判断,返回或,根据判断结果自动过滤掉不符合条件的元素,返回由符合条件元素组成的新。 showImg(https://segmentfault.com/img/remote/1460000011857550); 本文首发在 个人博客 ...

    shadajin 评论0 收藏0
  • 《网络黑白》一书所抄袭的文章列表

    摘要:网络黑白一书所抄袭的文章列表这本书实在是垃圾,一是因为它的互联网上的文章拼凑而成的,二是因为拼凑水平太差,连表述都一模一样,还抄得前言不搭后语,三是因为内容全都是大量的科普,不涉及技术也没有干货。 《网络黑白》一书所抄袭的文章列表 这本书实在是垃圾,一是因为它的互联网上的文章拼凑而成的,二是因为拼凑水平太差,连表述都一模一样,还抄得前言不搭后语,三是因为内容全都是大量的科普,不涉及技术...

    zlyBear 评论0 收藏0
  • 有趣的JavaScript原生数组函数

    摘要:对的描述如下将会给数组里的每一个元素执行一遍回调函数,直到回调函数返回。的运行原理和类似,但回调函数是返回而不是。回调函数只会对已经指定值的数组项调用。 在JavaScript中,创建数组可以使用Array构造函数,或者使用数组直接量[],后者是首选方法。Array对象继承自Object.prototype,对数组执行typeof操作符返回object而不是array。然而,[] in...

    mo0n1andin 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<