继续浏览精彩内容
慕课网APP
程序员的梦工厂
打开
继续
感谢您的支持,我会继续努力的
赞赏金额会直接到老师账户
将二维码发送给自己后长按识别
微信支付
支付宝支付

Redux实例学习 - Redux套用七步骤

Eddy张
关注TA
已关注
手记 5
粉丝 31
获赞 106

图片描述

今天的主题是Redux,一开始我们先看它是如何运作的,Redux并不是只能在React应用中使用,而是可以在一般的应用中使用。第一个例子是一个简单的JavaScript应用。

这个程序最后的呈现结果,就像下面的动态图片这样,重点是在于下面有个Redux DevTools,它有时光旅行调试的功能,可以倒带重播你作过的任何数据上的变动:

图片描述

这个简单的应用是让你学习Redux整个运作的过程用的,它只是个演示用的例子,在实际的应用中虽然会比较复杂,但基本的运作流程都是一样的。本章的下面附了一些详细的说明,建议你一定要看。Redux里面有很多基本的概念与专有名词,不学是很难看得到在说什么东西,代码通常写得很简洁,但概念都是要有些基础才会通的。

代码说明

首先我们要使用的是用于写ES6用的脚手架webpack-es6-startkit,因为这个例子中并没有要用到React,所以也不用安装React。

接着我们要多安装redux套件进来,在项目目录里用命令列工具(终端机)输入以下的指令:

npm install --save redux

另外你也需要安装Chrome浏览器的插件 - Redux DevTools,这可以让你使用Redux中的时光旅行调试功能。

我在index.html中加了一个文本框itemtext、按钮itemadd,以及一个准备要显示项目列表的div区域itemlist,代码如下:

index.html

<div>
  <p>
    <input type="text" id="itemtext" />
    <button id="itemadd">Add</button>
  </p>
</div>

<div id="itemlist">
</div>

代码档案只有一个,就是index.js,它是在src/目录下的,代码如下:

src/index.js

import { createStore } from 'redux'

// @Reducer
//
// Action Payload = action.text
// 使用纯函数的数组unshift,不能有副作用
// state(状态)一开始的值是空数组`state=[]`
function addItem(state = [], action) {
  switch (action.type) {
    case 'ADD_ITEM':
      return [action.text, ...state]
    default:
      return state
  }
}

// @Store
//
// store = createStore(reducer)
// 使用redux dev tools
// 如果要正常使用是使用 const store = createStore(addItem)
const store = createStore(addItem,
              window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__())

// @Render
//
// render(渲染)是从目前store中取出state数据,然后输出呈现在网页上
function render() {
  const items = store.getState().map(item => (
    (item) ? `<li>${item}</li>` : ''
  ))
  document.getElementById('itemlist').innerHTML = `<ul>${items.join('')}</ul>`
}

// 第一次要调用一次render,让网页呈现数据
render()

// 订阅render到store,这会让store中如果有新的state(状态)时,会重新调用一次render()
store.subscribe(render)

// 监听事件到 "itemadd" 按钮,
// 点按按钮会触发 store.dispatch(action),发送一个动作,
// 例如 store.dispatch({ type: 'ADD_ITEM', textValue })
document.getElementById('itemadd')
  .addEventListener('click', () => {
    const itemText = document.getElementById('itemtext')

    // 调用store dispatch方法
    store.dispatch({ type: 'ADD_ITEM', text: itemText.value })

    // 清空文本输入框中的字
    itemText.value = ''

  })

这个代码中我有排顺序与加上中文注释,因为你要启用Redux中的作用,是有顺序的。我们一步步看下来:

第一步,是要从redux中汇入createStore方法,这很简单如下面的代码:

import { createStore } from 'redux'

第二步,是要创建一个reducer(归纳函数),reducer请求一定要是纯函数。那么到底什么是reducer的作用,就是传入之前的state(状态)与一个action(动作)对象,然后要返回一个新的state(状态)。

对我们这个简单的应用来说,它只会有一种这种行为,就是在文本框输入一些文字,按下按钮后,把这串文字值加到state(状态)中。

所以它的state(状态)是个数组,每次一发送动作时,就加到这个数组的最前面(索引值为0)一个成员,动作就是像下面这样的一个纯对象描述:

