redux入门

Ref: 阮一峰的网络日志

安装

安装稳定版:

npm install --save redux

附加包

多数情况下,你还需要使用 React 绑定库和开发者工具。

npm install –save react-redux
npm install –save-dev redux-devtools

设计思想

Redux 的设计思想很简单,就两句话。
(1)Web 应用是一个状态机,视图与状态是一一对应的。
(2)所有的状态,保存在一个对象里面。

基本概念和API

  • Store
    Store就是保存数据的地方,你可以把它看成一个容器。整个应用只能有一个 Store。
    Redux 提供createStore这个函数,用来生成 Store。
import { createStore } from 'redux';
const store = createStore(fn);

上面代码中,createStore函数接受另一个函数作为参数,返回新生成的 Store 对象。

  • State
    Store对象包含所有数据。如果想得到某个时点的数据,就要对 Store 生成快照。这种时点的数据集合,就叫做 State。

当前时刻的 State,可以通过store.getState()拿到。

import { createStore } from 'redux';
const store = createStore(fn);

const state = store.getState();

Redux 规定, 一个 State 对应一个 View。只要 State 相同,View 就相同。你知道 State,就知道 View 是什么样,反之亦然。

  • Action
    State 的变化,会导致 View 的变化。但是,用户接触不到 State,只能接触到 View。所以,State 的变化必须是 View 导致的。Action 就是 View 发出的通知,表示 State 应该要发生变化了。
    Action 是一个对象。其中的type属性是必须的,表示 Action 的名称。其他属性可以自由设置,社区有一个规范可以参考。
const action = {
  type: 'ADD_TODO',
  payload: 'Learn Redux'
};

上面代码中,Action 的名称是ADD_TODO,它携带的信息是字符串Learn Redux。

可以这样理解,Action 描述当前发生的事情。改变 State 的唯一办法,就是使用 Action。它会运送数据到 Store。

  • Action Creator
    View 要发送多少种消息,就会有多少种 Action。如果都手写,会很麻烦。可以定义一个函数来生成 Action,这个函数就叫 Action Creator。
const ADD_TODO = '添加 TODO';

function addTodo(text) {
  return {
    type: ADD_TODO,
    text
  }
}
const action = addTodo('Learn Redux');

上面代码中,addTodo函数就是一个 Action Creator。

  • store.dispatch()

store.dispatch()是 View 发出 Action 的唯一方法。

import { createStore } from 'redux';
const store = createStore(fn);

store.dispatch({
  type: 'ADD_TODO',
  payload: 'Learn Redux'
});

上面代码中,store.dispatch接受一个 Action 对象作为参数,将它发送出去。
结合 Action Creator,这段代码可以改写如下。
store.dispatch(addTodo('Learn Redux'));

  • reducer 是一个接收 state 和 action,并返回新的 state 的函数。

  • 三大原则
    Redux 可以用这三个基本原则来描述:

    1. 单一数据源
      整个应用的 state 被储存在一棵 object tree 中,并且这个 object tree 只存在于唯一一个 store 中。
    2. State 是只读的
      唯一改变 state 的方法就是触发 action,action 是一个用于描述已发生事件的普通对象。
    3. 使用纯函数来执行修改
      为了描述 action 如何改变 state tree ,你需要编写 reducers。

Note:
ES6的...,该运算符将一个数组,变为参数序列。

// ES6 的写法  
Math.max(...[14, 3, 77])  
//  等同于  
Math.max(14, 3, 77);  

另一个例子是通过push函数,将一个数组添加到另一个数组的尾部。

// ES5 的写法  
var arr1 = [0, 1, 2];  
var arr2 = [3, 4, 5];  
Array.prototype.push.apply(arr1, arr2);  
// ES6 的写法  
var arr1 = [0, 1, 2];  
var arr2 = [3, 4, 5];  
arr1.push(...arr2);  

上面代码的 ES5 写法中,push方法的参数不能是数组,所以只好通过apply方法变通使用push方法。有了扩展运算符,就可以直接将数组传入push方法。

