Redux 入门教程,应用的状态管理器

ReduxFlux演变而来,提供几个简单的API来实现状态管理,所谓状态指的是应用数据,所以,Redux本质上是用来管理数据的。
进一步,Redux配合支持数据绑定的视图库使用,就可以将应用状态和视图一一对应,开发者不需要再去关心DOM操作,只关心如何组织数据即可。

由于Redux对于数据的管理拆分很细,一时间会有很多概念,并且Redux有自己丰富的生态,所以容易眼花缭乱。
所以强烈建议从头开始一步一步的来,深入体验并理解Redux的思想,不要步子迈太大。
✦ 不要一开始过多的纠结代码放在哪个目录
✦ 不要一开始就想对actionreducer的代码做精简
✦ 不要一开始就考虑数据缓存,离线数据等问题
✦ 不要一开始就过度设计数据,考虑数据扁平化的问题
反正一句话,饭要一口一口的吃,路要一步一步的走,Redux对于状态管理的东西拆得太细,需要多花一些时间去体会。

Redux是什么?

Redux其实很简单,总结起来就三句话:
✦ 将整个应用的state储存在唯一的store对象中。
✦ state只能通过触发action来修改,其中action就是一个描述性的普通对象。
✦ 使用reducer来描述action如何改变state。

是的,简而言之就是:Redux让应用的数据被集中管理,并且只能通过触发action的方式来修改,而具体如何修改state,是由reducer来决定的。

那么问题来了:
✦ store是什么鬼?
✦ action是什么鬼?
✦ reducer是什么鬼?
✦ 最重要的是,为啥要使用Redux,它能给我们带什么什么好处?或者说,引入这么一个状态理器到底有啥用?

接下来,我们先捉这三只鬼。

store是什么鬼?

前面提过,Redux的目的就是为了对应用数据进行集中管理,也就是state,而state是个普通对象。为了防止state被不小心更新,Redux创建了store对象,专门用来管理state数据。

所以,store就是state的守门员,管理并维护应用数据。

创建store

我们通过createStore(reducer, [initialState], enhancer)的方式来创建store。需要注意的是,应用中应该有且只有一个store。

import { createStore } from 'redux'

// 这是reducer,后文会详细介绍
function todos(state = [], action) {
  switch (action.type) {
    case 'ADD_TODO':
      return state.concat([ action.text ])
    default:
      return state
  }
}

// 创建store,并且给state一个初始值['HTML']
let store = createStore(todos, [ 'HTML' ])

// state.dispatch(),最常用的API
// 修改state的唯一方式就是调用store.dispatch()方法
// 显然,其中的描述性对象
// {
//  type: 'ADD_TODO',
//  text: 'CSS'
// }
// 就是action
store.dispatch({
  type: 'ADD_TODO',
  text: 'CSS'
})

// store.getState(),另一个常用的API
// 用来获取state的值
console.log(store.getState());  // [ 'HTML', 'CSS' ]

store的API们

store的API很简单,这儿我按重要顺序列出所有的API,主要记住前两个。
✦ dispatch(action):触发action,再次声明,这是改变state的唯一方式,请默念两次
✦ getState():获取当前的state
✦ subscribe(listener):注册一个监听函数,state发生变化时触发
✦ replaceReducer(nextReducer):替换reducer,用得较少

总结一下,store提供了简单的API,用来管理应用内的数据,它限制了只能通过dispatch(action)来修改state,那么这个action是什么呢?

action是什么鬼?

前文提过,action就是一个描述性的普通对象,所以它非常简单!说白了,就是一坨数据,然后这坨数据有名字。

action

action是一个描述性的普通对象。推荐如下的action结构,type是action的名称,payload是附带的数据。

{
    // 显然,这个名字取得很浅显易懂
    type: UPDATE_ARTICLES_LIST,
    payload: {
        articles: articles,
        lastkey: lastkey
    }
}

值得注意的是:实际项目中,我们应该尽量减少action中附带的数据,比如想要更新某篇文章的标题,我们只需要携带文章id和文章新标题即可,而不需要携带整个新文章字段。
为了让action更便于维护,我们通常使用action creator而不是action。

action creator

action create就是一个简单的函数,直接将action作为返回值。