{
  type: 'ADD_ITEM',
  text
}

注: 上面的{ text }{ text: text}的简写语法,这是在ES6之后可以用的对象属性初始化简写语法。

reducer里面通常会以动作的类型(action.type),用switch语句来区分要运行哪一段的代码,因为动作有可能会有很多不同的,像删除项目、刷新项目等等。代码如下:

// @Reducer
//
// Action Payload = action.text
// 使用纯函数的数组unshift,不能有副作用
// state(状态)一开始的值是空数组`state=[]`
function addItem(state = [], action) {
  switch (action.type) {
    case 'ADD_ITEM':
      return [action.text, ...state]
    default:
      return state
  }
}

上面的[action.text, ...state],它就是纯函数写法的数组unshift方法。

这里要注意的是,state需要给个初始值,用的是ES6中的传参默认值的写法。store实际上在创建时,会进行state的初始化。

第三步,是由写好的reducer,创建store,其实这没什么好说的,就用汇入的createStore方法把reducer传入就行了。正常情况下是用const store = createStore(addItem),因为你要使用浏览器中的Redux DevTools,所以要改写成下面这样的代码:

// @Store
//
// store = createStore(reducer)
// 使用redux dev tools
// 如果要正常使用是使用 const store = createStore(addItem)
const store = createStore(addItem,
              window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__())

第四步,是写一个render(渲染函数),这个函数是在如果状态上有新的变化时,要作输出呈现的动作。说穿了,这大概是仿照React应用的机制的作法,不过它这设计实际上与React差了十万八千里,这个渲染函数里最重要的是用store.getState()方法取出目前store里面的状态值,因为我们现在只有记一个state值,所以直接取出来就是刚刚在reducer里记录状态值的那个数组。剩下的就是一些格式的调整与输出工作而已。代码如下:

// @Render
//
// render(渲染)是从目前store中取出state数据,然后输出呈现在网页上
function render() {
  const items = store.getState().map(item => (
    (item) ? `<li>${item}</li>` : ''
  ))
  document.getElementById('itemlist').innerHTML = `<ul>${items.join('')}</ul>`
}

第五步,第一次调用一下render,让目前的数据呈现在网页上。因为我们一开始在state里并没有数据(空数组),但也有可能原本是有一些数据的,这只是一个初始化数据的动作而已,也很简单,代码如下:

// 第一次要调用一次render,让网页呈现数据
render()

第六步,订阅render函数到store中,用的是store.subscribe方法,这订阅的动作会让store中如果有新的state(状态)时,就会重新调用一次render()。这也是一个很像是从React中抄来的设计吧?"当React中的state值改变(用setState),就会触发重新渲染",不过在React中,setState你要自己作,没有自动的机制。实际上这是从一个设计模式学来的作法,这种设计模式称为pub-sub(发布-订阅)系统,在Flux架构中就有这个设计,Redux中也有,不过它更简化了整个流程。代码也只有一行:

// 订阅render到store,这会让store中如果有新的state(状态)时,会重新调用一次render()
store.subscribe(render)

第七步,触发事件的时候要调用store.dispatch(action)。在我们的这个简单的例子中,唯一会触发事件就是按下那个加入文字的按钮,按下后除了要抓取文本框的文字外,另外就是要调用store要进行哪一个action,这个动作用的是store.dispatch方法,把action值传入,action的格式上面有看到过了。代码如下:

// 监听事件到 "itemadd" 按钮,
// 点按按钮会触发 store.dispatch(action),发送一个动作,
// 例如 store.dispatch({ type: 'ADD_ITEM', textValue })
document.getElementById('itemadd')
  .addEventListener('click', () => {
    const itemText = document.getElementById('itemtext')

    // 调用store dispatch方法
    store.dispatch({ type: 'ADD_ITEM', text: itemText.value })

    // 清空文本输入框中的字
    itemText.value = ''
})