一个很好的redux入门step by step教程:

https://segmentfault.com/a/1190000011474522

使用Redux工具调试

如果我们的代码出错了,应该如何调试呢?

Redux拥有很多第三方的调试工具,可用于分析代码和修复bug。最受欢迎的是time-travelling tool,即redux-devtools-extension。设置它只需要三个步骤。

首先,在Chrome中安装Redux Devtools扩展。
然后,在运行Redux应用程序的终端里使用Ctrl+C停止服务器。并用npm或yarn安装redux-devtools-extension包。
yarn add redux-devtools-extension
一旦安装完成,我们对store.js稍作修改:


Learning from React.js 小书

React-redux 就是把 Redux 这种架构模式和 React.js 结合起来的一个库,就是 Redux 架构在 React.js 中的体现。

函数dispatch

它专门负责数据的更改。它接受一个参数action,这个action是一个普通的JavaScript对象,里面必须包含一个type字段来声明action的种类。dispatch在switch中识别这个type字段,能够被识别出来的action种类才能执行对appState的修改。

function dispatch (action) {
  switch (action.type) {
    case 'UPDATE_TITLE_TEXT':
      appState.title.text = action.text
      break
    case 'UPDATE_TITLE_COLOR':
      appState.title.color = action.color
      break
    default:
      break
  }
}

三:纯函数(Pure Function)

纯函数有如下特点:

  1. 函数的返回结果只依赖于它的参数。
    即,函数的返回结果与其他外部值无关。
  2. 函数执行过程里面没有副作用。
    不会修改外部传进来的对象。

除了修改外部的变量,一个函数在执行过程中还有很多方式产生外部可观察的变化,比如说调用 DOM API 修改页面,或者你发送了 Ajax 请求,还有调用 window.reload 刷新浏览器,甚至是 console.log 往控制台打印数据也是副作用。

纯函数很严格,也就是说你几乎除了计算数据以外什么都不能干,计算的时候还不能依赖除了函数参数以外的数据。

为什么要煞费苦心地构建纯函数?因为纯函数非常“靠谱”,执行一个纯函数你不用担心它会干什么坏事,它不会产生不可预料的行为,也不会对外部产生影响。不管何时何地,你给它什么它就会乖乖地吐出什么。如果你的应用程序大多数函数都是由纯函数组成,那么你的程序测试、调试起来会非常方便。

四:共享结构的对象提高性能

这里目的是优化多余的更新操作:因为有时候只是某个值被修改了,但是却导致整个页面的整体渲染,这明显是非常耗费时间的。
这里提出的解决方案是,在每个渲染函数执行渲染操作之前先做个判断,判断传入的新数据和旧的数据是不是相同,相同的话就不渲染了。

  • 共享结构的对象

希望大家都知道这种 ES6 的语法:

const obj = { a: 1, b: 2}
const obj2 = { ...obj } // => { a: 1, b: 2 }

const obj2 = { ...obj } 其实就是新建一个对象 obj2,然后把 obj 所有的属性都复制到 obj2 里面,相当于对象的浅复制。上面的 obj 里面的内容和 obj2 是完全一样的,但是却是两个不同的对象。除了浅复制对象,还可以覆盖、拓展对象属性:

const obj = { a: 1, b: 2}
const obj2 = { ...obj, b: 3, c: 4} // => { a: 1, b: 3, c: 4 },覆盖了 b,新增了 

例如,新建一个 appState,新建 appState.title,新建 appState.title.text

let newAppState = { // 新建一个 newAppState
  ...appState, // 复制 appState 里面的内容
  title: { // 用一个新的对象覆盖原来的 title 属性
    ...appState.title, // 复制原来 title 对象里面的内容
    text: '《React.js 小书》' // 覆盖 text 属性
  }
}

如果我们用一个树状的结构来表示对象结构的话:

