redux简介(一)基本使用

写在开头

本片内容主要为本人在阅读redux官方文档中基础和进阶部分的学习笔记。由于本人能力有限,所以文章中可能会存在不合理的地方,欢迎指正,内容也会迭代更新。
本篇为介绍redux的第一篇,主要介绍了基本的使用方法。

前言

redux是一个JavaScript状态容器,主要提供状态管理。可以运行于服务端,客户端,原生应用。除了支持React外还支持其他的UI框架,体积小,只有2kb。

redux受Elm的启发,从Flux演变而来。简单容易上手。

redux由于行为可以预测,所以可以实现,日志打印,热加载,时间旅行,同构应用,录制和重放等,不需要开发参与。

开始使用

  • 确认所开发的程序是否需要使用redux。对于较为简单的应用,完全没有必要为了使用redux而使用redux,redux会增加代码的复杂度。
  • 参考资料你

安装

npm install redux --save
npm install redux-thunk --save
npm install redux-logger --save
npm install react-redux --save
npm install redux-devtools --save-dev

npm i redux redux-thunk redux-logger --save
// redux-thunk用于支持异步的action,redux-logger用于打印log,主要用于在开发阶段。react-redux用于和react结合使用

问什么要使用redux

  • 使用JavaScript开发的单页应用逻辑复杂。有较多的状态(state)需要JavaScript去维护。例如:服务器返回数据,本低为持久化的数据,UI状态。或者分页等等。
  • 将变化和异步混合在一起会增加程序的复杂性,使得状态难以被管理。React在视图层禁止异步不鼓励直接操作DOM,来解决这个问题。不过react中的数据(state)需要自己来进行处理,redux就是为了解决这个问题
  • Redux试图让state的变化变得可预测。

redux的核心

使用state来存储数据,只能触发action通过reducer来更改state。

redux的三大原则

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

先前技术

链接

redux生态系统

中间件

  • redux-thunk — 用最简单的方式搭建异步
  • action 构造器
  • redux-promise — 遵从 FSA 标准的 promise 中间
  • redux-axios-middleware — 使用 axios HTTP 客户端获取数据的 Redux 中间件
  • redux-observable — Redux 的 RxJS 中间件
  • redux-rx — 给 Redux 用的 RxJS 工具,包括观察变量的中间件
  • redux-logger — 记录所有 Redux action 和下一次 state 的日志
  • redux-immutable-state-invariant — 开发中的状态变更提醒
  • redux-unhandled-action — 开发过程中,若 Action 未使 State 发生变化则发出警告
  • redux-analytics — Redux middleware 分析
  • redux-gen — Redux middleware 生成器
  • redux-saga — Redux 应用的另一种副作用 model
  • redux-action-tree — Redux 的可组合性 Cerebral-style 信号
  • apollo-client — 针对 GraphQL 服务器及基于 Redux 的 UI 框架的缓存客户端

路由

  • redux-simple-router — 保持 React Router 和 Redux 同步
  • redux-router — 由 React Router 绑定到 Redux 的库

更多

基础知识

Action

  • action用于将数据传入store。本质是普通的JavaScript对象,必须含有type字段。
  • 应该尽量减少在action中传递数据。
const ADD_TODO = 'ADD_TODO';
{
    type: ADD_TODO,
    text: 'Build my first Redux app'
}

使用函数创建Action

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

Action创建函数可以是异步非纯函数。
在store里能直接通过store.dispatch()调用dispatch()方法。但是在redux应用中通常会使用connect()帮助器来调用,启动bindActionCreators()可以自动把多个action创建的函数绑定到dispatch()方法上。

Reducer

设计state结构

  • state范式化,不要存在嵌套把state比拟成数据库。进行适当的数据扁平化。参考normalizr

reducer

  • reducer接受旧的satte和action,返回新的action
(previousState, action) => newState

reducer名字的由来是因为此函数与Array.prototype.reduce(reducer,?initiaValue)里的回调函数类型相似。

reducer禁止的操作

  • 修改传入的参数
  • 执行有副作用操作,Ajax请求,路由跳转等
  • 调用非纯函数:Date.now()或Math.random()