以上就是这七个步骤,在这个简单的小程序,你要套用Redux这个规模化的架构,自然是有些杀鸡用牛刀的感觉,但我们的目的是要学习它是怎么运作的,你可以看到整个运作的核心就是store,数据(state)在里面,要与里面的数据(state)更动,也是得用store附带的方法才行。实际上到React中也是类似的运作方式,不过因为又加了一些额外的辅助套件,会比目前看到的还会复杂些,基本的运作逻辑都差不多。

store中的方法

Redux中的store是一个保存整个应用state对象树的对象,其中包含了几个方法,它的原型如下:

type Store = {
  dispatch: Dispatch
  getState: () => State
  subscribe: (listener: () => void) => () => void
  replaceReducer: (reducer: Reducer) => void
}

各方法的解说,其中最重要的是前面两个,subscribe之后在React中不需要使用:

  • dispatch 用于发送action(动作)使用的的方法
  • getState 取得目前state的方法
  • subscribe 注册一个回调函数,当state有更动时会调用它
  • replaceReducer 高级API,用于动态加载其它的reducer,一般情况不会用到

以下分别解说这三个会用到的方法,其中的S代表状态(State),A代表动作(Action)这两种自订的类型。

getState方法

getState(): S

回传目前state树的数据,相当于reducer最后回传出来的值。

dispatch方法

dispatch: (action: A) => A

dispatch是唯一可以触发更动state的方法。

dispatch一发送动作,store中的reducer将会同步传入目前的状态(getState()),以及给定的action两者,开始计算新的状态并回传。回传后,更动的监听目标将会被通知(用subscribe注册的回调),再次调用getState()可以得到新的状态值。

dispatch方法的传入类型是一个action(动作)对象,回传的类型也是一个action(动作),看起来好像有些多馀,在真实开发的情况中这中间有经过Action Creator(动作创建器)的设计,Action Creator(动作创建器)可以针对传入的action(动作),进行预先的处理,或是可以再透过中介软体(middleware)处理有副作用的动作,在处理数据后,确保action是纯对象再进入reducer作状态的更动。

所以dispatch方法中的传参通常是一个Action Creator的调用,这种样式它有个名称,叫作"函数合成",在中介软体中也有用类似的语法样式,JS语言中原本就可以这样作,这是一种在合并不同抽象逻辑很有用的工具,例如下面的例子:

function a(x) { return x*x }
function b(x) { return x*2 }
function c(x) { return x+1 }

a(b(c(10)))

实际来看dispatch的用法,以下的例子来自Redux官网,我加上了注解说明:

import { createStore } from 'redux'

// 由todos这个reducer创建store
// 第二个传参是初始的状态,是可选的
let store = createStore(todos, [ 'Use Redux' ])

// Action Creator(动作创建器)
function addTodo(text) {
  return {
    type: 'ADD_TODO',
    text
  }
}

// 用store.dispatch(action)发送动作
// 传参先用Action Creator(动作创建器)来创建出action的对象格式
store.dispatch(addTodo('Read the docs'))
store.dispatch(addTodo('Read about the middleware'))

dispatch方法在使用有一些限制,首要注意的不能在reducer中调用dispatch,这会产生错误,因为dispatch的整个过程从触发开始,到最后更动完状态,reducer算是dispatch动作的中途过程。订阅通常会在reducer回传新的状态后被调用,dispatch调用的位置可能通常会在订阅的监听者(subscription listeners)代码中。

reducer
Reducer<S, A> = (state: S, action: A) => S

上面是reducer的原型,初学Redux第一个面临的难题是reducer,reducer要求的是纯函数而且无副作用,大部份的初学者对于FP(函数式编程)的风格并不太熟悉。

要写出合适的reducer是需要经过实作练习与思考的,要先考量的是状态模型(State Shape),也就是具体的状态该是什么样的数据结构,

状态模型(State Shape)

在简单的应用中,例如一个Todo(待办事项)的应用,它在应用中的状态只会有一个记录每笔事项的数组,像下面这样:

const todos = [
    {id: 1, text: 'buy car'},
    {id: 2, text: 'learn redux' },
    ...
]

但在一个博客应用中,状态的模型就会复杂得多,像是下面这样的数组,其中的对象会有嵌套的深层数据结构:

