Redux

Redux的核心运作流程

Redux这个npm包,提供若干API让我们使用reducer创建store,并能更新store中的数据或获取store中最新的状态。“Redux应用”指使用redux结合视图层实现(React)及其他前端应用必备组件(路由库、Ajax请求库)组成的完成类Flux思想的前端应用。

Redux 三大原则

  1. 单一数据源
    Redux思想中,一个应用永远只有唯一的数据源。combineReducers化解了数据源对象过于庞大的问题。

  2. 状态是只读的
    Redux中,我们并不会自己用代码来定义一个store。取而代之的是,我们定义一个rducer,它的功能是根据当前触发的action对当前应用的状态(state)进行迭代,这里我们斌没有直接修改应用的状态,而是返回一份全新的状态。
    Redux提供的createStore方法会根据reducer生成store。最后,我们利用 store.dispatch 方法来达到修改状态的目的。

  3. 状态修改均有纯函数完成
    在Redux中,我们通过定义reducer来确定状态的修改。

Redux核心API

Redux的核心是一个store,这个store由Redux提供的 createStore(reducers, [,initialState])方法生成。

Redux里,负责响应action并修改数据的角色是reducer。其函数签名为reducer(previousState, action ) => newState。所以,reducer的职责就是根据 previousState 和 action 计算出新的 newState。

// MyReducer.js
const initialState = {
  todos: [],
}

// 我们定义的todos这个reducer在第一次执行的时候,会返回  { todos: [] }作为初始化状态
function todos(previousState = initalState, action) {
  switch(action.type) {
    case 'xx': {
      // 具体的业务逻辑
    }

    default: 
      return previousState;
  }
}

Redux = Reducer + Flux
通过createStore方法创建的store是一个对象,它本身又包含4个方法

  • getState():获取store中当前的状态
  • dispatch(action): 分发一个action,并返回这个action,这是唯一能改变 store 中数据的方式
  • subscribe(listener):注册一个监听者,它在store发生变化时被调用
  • replaceReducer(nextReducer):更新当前store里的reducer,一般只会在开发模式中调用该方法

与React绑定

react-redux提供了一个组件和一个API帮助Redux和React进行绑定,一个是React组件<Provider />,一个是 connect(). <Provider />接受一个store作为props,是整个Redux应用的顶层组件,connect()提供整个React应用的任意组件中获取store中数据的功能。

Redux middleware

它提供了一个分类处理action的机会。


Redux同步数据流动

面对多样的业务场景,单纯地修改 dispatch 或 reducer 的代码显然不具有普适性,我们需要的是可以组合的、自由插拔的插件机制,这一点 Redux借鉴了 Koa (它是用于构建 Web 应用的Node.js 框架)里 middleware 的思想。


应用middleware后Redux处理事件的逻辑

理解middleware机制

Redux提供了applyMiddleware方法来加载middleware。
Redux中的applyMiddleware源码

import compose from './compose'

export default function applyMiddleware(...middlewares) {
  return (next) => (reducer, initialState) => {
    let store = next(reducer, initialState)
    let dispatch = store.dispatch
    let chain = []
    // 把store的getState方法和dispatch方法分别直接或间接赋值给middlewareAPI
    var middlewareAPI = {
      getState: store.getState,
      dispatch: (action) => dispatch(action)
    }
    // 让每个middleware带着middlewareAPI这个参数分别执行一遍
    // 执行完后,获得数组  [f1,f2,f3,f4,...,fn]
    // middlewareAPI第二个箭头函数返回的匿名函数,因为闭包,每个匿名函数都可以访问相同的store,即middlewareAPI

  /*
middlewareAPI中的dispatch为什么要用匿名函数包裹呢?
我们用 applyMiddleware 是为了改造 dispatch,所以 applyMiddleware 执行完后,dispatch 是 变化了的,而 middlewareAPI 是 applyMiddleware 执行中分发到各个 middleware 的,所以 必须用匿名函数包裹 dispatch,这样只要 dispatch 更新了,middlewareAPI 中的 dispatch 应 用也会发生变化。
*/
    chain = middlewares.map(middleware => middleware(middlewareAPI))
    dispatch = compose(...chain)(store.dispatch)

    return {
      ...store,
      dispatch
    }
  }
}
  1. 函数式编程思想设计
    middleware设计的是一层层包裹的匿名函数,这其实是函数式编程中的currying,他是一种私用匿名单参数函数来实现多参数函数的方法。
    currying的middleware结构的好处有以下两点:
  • 易串联:currying函数具有延迟执行的特性,通过不断currying形成的middleware可以累积参数,再配合组合(compose)的方式,很容易形成pipeline来处理数据流
  • 共享store:在applyMiddleware执行的过程中,store还是旧的,但是因为闭包的存在,applyMiddleware完成后,所有的middleware内部拿到的store是最新且相同的。
