Redux实例学习

来源:互联网 发布:m1a2和豹2知乎 编辑:程序博客网 时间:2024/05/19 05:29
http://www.imooc.com/article/16065

图片描述

今天的主题是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。

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

本文原创发布于慕课网 ,转载请注明出处,谢谢合作!

0 0