const blogPosts = [
  {
    "id": "123",
    "author": {
      "id": "1",
      "name": "Paul"
    },
    "title": "My awesome blog post",
    "comments": [
      {
        "id": "324",
        "commenter": {
          "id": "2",
          "name": "Nicole"
        }
      }
    ]
  },
]

这样的结构建议要使用像normalizr库进行正规化,转变为下面的结构:

{
  result: "123",
  entities: {
    "articles": {
      "123": {
        id: "123",
        author: "1",
        title: "My awesome blog post",
        comments: [ "324" ]
      }
    },
    "users": {
      "1": { "id": "1", "name": "Paul" },
      "2": { "id": "2", "name": "Nicole" }
    },
    "comments": {
      "324": { id: "324", "commenter": "2" }
    }
  }
}

这在Redux中会更容易对数据进行处理,这部份属于高级的主题,有很多解决的方式,但这里先提出来说明一下。

状态的更动

reducer既然是FP的作法,在对状态的更动编程,也会使用FP的编写方式来撰写,最基本的几个例子,在这里先提出来。

首先有一个大的基本原则,就是用拷贝传入参数值的方式处理,这是一个通用的方式,不论是对象或是数组,能先掌握这基本的原则就不会乱了套。

第一个演示的例子,是要在reducer传入一个新的action,然后附加到原本的状态数组中,会这样写:

function addItem(state = [], action) {
  return [action.payload, ...state]
}

这句[action.payload, ...state]用的是ES6中的展开运算符的语法样式,写起来很简洁,它相当于下面的写法:

function addItem(state = [], action) {
  // 拷贝出一个新的数组
  // newState = [...state]语法也可以
  // newState = state.concat()也可以
  const newState = state.slice()

  // 附加新成员在新的数组前面,
  // 注意这是一个有副作用的数组方法
  newState.unshift(action.payload)

  // 回传处理过的新数组
  return newState
}

当然你也可以用for语句来写这个处理的程序,不过会愈写代码愈长,FP的风格会追求简洁,尽可能利用无副作的JS内建方法,这部份是需要经过练习与学习的。

第二个演示的例子是要从数组中删除其中一个索引值为action.index的成员,会这样写:

function removeItem(state = [], action) {
  return state.filter((item, index) => index !== action.index)
}

filter这个JS中内建的数组方法,可能对初学者来说没那么直观,filter会依照回调传参布尔结果进行过滤,产生一个全新的数组。

如果你不用这语法会怎么写?要用slice这个用来拆分一个数组的为子数组的方法,像下面这样,你会发现代码又开始变长了:

function removeItem(state, action) {
    return [
        ...state.slice(0, action.index),
        ...state.slice(action.index + 1)
    ]
}

当然你也可以用for语句来写,只要能保持上面说的原则,不要去更动到传入的state数组就可以了。

其它的还有在数组其中的一个索引值中进行插入,以及更动其中一个索引值的成员值(通常是对象),这部份就留作练习参考,我把两个语法写出来:

// 插入数组的其中一个索引值
function insertItem(state, action) {
    return [
        ...state.slice(0, action.index),
        action.item,
        ...state.slice(action.index)
    ]
}
function updateObjectInArray(state, action) {
    return state.map( (item, index) => {
        if(index !== action.index) {
            // 这不是要更动的数组成员,直接回传
            return item;
        }

        // 这是要更动的数组成员,作合成
        return {
            ...item,
            ...action.item
        }
    })
}

至于如果state是一个对象值时,用的也是拷贝出一个新的对象的语法,这通常是使用Object.assign这个JS的内建方法来作,例如以下的例子:

const initialState = {
   fetching: false,
   list: []
}

function addUser(state = initialState, action) {
    return Object.assign({}, state, { fetching: true })
}

如果是复杂的状态对象,你要更动里面的数据,改采用Immutable.js来作会比较便利。如果最上层的状态并非JS的纯对象,还另外改用redux-immutable

注意: 上述的数组与对象的拷贝都是浅拷贝的语法,深拷贝需要用自订撰写或使用额外的函数库来作。

打开App,阅读手记
4人推荐
发表评论
随时随地看视频慕课网APP

热门评论

读了您的5篇关于redux的文章,获益匪浅,深表感谢!

有空可否出一篇关于react-redux的文章?

查看全部评论