import { createStore, applyMiddleware, compose } from 'Redux';
import rootReducer from '../reducers';

const finalCreateStore = compose(
    // 在开发环境中使用 middleware
    applyMiddleware(d1,  d2, d3),
    DevTools.instrument()
)
  1. 给middleware分发store
    let newStore = applyMiddleware(mid1,mid2,mid3,...)(createStore)(reducer, null)
    看注释。

  2. 组合串联middleware
    dispatch = compose(...chain)(store.dispatch);

// compose 实现方式
function compose(...funs) {
  return arg => func.reduceRight((composed, f) => f(composed), arg)
}

compose(...funcs)返回一个匿名函数,其中funcs就是chain数组。当调用reduceRight时,依次从funcs数组的右端取一个函数fx拿来执行,fx的参数composed就是前一次fx+1执行的结果,而第一次执行的fn(n代表chain的长度)的参数arg就是 store.dispath.假设n=3
dispatch = f1(f2(f3(store.dispatch)))
这时调用新dispatch时,每一个middleware都依次执行了。

  1. 在middleware中调用dispatch会发声什么
    compose之后,所有的middleware算是串联起来了。可是还有一个问题,在分发store时,我们提到过每个middleware都可以访问store,即middlewareAPI这个变量,也可以拿到store的dispatch属性。那么,在middleware中调用store.dispatch()会发生什么,和调用next()有什么区别?
const logger = store => next => action => {       
  console.log('dispatch:', action); 
  next(action);
  console.log('finish:', action);
};
const logger = store => next => action => { c 
  onsole.log('dispatch:', action); 
  store.dispatch(action); 
  console.log('finish:', action);
};

在分发 store 时我们解释过,middleware 中 store 的 dispatch 通过匿名函数的方式和最终 compose 结束后的新 dispatch 保持一致,所以,在 middleware 中调用 store.dispatch() 和在其他 任何地方调用的效果一样。而在 middleware 中调用 next(),效果是进入下一个 middleware。

这就是一个洋葱模型
next代表下一个执行的中间件,每次return回去的都是一个未执行的函数,只有最后调用才能执行。

// 实现之后的效果
// 这时候返回的是一个经过层层中间件封装的dispatch,新的dispatch函数
newDispatch = M1(M2(M3(dispatch)))
// M1中的next 就是M2
// M2中的next就是M3
// M3中的next就是dispatch,执行dispatch
// 调用
newDispatch(action)
Redux middleware流程图

正常情况下,如上图左,我们分发一个action时,middleware通过next(action)一层层传递和处理action直到Redux原生的dispathc。当某个middleware使用store.dispatch(action)分发action,会发声右图的情况,就会形成无限循环。那么store.dispatch(action)的用武之地在哪里呢?
异步请求的时候,使用到Redux Thunk

const thunk = store => next => action => {
  typeof action === 'function'?
    action(store.dispatch, store.getState) : next(action)
}

Redux Thunk会判断action是否是函数。如果是,执行action,否则继续传递action到下一个middleware。

const getThenShow = (dispatch, getState) => {
  const url = 'http://xxx.json'
  fetch(url)
    .then((res) => {
      dispatch({
        type: 'SHOW_MESSAGE_FOR_ME',
        message: res.json(),
      })
    }).catch( ()=> {
      dispatch({
        type: 'FETCH_DATA_FAIL',
        message: 'error'
      })
    } )
}

// 再应用中调用 store.dispatch(getThenShow)

Redux异步流

使用middleware简化异步请求

  1. redux-thunk
    Thunk函数实现上就是针对多参数的currying以实现对函数的惰性求值。任何函数,只要参数有回调函数,就能写成Thunk函数的形式。
    redux-thunk的源代码:
function createThunkMiddleware(extraArg) {
  return ({dispath, getState} => next => action => {
    if ( typeof action === 'function' ) {
      return action(dispatch, getState, extraArg)
    }
    return next(action)
  })
}
  1. redux-promise
    抽象promise来解决异步流问题。
    redux-promise 兼容了 FSA 标准,也就是说将返回的结果保存在 payload 中。实现过程非常容易理解,即判断 action 或action.payload是否为 promise,如果是,就执行 then,返回的结果再发送一次 dispatch。

使用ES7的async和await语法,简化异步过程

const fetchData = (url, params) => fetch(url, params);