appState 和 newAppState 其实是两个不同的对象,因为对象浅复制的缘故,其实它们里面的属性 content 指向的是同一个对象;但是因为 title 被一个新的对象覆盖了,所以它们的 title 属性指向的对象是不同的。

修改数据的时候就把修改路径都复制一遍,但是保持其他内容不变,最后的所有对象具有某些不变共享的结构(例如上面2个对象共享 content 对象)。大多数情况下我们可以保持 50% 以上的内容具有共享结构,这种操作具有非常优良的特性,我们可以用它来优化上面的渲染性能。

  • 优化性能
    修改数据的时候,并不会直接修改原来的数据 state,而是产生上述的共享结构的对象:
function stateChanger (state, action) {
  switch (action.type) {
    case 'UPDATE_TITLE_TEXT':
      return { // 构建新的对象并且返回
        ...state,
        title: {
          ...state.title,
          text: action.text
        }
      }
    case 'UPDATE_TITLE_COLOR':
      return { // 构建新的对象并且返回
        ...state,
        title: {
          ...state.title,
          color: action.color
        }
      }
    default:
      return state // 没有修改,返回原来的对象
  }
}

五: reducer

createStore 接受一个叫 reducer 的函数作为参数,这个函数规定是一个纯函数,它接受两个参数,一个是 state,一个是 action。如果没有传入state或者state===null的话,那么它就会返回一个初始化的数据,如果有传入state的话,它就会根据action来”修改数据”(其实并不是修改,而是把修改路径的对象都赋值一遍,然后产生一个新的对象返回)。如果不能识别action的话,它就会原封不动地将state返回。

reducer 是不允许有副作用的。你不能在里面操作 DOM,也不能发 Ajax 请求,更不能直接修改 state,它要做的仅仅是 —— 初始化和计算新的 state。

六:redux总结

我们优化了 stateChanger 为 reducer,定义了 reducer 只能是纯函数,功能就是负责初始 state,和根据 state 和 action 计算具有共享结构的新的 state。

createStore 现在可以直接拿来用了,套路就是:

// 定一个 reducer
function reducer (state, action) {
  /* 初始化 state 和 switch case */
}

// 生成 store
const store = createStore(reducer)

// 监听数据变化重新渲染页面
store.subscribe(() => renderApp(store.getState()))

// 首次渲染页面
renderApp(store.getState()) 

//后面可以随意 dispatch 了,页面自动更新
store.dispatch(...)

例子练习: #16 实现 Users Reducer

动手实现 React-redux

  • connectmapStateToProps

策略:既然store有严格规定的的方式进行数据的修改(使用dispatch),这样就避免了之前直接用context进行数据共享的时候,数据紊乱的问题。那我们现在就把store作为context放在父组件中,子组件通过调用父组件的context得到store,就可以安全地获取数据了。
带来问题:用context解决变量共享问题会导致:
1. 大量重复的逻辑: 取出context,取出里面的store,然后用store里面的状态设置自己的状态。
2. 对context依赖性过强:这些子组件都要依赖父组件的context来获取数据,使得该子组件本身的复用性基本为零。

这里我们要引用高阶组件。我们称这个高阶组件为connect,因为它把Dumb组件(可预测性很强,对于参数以外零依赖,复用性强)和context连接起来了:

import React, { Component } from 'react'
import PropTypes from 'prop-types'

export connect = (WrappedComponent) => {
  class Connect extends Component {
    static contextTypes = {
      store: PropTypes.object
    }

    // TODO: 如何从 store 取数据?

    render () {
      return <WrappedComponent />
    }
  }

  return Connect
}

每个传进去的组件需要 store 里面的数据都不一样的,所以除了给高阶组件传入 Dumb 组件以外,还需要告诉高级组件我们需要什么数据,高阶组件才能正确地去取数据。

为了解决这个问题,我们可以给高阶组件传入类似下面这样的函数:

const mapStateToProps = (state) => {
  return {
    themeColor: state.themeColor,
    themeName: state.themeName,
    fullName: `${state.firstName} ${state.lastName}`
    ...
  }
}