// action creator,返回一个action
// 除此之外,没有其他的动作
function updateArticlesList(normalizeData, lastkey) {
    return {
        type: UPDATE_ARTICLES_LIST,
        payload: {
            normalizeData: normalizeData,
            listLastkey: lastkey
        }
    }
}

// 通过dispatch触发一个action,这是我们修改state的唯一方式
dispatch(updateArticlesList(
    normalizeData,
    lastkey
));

// 将dispatch(action)整个动作取个别名,方便调用
const updatePosts = (normalizeData, lastkey) => {
    return dispatch(updateArticlesList(
        normalizeData,
        lastkey
    ));
}
updatePosts(...);

那么为什么需要action creatore呢?
试想一个场景,我们有好几处dispatch(action),现在突然想要修改这个action的定义,那么我们需要修改所有地方,代码也比较冗余!
而使用action creator,相当于对action做了简单的封装,避免了这些问题。既灵活又便于维护!

异步action creator

我们已经知道,修改state的唯一方式就是触发action,也就是dispatch(action)
但是如果是异步操作,比如一个网络请求,我们需要等到请求返回之后才会返回action,怎么办呢?

function updateArticlesList() {
    return GET(url).then(function(res) {
        // 难道直接return action?
        // 显然是不行的,这儿的返回值并不是updateArticlesList函数的返回值
        return action;
    }).catch(function(err) {
        console.log(err);
    });
}

对于异步场景,我们的解决方案是返回函数而不是直接返回action。就像下面这样。
为了让dispatch方法可以接受函数作为参数,我们需要使用redux-thunk这个中间件。

import thunk from 'redux-thunk';
import { rootReducer } from './reducer.js';

const store = createStore(
    rootReducer,
    applyMiddleware(thunk)
);

然后你就可以dispatch一个函数了

function fetchArticlesList() {
    // 传入dispatch/getstate,当然是为了获取state以及更新state
    return (dispatch, getState) => {
        return GET(url).then(function(res) {
            dispatch(updateArticlesList(
                normalizeData,
                lastkey
            ));
        }).catch(function(err) {
            console.log(err);
        });
    }
}

看起来有点迷糊?其实就是把异步请求抽象成action creator,然后放到了redux的代码中。
试想一下,如果没有这种方式,你会怎么去处理异步请求?
是不是会在组件或者页面中去发异步请求,然后在回调函数中dispatch(action)更新state。本质上也没太大区别。但是好处却是很明显的。

稍微提一下,如果我们可以使用async/await的话,异步action creator可以长得和同步action creator差不多。

action就是一坨数据,它并没有告诉Redux应该怎么去更新state,接下来介绍的reducer就是负责如何更新state这个工作的。

reducer是什么鬼?

action本身没有任何意义,就是一个描述性的普通对象。它并没有说明这个数据应该如何更新state。
具体如何更新state,是由reducer决定的。reducer的核心就一行代码:(state, action) => newstate

// ------------------------------------
// Action Handlers
// ------------------------------------
const ACTION_HANDLERS = {
    [UPDATE_ARTICLES_DETAIL]: (articles, action) => articles,
    [UPDATE_ARTICLES_LIST]: (articles, action) => {
        let payload = action.payload,
            normalizeData = payload.normalizeData,
            list = articles.list.concat(normalizeData.result),
            listLastkey = payload.listLastkey;

        // 更新articles.list字段和articles.lastkey字段
        // 这儿为什么不是state,而是articles呢?留着后文介绍
        return updateObject(articles, {
            list,
            listLastkey
        });
    }
}

// ------------------------------------
// Reducer
// ------------------------------------
export function articlesReducer(articles = {
    list: [],
    listLastkey: 0
}, action) {
    const handler = ACTION_HANDLERS[action.type]

    return handler ? handler(articles, action) : articles
}

reducer函数应该是纯函数,它要保证:只要传入参数相同,那么返回的新state就一定相同。
所以永远不要再reducer中做如下操作:
✦ 修改传入的state参数
✦ 执行有副作用的操作,比如API请求,路由跳转等
✦ 调用非纯函数,比如Math.random()或Date.now()

而一旦state变得复杂、层级较多的时候,如何设计reducer就是一个比较复杂的话题了。
关于如何设计state?如何分拆reducer?reducer之间如何共享数据?以及如何重构reducer的代码?可以移步另一篇博客:如何最佳实践的设计reducer

那么,回到最初的话题,引入Redux到我们的应用中,到底有什么好处?我们为什么需要一个专门的状态管理器?