慎记:只要传入参数相同,返回计算得到的下一个 state 就一定相同。没有特殊情况、没有副作用,没有 API 请求、没有变量修改,单纯执行计算

import { VisibilityFilters } from './actions'

const initialState = {
  visibilityFilter: VisibilityFilters.SHOW_ALL,
  todos: []
};

function todoApp(state = initialState, action) {
  switch (action.type) {
    case SET_VISIBILITY_FILTER:
      return Object.assign({}, state, {
        visibilityFilter: action.filter
      })
    default:
      return state
  }
}

注意

  1. 不要修改state。使用Object.assign({},state,newState),或者{...state,...newState}
  2. 在default情况下返回旧的state。

拆分reducer
注意每个 reducer 只负责管理全局 state 中它负责的一部分。每个 reducer 的 state 参数都不同,分别对应它管理的那部分 state 数据。

使用combineReducers()

import { combineReducers } from 'redux'

const todoApp = combineReducers({
  visibilityFilter,
  todos
})

等价

function todoApp(state = {}, action) {
  return {
    visibilityFilter: visibilityFilter(state.visibilityFilter, action),
    todos: todos(state.todos, action)
  }
}

combineReducers()生成一个函数,这个函数来调用你的一系列 reducer,每个 reducer 根据它们的 key 来筛选出 state 中的一部分数据并处理,然后这个生成的函数再将所有 reducer 的结果合并成一个大的对象。没有任何魔法。正如其他 reducers,如果 combineReducers() 中包含的所有 reducers 都没有更改 state,那么也就不会创建一个新的对象。

es6 使用import *

import { combineReducers } from 'redux'
import * as reducers from './reducers'

const todoApp = combineReducers(reducers)

Store

职责
  • 维持应用的state
  • 提供 getState() 方法获取 state;
  • 提供 dispatch(action) 方法更新 state;
  • 通过 subscribe(listener) 注册监听器;
  • 通过 subscribe(listener) 返回的函数注销监听器。
创建store
import { createStore } from 'redux'
import todoApp from './reducers'
let store = createStore(todoApp)

let store = createStore(todoApp, window.STATE_FROM_SERVER)

let store = createStore(todoApp, window.STATE_FROM_SERVER,applyMiddleware())
发起Actions
import {
  addTodo,
  toggleTodo,
  setVisibilityFilter,
  VisibilityFilters
} from './actions'

// 打印初始状态
console.log(store.getState())

// 每次 state 更新时,打印日志
// 注意 subscribe() 返回一个函数用来注销监听器
const unsubscribe = store.subscribe(() =>
  console.log(store.getState())
)

// 发起一系列 action
store.dispatch(addTodo('Learn about actions'))

// 停止监听 state 更新
unsubscribe();

数据流

严格的单向数据流是 Redux 架构的设计核心
Redux 应用中数据的生命周期遵循下面 4 个步骤。

  1. 调用 store.dispatch(action)
  2. Redux store 调用传入的 reducer 函数。
  3. 根 reducer 应该把多个子 reducer 输出合并成一个单一的 state 树。
  4. Redux store 保存了根 reducer 返回的完整 state 树。

进阶

异步Action

需求:发送异步的Action

  • 一种通知reducer请求开始的action
  • 一种通知reducer请求成功的action
  • 一种通知action请求失败的action

实现

export const REQUEST_POSTS = 'REQUEST_POSTS';
export const RECEIVE_POSTS = 'RECEIVE_POSTS';
export const INVALIDATE_SUBREDDIT = 'INVALIDATE_SUBREDDIT';


function requestPosts(subreddit) {
    return {
        type: REQUEST_POSTS,
        subreddit
    }
}

function receivePosts(subreddit, json) {
    return {
        type: RECEIVE_POSTS,
        subreddit,
        posts: json.data.children.map(child => child.data),
        receivedAt: Date.now()
    }
}

 export function invalidateSubreddit(subreddit) {
     return {
         type: INVALIDATE_SUBREDDIT,
         subreddit
     }
 }

// 来看一下我们写的第一个 thunk action 创建函数!
// 虽然内部操作不同,你可以像其它 action 创建函数 一样使用它:
// store.dispatch(fetchPosts('reactjs'))