这个函数会接受store.getState()的结果作为参数,然后返回一个对象,这个对象时根据state生成的。mapStateToProps相当于告知了Connect应该如何去store里面取数据,然后可以把这个函数的返回结果传给被包装的组件。

import React, { Component } from 'react'
import PropTypes from 'prop-types'

export const connect = (mapStateToProps) => (WrappedComponent) => {
  class Connect extends Component {
    static contextTypes = {
      store: PropTypes.object
    }

    render () {
      const { store } = this.context
      let stateProps = mapStateToProps(store.getState())
      // {...stateProps} 意思是把这个对象里面的属性全部通过 `props` 方式传递进去
      return <WrappedComponent {...stateProps} />
    }
  }

  return Connect
}

这个connect接受一个参数mapStateToProps,然后返回一个函数,这个返回的函数才是高阶组件。connect的用法是:

...
const mapStateToProps = (state) => {
  return {
    themeColor: state.themeColor
  }
}
Header = connect(mapStateToProps)(Header)
...

connect 还没有监听数据变化然后重新渲染,所以现在点击按钮只有按钮会变颜色。我们给 connect 的高阶组件增加监听数据变化重新渲染的逻辑,稍微重构一下 connect:

export const connect = (mapStateToProps) => (WrappedComponent) => {
  class Connect extends Component {
    static contextTypes = {
      store: PropTypes.object
    }

    constructor () {
      super()
      this.state = { allProps: {} }
    }

    componentWillMount () {
      const { store } = this.context
      this._updateProps()
      store.subscribe(() => this._updateProps())
    }

    _updateProps () {
      const { store } = this.context
      let stateProps = mapStateToProps(store.getState(), this.props) // 额外传入 props,让获取数据更加灵活方便
      this.setState({
        allProps: { // 整合普通的 props 和从 state 生成的 props
          ...stateProps,
          ...this.props
        }
      })
    }

    render () {
      return <WrappedComponent {...this.state.allProps} />
    }
  }

  return Connect
}

我们在Connect组件的constructor里面初始化了state.allProps,它是一个对象,用来保存需要传给被包装组件的所有参数。生命周期componentWillMount会调用_updateProps进行初始化,然后通过store.subscribe监听数据变化重新调用_updateProps

为了让connect返回新组件和被包装的组件使用参数保持一致,我们会把所有传给Connectprops原封不动地传给WrappedComponent。因此在_updateProps()里面,我们整合了普通的this.props和从state生成的props,一起放在this.state.allProps里面,再通过render方法,把所有参数都传给WrappedComponent。

  • mapDispatchToProps

有些时候,我们不仅需要store里面的数据,也需要store来dispatch.
我们可以给connect函数传入另一个参数来告诉它,我们的组件需要如何触发dispatch,这个参数称为mapDispatchToProps.

const mapDispatchToProps = (dispatch) => {
  return {
    onSwitchColor: (color) => {
      dispatch({ type: 'CHANGE_COLOR', themeColor: color })
    }
  }
}

mapStateToProps一样,它返回一个对象,这个对象内容会同样被connect当作是props参数传给被包装的组件。不同的是,这个函数不是接受state作为参数,而是dispatch, 这里用到dispatch来触发特定的action
修改connect函数内的_updateProps():

...
_updateProps () {
      const { store } = this.context
      let stateProps = mapStateToProps
        ? mapStateToProps(store.getState(), this.props)
        : {} // 防止 mapStateToProps 没有传入
      let dispatchProps = mapDispatchToProps
        ? mapDispatchToProps(store.dispatch, this.props)
        : {} // 防止 mapDispatchToProps 没有传入
      this.setState({
        allProps: {
          ...stateProps,
          ...dispatchProps,
          ...this.props
        }
      })
    }
...

