在学习Redux之前,不妨先了解下Flux:
Flux
- 同MVC一样,是一种架构思想,但更简单清晰
- 解决软件的结构问题,提供了一套数据流动方案
- 通过事件和监听实现数据单向流动,中心化控制
- View: 视图层
- Action(动作):视图层发出的消息
- Dispatcher(派发器):用来接收Actions、执行回调函数
- Store(数据层):用来存放应用的状态,动态更新Views页面
一个基本的流程可以描述为
- Your Views "Dispatch" "Actions"(视图触发事件)
- Your "Store" Responds to Dispatched Actions(store触发回调)
- Your Store Emits a "Change" Event(store触发change事件)
- Your View Responds to the "Change" Event(视图接收到事件重新渲染)
与React关系可以理解为:
Flux:是一个系统架构,用于推进应用中的数据单向流动 React:是一个JavaScript库,用于构建“可预期”和“声明式”的可组合式Web用户界面
问题
- 代码冗余
- dispatcher实例手动创建
- store既保存状态数据,又有处理逻辑:直接处理数据
- 程序可能会涉及多种数据结构,必然导致有多个store
- 深层次组件通信问题
参考
Redux
Redux是基于Flux架构思想的一个库的实现,JavaScript状态容器,提供可预测化的状态管理。
设计思想
- Web 应用是一个状态机,视图与状态是一一对应的。
- 所有的状态,保存在一个对象里面。
- 为应用程序提供一个可预测的状态容器
三大原则
- 单一数据源
- State 是只读的
- 使用纯函数来执行修改
显著特点
- 可预测性(Reducer 是纯函数)
- 可扩展性(middleware)
核心元素
- store
- action
- dispatch
- reducer
- subscribe
Redux中的 reducer 就是一个纯函数,store.dispatch(_action) 会自动触发 reducer 方法,更新state。
注意,reducer方法不会改变state,而是返回一个全新的state对象。
关于纯函数:同样的输入,必定得到同样的输出
- 不得改写参数
- 不能调用系统 I/O 的API
- 不能调用
Date.now()
或Math.random()
等不纯的方法,因为每次会得到不一样的结果
适用场景
多交互、多数据源
- 用户的使用方式复杂,不同身份的用户有不同的使用方式(比如普通用户和管理员),多个用户之间可以协作
- 与服务器大量交互,或者使用了WebSocket
- View要从多个来源获取数据
关于Redux的生态系统,请参见:http://www.redux.org.cn/docs/introduction/Ecosystem.html
store
本质:状态树
let { subscribe, dispatch, getState } = createStore(reducer);
关于createStore的基本实现,辅助理解
function createStore(reducer, initialState) { //闭包私有变量 var currentReducer = reducer; var currentState = initialState; var listeners = []; function getState() { return currentState; } function subscribe(listener) { listeners.push(listener); return function unsubscribe() { var index = listeners.indexOf(listener); listeners.splice(index, 1); }; } function dispatch(action) { currentState = currentReducer(currentState, action); listeners.slice().forEach(listener => listener()); return action; } //返回一个包含可访问闭包变量的公有方法 return {dispatch, subscribe, getState}; }
store可以看作是对reducer的封装,通过store.dispatch(action)自动触发对reducer的调用,而不是直接调用reducer(currentState, action),在一定程度上可以避免频繁传参,以更好地对store进行统一管理。
Action & Action Creator & bindActionCreators
- action:传递操作消息
- action creator:操作制造机
- bindActionCreators:对dispatch封装
var actionCreatorsNew = bindActionCreators(actionCreators, store.dispatch);
借鉴store对reducer的封装,对store.dispatch作封装,自动把actionCreators绑定到dispatch,使actionCreators成为具有操作全局state的函数集合。
其中,actionCreators表示action集合。触发action,会自动调用dispatch(action),避免直接对dispatch的调用。
middleware
异步场景:在异步操作结束后自动执行reducer
即,如何在操作结束时,自动送出第二个 Action
扩展点:dispatch()发出时,reducer()处理前
对于中间件,举例,日志中间件 redux-logger
import { applyMiddleware, createStore } from 'redux'; import createLogger from 'redux-logger'; const logger = createLogger(); const store = createStore(reducer, initial_state, applyMiddleware(logger) );
other,请注意中间件的引入次序。
给出 applymiddleware 的实现,仅供参考
export default function applyMiddleware(...middlewares) { return (createStore) => (reducer, preloadedState, enhancer) => { var store = createStore(reducer, preloadedState, enhancer); var dispatch = store.dispatch; var chain = []; var middlewareAPI = { getState: store.getState, dispatch: (action) => dispatch(action) }; chain = middlewares.map(middleware => middleware(middlewareAPI)); dispatch = compose(...chain)(store.dispatch); return {...store, dispatch} } }
其中,compose 用于组合函数、串联链式执行,顺序自右向左,维护扩展方便
var greeting = (firstName, lastName) => 'hello, ' + firstName + ' ' + lastName var toUpper = str => str.toUpperCase() var fn = compose(toUpper, greeting) console.log(fn('jack', 'smith')) // ‘HELLO,JACK SMITH’
异步情况,涉及发出三种不同种类的 Action
- 操作发起时的 Action
- 操作成功时的 Action
- 操作失败时的 Action
同时,state需要维护、以反映不同的操作状态
let state = { // ... isFetching: true, // 表示是否在抓取数据 didInvalidate: true, // 表示数据是否过时 lastUpdated: 'xxxxxxx' // 表示上一次更新时间 };
整个异步操作流程应该是这样的
- 操作开始时,送出一个 Action,触发 State 更新为"正在操作"状态,View 重新渲染
- 操作结束后,再送出一个 Action,触发 State 更新为"操作结束"状态,View 再一次重新渲染
在Redux中,中间件是纯粹的函数,有明确的使用方法并且严格的遵循以下格式:
var anyMiddleware = function ({ dispatch, getState }) { return function(next) { return function (action) { // 你的中间件业务相关代码 } } }
所有的这些,applyMiddleware会全部替我们封装实现。
异步解决方案可以引入中间件 redux-thunk 或 redux-promise
- redux-thunk:解决stroe.dispatch()的参数只能为action对象、不能是函数的问题
- 返回函数的 Action Creator(返回的function在合适的时机dispatch action)
- redux-promise:允许 Promise 对象作为stroe.dispatch()的参数,以类似同步的方式来组织代码
- 返回 Promise 对象的 Action Creator
- 或者,Action 对象的 payload 属性为 Promise 对象
但是,两者均是相对原始的解决方案,在action需要组合、取消时操作不易处理。最佳实践dva推荐 redux-saga,可测试、可mock、声明式的指令,管理actions,处理异步逻辑,管理所有的业务逻辑。
React-Redux
组件分类
- UI组件:presentational component,无状态的纯组件,只负责UI视觉
- 容器组件:container component,有状态,负责管理数据和逻辑
Redux的重要思想就是:容器组件和展示组件的分离
业务逻辑:针对UI组件
- 输入逻辑:外部的数据(即state对象)如何转换为 UI 组件的参数
- 输出逻辑:用户发出的动作如何变为 Action 对象,从 UI 组件传出去
在网上看到一个非常不错的关系图,分享给大家
connect
将 UI 组件生成容器组件:
const HocView = connect(mapStateToProps, mapDispatchToProps)(myComp)
其中,mapStateToProps
负责输入逻辑、将状态数据state映射到 UI 组件的参数props,mapDispatchToProps
负责输出逻辑、将用户对 UI 组件的操作映射成 Action。
mapStateToProps会订阅Store,每当state更新,就会自动执行,重新计算 UI 组件的参数,从而触发 UI 组件的重新渲染。
<Provider>
用来实现对store的全局访问,使容器组件获取到state:
render( <Provider store={store}> <Root /> </Provider>, document.getElementById('root') )
原理是利用React的 context 属性:
class Provider extends Component { getChildContext() { return { store: this.props.store }; } render() { return this.props.children; } } Provider.childContextTypes = { store: React.PropTypes.object }
子组件通过 this.context.store 获取。一个简单的计数器例子,供参见。
Redux扩展: redux-devtools-extension
- 时间旅行式调试工具,time-travelling tool
- 热重加载,hot reloading
- 基于数据不可变性(immutable)
使用步骤
- 在Chrome中安装Redux Devtools扩展
- npm安装
redux-devtools-extension
包 - import引入
参考
Redux教程(1-4) - 阮一峰;Redux视频前30集;后30集;
redux在react中的应用(基础篇);redux入门教程;
看漫画,学Redux;redux 三重境 - 对 redux 最佳实践的思考和总结;
redux-saga
前面提到 redux-saga 可以作为 Redux 的异步解决方案,简单学习之,为后面学习 dva 作个铺垫。
A Redux middleware for handling side effects (异步任务).
Redux中间件,基于ES6的Generator功能,用于管理应用程序Side Effect的库,副作用例如
- 异步获取数据
- 访问浏览器缓存
建议先了解下 Generator语法。通过redux-saga中间件将 Saga 与 Redux Store 建立连接:
import { createStore, applyMiddleware } from 'redux' import createSagaMiddleware from 'redux-saga' import reducer from './reducers' import mySaga from './sagas' const sagaMiddleware = createSagaMiddleware(); const store = createStore(reducer, applyMiddleware(sagaMiddleware)); sagaMiddleware.run(mySaga); /// then run the saga
其中,'./sagas' 用于处理所有异步操作逻辑,'./reducers' 用于处理action对stage更新。
参考
redux-saga | 中文教程; redux-saga-beginner-tutorial;
dva
由支付宝前端团队开发,相关历史可参见:支付宝前端应用架构的发展和选择: 从 roof 到 redux 再到 dva
- 基于现有应用架构 (redux + react-router + redux-saga 等)的一层轻量封装
- React + Redux 最佳实践,简化使用 redux 和 redux-saga 时繁杂的操作
- 组件耦合度低
- 结合了 react 和 vue 两者的优点,但格式固定、降低了灵活度
- 除 react 和 react-dom 外,封装了所有其他依赖
相关简介参见:dva - what&why -简介;
注意,dva 是 framework,而 redux 是 library。
最核心功能是提供 app.model 方法,用于把 reducer, initialState, action, saga 封装到一起
每个路由对应一个model,这个model掌管该路由的所有状态(action、state、reducer、sagas),组件想改变状态只要dispatch type即可。
参考