为啥要使用redux?

早些时候,前端并没有这么复杂,几乎不怎么涉及数据管理。
随着前端的发展,前端也开始引入MVC之类的架构,对数据、视图、逻辑进行拆分处理。为了保持数据和视图的同步,我们会频繁的操作DOM元素。简直是噩梦。
而后KnockoutJS,angularJS等出现了,他们都支持数据绑定,终于让开发可以不在频繁的操作DOM,而是仅仅修改数据,然后自动同步到view。
但这还不够彻底,数据仍然是分散的。我们会在controller中写很多操作数据、操作视图的代码,甚至存在冗余数据,想要修改、更新、同步的话,有很大的隐患。
Redux的出现,提供了对数据的集中管理,让单向数据流成为了可能。
另外,Redux还让前后端彻底分离变成了可能,这一点也有极大的意义。

Redux的数据流

Redux通过一些限制告诉你:数据只能保存在我这儿,别想太分散!想要修改数据?告诉我一个带新数据的action,我会通过reducer自动修改,然后返回修改后的数据给你!
是的,redux很想“数据库”,数据被集中存储,并且只能通过“预先定义的action操作”来修改。

更厉害的是,配上支持数据绑定的视图库,你会发现一个神奇的事情:
之前我们是面向view和controller编程,随着项目的复杂,代码会彼此影响而且数据会分散到各处。
而引入redux之后,我们单纯的面向数据编程即可,我们在Redux中统一的管理数据,然后数据变换会反映到view上,而数据上的交互,本质上也是触发了Redux中的action。如下图


Redux数据流

所以,设计redux程序的时候,提前想清楚state的结构尤其重要,就好比设计数据库表结构之于后台。

服务器渲染让前后端彻底分离成为了可能

上图也可以看出,Redux构建出一份单向数据流。这让服务端渲染变成了可能,而这个特性,让前后端彻底分离变成了可能,还不用担心SEO的问题。
想当初,为了解决前后端分离的问题,大家费尽心思,奈何进展甚微,淘宝甚至提出中途岛midway项目,通过中间搭建由前端维护的Nodejs服务器来实现简单的渲染然后返回HTML,但其实这个Nodejs服务器一点都不简单,需要考虑太多东西,比如安全、性能、缓存等。

总结说点啥?

Redux主要用于对数据进行集中管理,并且让整个应用的数据流变得清晰。让应用开发更流畅,数据管理更有效。有了Redux,开发者们慢慢的转化为面向数据编程,而不再是频繁的操作DOM,维护越来越复杂的controller逻辑。
简单来说,Redux的东西不多,更重要的是理解它的思路:
✦ 将整个应用的state储存在唯一的store对象中。
✦ state只能通过触发action来修改,其中action就是一个描述性的普通对象。
✦ 使用reducer来描述action如何改变state。
✦ Redux的单向数据流,可以实现服务端渲染,让前后端彻底分离成为可能,这个有里程碑的意义。
✦ Redux非常适合复杂的应用,尤其是多交互、多数据源的应用。

还是那句话,Redux将数据管理拆得很细,所以会有很多新东西去了解,但其实只要了解它的思想,其他的就很顺其自然了。

更多关于Redux的内容,可以查看其它文章:
State 设计,Reudx 开发第一步
Reducer 最佳实践,Redux 开发最重要的部分

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 143,048评论 1 300
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 61,330评论 1 256
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 94,541评论 0 213
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 41,042评论 0 176
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 48,821评论 1 255
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 38,795评论 1 176
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 30,413评论 2 271
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,173评论 0 167
  • 想象着我的养父在大火中拼命挣扎,窒息,最后皮肤化为焦炭。我心中就已经是抑制不住地欢快,这就叫做以其人之道,还治其人...
    爱写小说的胖达阅读 28,994评论 6 229
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 32,525评论 0 213
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,327评论 2 215
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 30,648评论 1 230
  • 白月光回国,霸总把我这个替身辞退。还一脸阴沉的警告我。[不要出现在思思面前, 不然我有一百种方法让你生不如死。]我...
    爱写小说的胖达阅读 24,248评论 0 31
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,142评论 2 214
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 31,521评论 3 204
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,622评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,025评论 0 166
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 33,543评论 2 230
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 33,636评论 2 231

推荐阅读更多精彩内容