在这个函数中,我们把store.dispatch作为参数传给mapDispatchToProps,它会返回一个对象dispatchProps。接着把stateProps,dispatchProps, this.props三者合并到this.state.allProps里面去,它们都在render的时候传给被包装的组件: return <WrappedComponent {...this.state.allProps} />

我们看ThemeSwitch的代码,

import React, { Component } from 'react'
import PropTypes from 'prop-types'
import {connect} from "./Connect";

class ThemeSwitch extends Component {
    //定义了两个props
    static propTypes = {
        themeColor: PropTypes.string,
        onSwitchColor : PropTypes.func
    }
    handleSwitchColor(color) {
        if (this.props.onSwitchColor){
        //如果onSwitchColor函数存在的话,则调用
            this.props.onSwitchColor(color)
        }
    }
    render () {
        return (
            <div>
                <button style={{ color: this.props.themeColor}}
                onClick={this.handleSwitchColor.bind(this,'red')}>Red</button>
                <button style={{ color: this.props.themeColor }}
                onClick={this.handleSwitchColor.bind(this,'blue')}>Blue</button>
            </div>
        )
    }
}
//以下是从store获取参数themeColor和onSwitchColor
const mapDispatchToProps = (dispatch) => {
    return {
        onSwitchColor: (color) => {
            dispatch({
                type:'CHANGE_COLOR',
                themeColor:color
            })
        }
    }
}
const mapStateToProps = (state) => {
    return {
        themeColor:state.themeColor
    }
}
ThemeSwitch = connect(mapStateToProps,mapDispatchToProps)(ThemeSwitch)

export default ThemeSwitch
  • Provider

为什么需要Provider
我们在定义父组件Index的时候,有一段这样的代码⬇️。它主要的功能其实是:创建context,把store存放到里面,好让子组件connect的时候能够取到store。

class Index extends Component {
  static childContextTypes = {
    store: PropTypes.object
  }

  getChildContext () {
    return { store }
  }

  render () {
    return (
      <div>
        <Header />
        <Content />
      </div>
    )
  }
}

现在我们额外构建一个组件来做这件事,然后让这个组件称为组件树的根节点,那么它的子组件就都可以获取到context了。
我们把这个组件叫做Provider,因为它提供了store

以下是Provider的定义:

export class Provider extends Component {
  static propTypes = {
    store: PropTypes.object,
    children: PropTypes.any
  }

  static childContextTypes = {
    store: PropTypes.object
  }

  getChildContext () {
    return {
      store: this.props.store
    }
  }

  render () {
    return (
      <div>{this.props.children}</div>
    )
  }
}

Provider就是一个容器组件,会把嵌套的内容原封不动地作为自己的子组件渲染出来,它还会把外界传给他的props.store放到context,这样子,子组件connect的时候可以获取到。

一般Provider这样用:


// 头部引入 Provider
import { Provider } from './react-redux'


// 删除 Index 里面所有关于 context 的代码
class Index extends Component {
  render () {
    return (
      <div>
        <Header />
        <Content />
      </div>
    )
  }
}

// 把 Provider 作为组件树的根节点
ReactDOM.render(
  <Provider store={store}>
    <Index />
  </Provider>,
  document.getElementById('root')
)
  • 真正地使用redux和react-redux

在工程目录下使用 npm 安装 Redux 和 React-redux 模块:

npm install redux react-redux --save

把 src/ 目录下 Header.js、ThemeSwitch.js、Content.js 的模块导入中的:

import { connect } from './react-redux'

改成:

import { connect } from 'react-redux'

也就是本来从本地(自己写的connect) 导入的 connect 改成从第三方 react-redux 模块中导入。

然后,删除自己写的 createStore,改成使用第三方模块 redux 的 createStore;Provider 本来从本地 引入,改成从第三方 react-redux 模块中引入。其余代码保持不变。

{connect} 高阶组件(函数) 和 Provider组件都由'react-redux'包提供。
{connect} <- 'react-redux'
Provider  <- 'react-redux'

#17 React-redux 实现用户列表的显示、增加、删除

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值