export function fetchPosts(subreddit) {

    // Thunk middleware 知道如何处理函数。
    // 这里把 dispatch 方法通过参数的形式传给函数,
    // 以此来让它自己也能 dispatch action。

    return function (dispatch) {

        // 首次 dispatch:更新应用的 state 来通知
        // API 请求发起了。

        dispatch(requestPosts(subreddit))
        //可以多次dispatch
        // thunk middleware 调用的函数可以有返回值,
        // 它会被当作 dispatch 方法的返回值传递。

        // 这个案例中,我们返回一个等待处理的 promise。
        // 这并不是 redux middleware 所必须的,但这对于我们而言很方便。

        return fetch(`http://www.subreddit.com/r/${subreddit}.json`)
            .then(
                response => response.json(),
                // 不要使用 catch,因为会捕获
                // 在 dispatch 和渲染中出现的任何错误,
                // 导致 'Unexpected batch number' 错误。
                // https://github.com/facebook/react/issues/6895
                error => console.log('An error occurred.', error)
            )
            .then(json =>
                // 可以多次 dispatch!
                // 这里,使用 API 请求结果来更新应用的 state。

                dispatch(receivePosts(subreddit, json))
            )
    }
}

引入thunk函数

import thunkMiddleware from 'redux-thunk'
import {createLogger} from 'redux-logger'
import {createStore, applyMiddleware} from 'redux'
import {selectSubreddit, fetchPosts} from './actions'
import rootReducer from './reducers'

const store = createStore(
    rootReducer,
    applyMiddleware(
        thunkMiddleware, // 允许我们 dispatch() 函数
        createLogger() // 一个很便捷的 middleware,用来打印 action 日志
    )
);

store.dispatch(selectSubreddit('reactjs'));
store.dispatch(fetchPosts('reactjs')).then(
    () => console.log(store.getState())
);

除了redux-thunk不是处理异步action的唯一方式。

  • 你可以使用 redux-promise 或者 redux-promise-middleware 来 dispatch Promise 来替代函数。
  • 你可以使用 redux-observable 来 dispatch Observable。
  • 你可以使用 redux-saga 中间件来创建更加复杂的异步 action。
  • 你可以使用 redux-pack 中间件 dispatch 基于 Promise 的异步 Action。
  • 你甚至可以写一个自定义的 middleware 来描述 API 请求,就像这个 真实场景的案例 中的做法一样

异步数据流

默认情况下使用createStore()创建的只支持同步数据流,不过可以通过applyMiddleware()来增强createStore()以使其支持异步action。
redux-thunk或redux-promise通过封装store.dispatch()来实现对于异步的支持。使用middleware可以以自定义的方式解决dispatch的任何内容,并传递action给下一个middleware。支持Promise的middleware能够拦截Promise,然后为每个Promsie异步地dispatch一对begin/end。
需要确保middleware最后一个middleware开始dispatch action,这个action必须是一个普通代码。

Middleware

Redux middleware提供的是位于action被发现之后,到达reducer之前的扩展点。

Middleware 接收了一个 next() 的 dispatch 函数,并返回一个 dispatch 函数,返回的函数会被作为下一个 middleware 的 next(),以此类推。由于 store 中类似 getState() 的方法依旧非常有用,我们将 store 作为顶层的参数,使得它可以在所有 middleware 中被使用。

/**
 * 让你可以发起一个函数来替代 action。
 * 这个函数接收 `dispatch` 和 `getState` 作为参数。
 *
 * 对于(根据 `getState()` 的情况)提前退出,或者异步控制流( `dispatch()` 一些其他东西)来说,这非常有用。
 *
 * `dispatch` 会返回被发起函数的返回值。
 */
const thunk = store => next => action =>
  typeof action === 'function' ?
    action(store.dispatch, store.getState) :
    next(action)

技巧

1.使用对象展开运算符
2.缩减样板代码
Actions

action需要拥有一个不变的type帮助reducer识别他们,因为string是可序列化的,使用Symbol()会使纪录和重载变得困难,所以推荐使用string
将每个action type定义为string常量。

  • 维护命名的一致性
  • 避免冲突
  • 及时追踪新功能的范围和实现
  • 减少由于书写造成的错误的可能性