async function getWeather(url, params) {
  const result = await fetchData(url, params);
  if( result.error ) {
    return {
      type: 'GET_WEATHER_ERROR',
      error: result.error
    }
  }

  return {
    type: 'GET_WEATHER_SUCCESS',
    payload: result
  }
}
  1. redux-composable-fetch
    实际请求中,加上loading状态
    这时候异步请求的action
{
url: '/api/weather.json',
params: {
city: encodeURI(city),
},
types: ['GET_WEATHER', 'GET_WEATHER_SUCESS', 'GET_WEATHER_ERROR'],
}

和FSA不一样了,没有types,有了url和type代表请求状态

const fetchMiddleware = store => next => action => {
  if (!action.url || !Array.isArray(action.types)) {
    return next(action);
  }
  const [LOADING, SUCCESS, ERROR] = action.types;
  next({
    type: LOADING,
    loading: true,
    ...action,
  });
  fetch(action.url, { params: action.params })
    .then(result => {
    next({
      type: SUCCESS,
      loading: false,
      payload: result,
    });
  })
  .catch(err => {
    next({
      type: ERROR,
      loading: false,
      error: err,
    });
  });
}

使用middleware处理复杂异步流

1.轮询

  1. 多异步串联
    使用Promise

  2. redux-saga
    最优雅通用的解决方法,有灵活而强大的协程机制,可以解决任何复杂的异步交互。

Redux和路由

我们需要一个这样的路由系统,它既能利用React Router 的声明式特性,又能将路由信息整合进 Redux store 中。

React Router

1.基本原理


React Router流程图
  1. React Router特性


    React Router与React对比
  • 声明式的路由
// 实例
import { Router, Route, browserHistory } from 'react-router';
const routes = (
  <Router history={browserHistory}>
    <Route path='/' component{App} />
  </Router>
)
  • 嵌套路由及路径匹配
import { Router, Route, IndexRoute, browserHistory } from 'react-router';
const routes = (
  <Router history={browserHistory}>
    <Route path="/" component={App}>
      <IndexRoute component={MailList} />
      <Route path="/mail/:mailId" component={Mail} />
    </Route>
  </Router>
);

App 组件承载了显示顶栏和侧边栏的功能,而 React Router 会根据当前的 url 自动判断该显示邮件列表页还是详情页:
. 当 url 为 / 时,显示列表页;
. 当 url 为 /mail/123 时,显示详情页。

  • 支持多种路由切换方式
    hashChange 或是 history.pushState。hashChange 的方式拥有良好的浏览器兼容性,但是 url 中却多了丑陋的 /#/ 部分;而 history.pushState 方法则能给我们提供优雅的 url,却需要额外的服务端配置解决任意路径刷新的问题。

React Router Redux

当我们采用 Redux 架构时,所有的应用状态必须放在一个单一的 store 中管理,路由状态也不例外。而这就是 React Router Redux 为我们实现的主要功能。

  1. React Router与Redux store绑定
    React Router Redux 提供了简单直白的 API——syncHistoryWithStore 来完成与 Redux store的绑定工作。
import { browserHistory } from 'react-router'
import { syncHistoryWithStore } from 'react-router-redux'
import reducers from '<project-path>/reducers'
const store = createStore(reducers);
const history = syncHistoryWithStore(browserHistory, store);
  1. 用Redux的方式改变路由
    对Redux的store进行增强,以便分发的action能被正确识别
import { browserHistory } from 'react-router';
import { routerMiddleware } from 'react-router-redux';
const middleware = routerMiddleware(browserHistory);
const store = createStore(
  reducers,
  applyMiddleware(middleware)
);

使用

import { push } from 'react-router-redux';
// 切换路由到 /home
store.dispatch(push('/home'));

Redux 与 组件

对比容器组件和展示型组件

Redux中,强调了3中不同类型的布局组件:Layouts、Views和Components。它常常是无状态函数,传入主体内容的children属性。

const Layout = ({ children } => {
  <div className = 'container'>
    <Header />
    <div className="contaier">
      { children }
    </div>
  </div>
})
  1. Views
    子路由入口组件,描述子路由入口的基本结构,包含此路由下所有的展示型组件。
@connect((state) => {
//...
})
class HomeView extends Component {
render() {
  const { sth, changeType } = this.props;
  const cardProps = { sth, changeType };
  return (
    <div className="page page-home">
      <Card {...cardProps} />
    </div>
    );
  }
}

3、Components
末级渲染组件,描述了从路由以下的子组件。包含具体的业务逻辑和交互,但所有的数据和action都是油Views传下来。

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

推荐阅读更多精彩内容