博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Redux实例学习 - Redux套用七步骤
阅读量:4085 次
发布时间:2019-05-25

本文共 10041 字,大约阅读时间需要 33 分钟。

http://www.imooc.com/article/16065

图片描述

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

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

图片描述

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

代码说明

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

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

npm install --save redux

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

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

index.html

代码档案只有一个,就是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) ? `
  • ${
    item}
  • ` : '' )) document.getElementById('itemlist').innerHTML = `
      ${
      items.join('')}
    `}// 第一次要调用一次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),因为你要使用浏览器中的,所以要改写成下面这样的代码:

    // @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) ? `
  • ${
    item}
  • ` : '' )) document.getElementById('itemlist').innerHTML = `
      ${
      items.join('')}
    `}

    第五步,第一次调用一下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
    = (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" } } ] },]

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

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

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

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

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

    你可能感兴趣的文章
    React非嵌套组件通信
    查看>>
    Websocket 使用指南
    查看>>
    浏览器兼容性问题解决方案 · 总结
    查看>>
    一个很棒的Flutter学习资源列表
    查看>>
    为什么你应该放弃React老的Context API用新的Context API
    查看>>
    Flutter 布局控件完结篇
    查看>>
    Koa2初体验
    查看>>
    Koa 2 初体验(二)
    查看>>
    Koa2框架原理解析和实现
    查看>>
    vue源码系列文章good
    查看>>
    你不知道的Virtual DOM
    查看>>
    VUE面试题总结
    查看>>
    写好JavaScript条件语句的5条守则
    查看>>
    原生JS中DOM节点相关API合集
    查看>>
    【TINY4412】U-BOOT移植笔记:(7)SDRAM驱动
    查看>>
    【TINY4412】U-BOOT移植笔记:(12)BEEP驱动
    查看>>
    单链表的修改和删除
    查看>>
    C++的三个基本特征:封装、继承、多态
    查看>>
    C++虚函数的总结
    查看>>
    什么是URL地址?
    查看>>