Action Creators
// event handler 里的某处
dispatch({
  type: 'ADD_TODO',
  text: 'Use Redux'
});

export function addTodo(text) {
  return {
    type: 'ADD_TODO',
    text
  };
}

// event handler 里的某处
dispatch(addTodo('Use Redux'))

使用Action creators的优势。

例如,最多只允许三个todo

function addTodoWithoutCheck(text) {
  return {
    type: 'ADD_TODO',
    text
  };
}

export function addTodo(text) {
  // Redux Thunk 中间件允许这种形式
  // 在下面的 “异步 Action Creators” 段落中有写
  return function (dispatch, getState) {
    if (getState().todos.length === 3) {
      // 提前退出
      return;
    }

    dispatch(addTodoWithoutCheck(text));
  }
}

Action creator 可以解耦额外的分发 action 逻辑与实际发送这些 action 的 components的耦合。

ActionCreators生成器

 export function addTodo(text) {
  return {
    type: 'ADD_TODO',
    text
  }
}

export function editTodo(id, text) {
  return {
    type: 'EDIT_TODO',
    id,
    text
  }
}

export function removeTodo(id) {
  return {
    type: 'REMOVE_TODO',
    id
  }
}
function makeActionCreator(type, ...argNames) {
  return function(...args) {
    let action = { type }
    argNames.forEach((arg, index) => {
      action[argNames[index]] = args[index]
    })
    return action
  }
}

const ADD_TODO = 'ADD_TODO'
const EDIT_TODO = 'EDIT_TODO'
const REMOVE_TODO = 'REMOVE_TODO'

export const addTodo = makeActionCreator(ADD_TODO, 'todo')
export const editTodo = makeActionCreator(EDIT_TODO, 'id', 'todo')
export const removeTodo = makeActionCreator(REMOVE_TODO, 'id')
异步Action Creators

中间件 让你在每个 action 对象 dispatch 出去之前,注入一个自定义的逻辑来解释你的 action 对象
中间件让我们能写表达更清晰的、潜在的异步 action creators。允许我们 dispatch 普通对象之外的东西,并且解释它们的值。比如,中间件能 “捕捉” 到已经 dispatch 的 Promises 并把他们变为一对请求和成功/失败的 action。

redux-thunk. “Thunk” 中间件让你可以把 action creators 写成 “thunks”,也就是返回函数的函数。 这使得控制被反转了: 你会像一个参数一样取得 dispatch ,所以你也能写一个多次分发的 action creator 。

export function loadPosts(userId) {
  // 用 thunk 中间件解释:
  return function (dispatch, getState) {
    let { posts } = getState();
    if (posts[userId]) {
      // 这里是数据缓存!啥也不做。
      return;
    }

    dispatch({
      type: 'LOAD_POSTS_REQUEST',
      userId
    });

    // 异步分发原味 action
    fetch(`http://myapi.com/users/${userId}/posts`).then(
      response => dispatch({
        type: 'LOAD_POSTS_SUCCESS',
        userId,
        response
      }),
      error => dispatch({
        type: 'LOAD_POSTS_FAILURE',
        userId,
        error
      })
    );
  }
}

未避免多次重复代码,使用middleware进行处理。

Reducers生成器

写一个函数将 reducers 表达为 action types 到 handlers 的映射对象。例如,如果想在 todos reducer 里这样定义:

export const todos = createReducer([], {
  [ActionTypes.ADD_TODO](state, action) {
    let text = action.text.trim();
    return [...state, text];
  }
})

可以编写下面的辅助函数来完成:

function createReducer(initialState, handlers) {
  return function reducer(state = initialState, action) {
    if (handlers.hasOwnProperty(action.type)) {
      return handlers[action.type](state, action);
    } else {
      return state;
    }
  }
}

鉴于写法多种多样,Redux 没有默认提供这样的辅助函数

写在最后

谢谢阅读,欢迎点赞,

推荐阅读原版英文/中文文档。在可以灵活使用的基础上,下载源码进行阅读学习,相信会有很多的收获,加油 💪💪💪

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,311评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,339评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,671评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,252评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,253评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,031评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,340评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,973评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,466评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,937评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,039评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,701评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,254评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,259评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,485评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,497评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,786评论 2 345

推荐阅读更多精彩内容