介绍
什么是redux
Redux是一个流行的JavaScript框架,为应用程序提供一个可预测的状态容器。Redux基于简化版本的Flux框架,Flux是Facebook开发的一个框架。在标准的MVC框架中,数据可以在UI组件和存储之间双向流动,而Redux严格限制了数据只能在一个方向上流动
动机
随着 JavaScript 单页应用开发日趋复杂,JavaScript 需要管理比任何时候都要多的 state (状态)。 这些 state 可能包括服务器响应、缓存数据、本地生成尚未持久化到服务器的数据,也包括 UI 状态,如激活的路由,被选中的标签,是否显示加载动效或者分页器等等。
为什么要管理状态
管理不断变化的 state 非常困难。如果一个 model 的变化会引起另一个 model 变化,那么当 view 变化时,就可能引起对应 model 以及另一个 model 的变化,依次地,可能会引起另一个 view 的变化。直至你搞不清楚到底发生了什么。state 在什么时候,由于什么原因,如何变化已然不受控制。 当系统变得错综复杂的时候,想重现问题或者添加新功能就会变得举步维艰。
跟随 Flux、CQRS 和 Event Sourcing 的脚步,通过限制更新发生的时间和方式,Redux 试图让 state 的变化变得可预测。这些限制条件反映在 Redux 的三大原则中。
三大原则
Redux 可以用这三个基本原则来描述:
单一数据源整个应用的 state 被储存在一棵 object tree 中,并且这个 object tree 只存在于唯一一个 store 中。
这让同构应用开发变得非常容易。来自服务端的 state 可以在无需编写更多代码的情况下被序列化并注入到客户端中。由于是单一的 state tree ,调试也变得非常容易。在开发中,你可以把应用的 state 保存在本地,从而加快开发速度。此外,受益于单一的 state tree ,以前难以实现的如“撤销/重做”这类功能也变得轻而易举。
console.log(store.getState())
/ 输出 { visibilityFilter: ‘SHOW_ALL’, todos: [ { text: ‘Consider using Redux’, completed: true, }, { text: ‘Keep all state in a single tree’, completed: false } ] } / State 是只读的 唯一改变 state 的方法就是触发 action,action 是一个用于描述已发生事件的普通对象。
这样确保了视图和网络请求都不能直接修改 state,相反它们只能表达想要修改的意图。因为所有的修改都被集中化处理,且严格按照一个接一个的顺序执行,因此不用担心 race condition 的出现。 Action 就是普通对象而已,因此它们可以被日志打印、序列化、储存、后期调试或测试时回放出来。
store.dispatch({ type: ‘COMPLETE_TODO’, index: 1 })
store.dispatch({ type: ‘SET_VISIBILITY_FILTER’, filter: ‘SHOW_COMPLETED’ }) 使用纯函数来执行修改 为了描述 action 如何改变 state tree ,你需要编写 reducers。
Reducer 只是一些纯函数,它接收先前的 state 和 action,并返回新的 state。刚开始你可以只有一个 reducer,随着应用变大,你可以把它拆成多个小的 reducers,分别独立地操作 state tree 的不同部分,因为 reducer 只是函数,你可以控制它们被调用的顺序,传入附加数据,甚至编写可复用的 reducer 来处理一些通用任务,如分页器。
function visibilityFilter(state = ‘SHOW_ALL’, action) { switch (action.type) { case ‘SET_VISIBILITY_FILTER’: return action.filter default: return state } }
function todos(state = [], action) { switch (action.type) { case ‘ADD_TODO’: return [ …state, { text: action.text, completed: false } ] case ‘COMPLETE_TODO’: return state.map((todo, index) => { if (index === action.index) { return Object.assign({}, todo, { completed: true }) } return todo }) default: return state } }
import { combineReducers, createStore } from ‘redux’ let reducer = combineReducers({ visibilityFilter, todos }) let store = createStore(reducer) 就是这样,现在你应该明白 Redux 是怎么回事了。
先前技术
Redux 是一个混合产物。它和一些设计模式及技术相似,但也有不同之处。让我们来探索一下这些相似与不同。
Flux Redux 可以被看作 Flux 的一种实现吗? 是,也可以说 不是。
(别担心,它得到了Flux 作者的认可,如果你想确认。)
Redux 的灵感来源于 Flux 的几个重要特性。和 Flux 一样,Redux 规定,将模型的更新逻辑全部集中于一个特定的层(Flux 里的 store,Redux 里的 reducer)。Flux 和 Redux 都不允许程序直接修改数据,而是用一个叫作 “action” 的普通对象来对更改进行描述。
而不同于 Flux ,Redux 并没有 dispatcher 的概念。原因是它依赖纯函数来替代事件处理器。纯函数构建简单,也不需额外的实体来管理它们。你可以将这点看作这两个框架的差异或细节实现,取决于你怎么看 Flux。Flux 常常被表述为 (state, action) => state。从这个意义上说,Redux 无疑是 Flux 架构的实现,且得益于纯函数而更为简单。
和 Flux 的另一个重要区别,是 Redux 设想你永远不会变动你的数据。你可以很好地使用普通对象和数组来管理 state ,而不是在多个 reducer 里变动数据。正确且简便的方式是,你应该在 reducer 中返回一个新对象来更新 state, 同时配合 object spread 运算符提案 或一些库,如 Immutable。
虽然出于性能方面的考虑,写不纯的 reducer 来变动数据在技术上是可行的,但我们并不鼓励这么做。不纯的 reducer 会使一些开发特性,如时间旅行、记录/回放或热加载不可实现。此外,在大部分实际应用中,这种数据不可变动的特性并不会带来性能问题,就像 Om 所表现的,即使对象分配失败,仍可以防止昂贵的重渲染和重计算。而得益于 reducer 的纯度,应用内的变化更是一目了然。
Elm Elm 是一种函数式编程语言,由 Evan Czaplicki 受 Haskell 语言的启发开发。它执行一种 “model view update” 的架构 ,更新遵循 (state, action) => state 的规则。 Elm 的 “updater” 与 Redux 里的 reducer 服务于相同的目的。
不同于 Redux,Elm 是一门语言,因此它在执行纯度,静态类型,不可变动性,action 和模式匹配等方面更具优势。即使你不打算使用 Elm,也可以读一读 Elm 的架构,尝试一把。基于此,有一个有趣的使用 JavaScript 库实现类似想法 的项目。Redux 应该能从中获得更多的启发! 为了更接近 Elm 的静态类型,Redux 可以使用一个类似 Flow 的渐进类型解决方案 。
Immutable Immutable 是一个可实现持久数据结构的 JavaScript 库。它性能很好,并且命名符合 JavaScript API 的语言习惯 。
Immutable 及类似的库都可以与 Redux 对接良好。尽可随意捆绑使用!
Redux 并不在意你如何存储 state,state 可以是普通对象,不可变对象,或者其它类型。 为了从 server 端写同构应用或融合它们的 state ,你可能要用到序列化或反序列化的机制。但除此以外,你可以使用任何数据存储的库,只要它支持数据的不可变动性。举例说明,对于 Redux state ,Backbone 并无意义,因为 Backbone model 是可变的。
注意,即便你使用支持 cursor 的不可变库,也不应在 Redux 的应用中使用。整个 state tree 应被视为只读,并需通过 Redux 来更新 state 和订阅更新。因此,通过 cursor 来改写,对 Redux 来说没有意义。而如果只是想用 cursor 把 state tree 从 UI tree 解耦并逐步细化 cursor,应使用 selector 来替代。 Selector 是可组合的 getter 函数组。具体可参考 reselect,这是一个优秀、简洁的可组合 selector 的实现。
Baobab Baobab 是另一个流行的库,实现了数据不可变特性的 API,用以更新纯 JavaScript 对象。你当然可以在 Redux 中使用它,但两者一起使用并没有什么优势。
Baobab 所提供的大部分功能都与使用 cursors 更新数据相关,而 Redux 更新数据的唯一方法是分发一个 action 。可见,两者用不同方法,解决的却是同样的问题,相互并无增益。
不同于 Immutable ,Baobab 在引擎下还不能实现任何特别有效的数据结构,同时使用 Baobab 和 Redux 并无裨益。这种情形下,使用普通对象会更简便。
Rx Reactive Extensions (和它们正在进行的 现代化重写) 是管理复杂异步应用非常优秀的方案。以外,还有致力于构建将人机交互作模拟为相互依赖的可观测变量的库。
同时使用它和 Redux 有意义么?当然!它们配合得很好。将 Redux store 视作可观察变量非常简便,例如:
function toObservable(store) { return { subscribe({ next }) { const unsubscribe = store.subscribe(() => next(store.getState())) next(store.getState()) return { unsubscribe } } } } 使用类似方法,你可以组合不同的异步流,将其转化为 action ,再提交到 store.dispatch() 。
问题在于: 在已经使用了 Rx 的情况下,你真的需要 Redux 吗? 不一定。通过 Rx 重新实现 Redux 并不难。有人说仅需使用一两句的 .scan() 方法即可。这种做法说不定不错!
如果你仍有疑虑,可以去查看 Redux 的源代码 (并不多) 以及生态系统 (例如开发者工具)。如果你无意于此,仍坚持使用交互数据流,可以去探索一下 Cycle 这样的库,或把它合并到 Redux 中。记得告诉我们它运作得如何!
生态系统
Redux 是一个体小精悍的库,但它相关的内容和 API 都是精挑细选的,足以衍生出丰富的工具集和可扩展的生态系统。
如果需要关于 Redux 所有内容的列表,推荐移步至 Awesome Redux。它包含了示例、样板代码、中间件、工具库,还有很多其它相关内容。要想学习 React 和 Redux ,React/Redux Links 包含了教程和不少有用的资源,Redux Ecosystem Links 则列出了 许多 Redux 相关的库及插件。
本页将只列出由 Redux 维护者审查过的一部分内容。不要因此打消尝试其它工具的信心!整个生态发展得太快,我们没有足够的时间去关注所有内容。建议只把这些当作“内部推荐”,如果你使用 Redux 创建了很酷的内容,不要犹豫,马上发个 PR 吧。
学习 Redux 演示 开始学习 Redux — 向作者学习 Redux 基础知识(30 个免费的教学视频) 学习 Redux — 搭建一个简单的图片应用,简要使用了 Redux、React Router 和 React.js 的核心思想 示例应用 官方示例 — 一些官方示例,涵盖了多种 Redux 技术 SoundRedux — 用 Redux 构建的 SoundCloud 客户端 grafgiti — 在你的 Github 的 Contributor 页上创建 graffiti React-lego — 如何像积木一样,一块块地扩展你的 Redux 技术栈 教程与文章 Redux 教程 Redux Egghead 课程笔记 使用 React Native 进行数据整合 What the Flux?! Let’s Redux. Leveling Up with React: Redux A cartoon intro to Redux Understanding Redux Handcrafting an Isomorphic Redux Application (With Love) Full-Stack Redux Tutorial Getting Started with React, Redux, and Immutable Secure Your React and Redux App with JWT Authentication Understanding Redux Middleware Angular 2 — Introduction to Redux Apollo Client: GraphQL with React and Redux Using redux-saga To Simplify Your Growing React Native Codebase Build an Image Gallery Using Redux Saga Working with VK API (in Russian) 演讲
react-redux
安装
cnpm install react-redux —save
仓库的引用
import React from "react";
import ReactDOM from "react-dom";
import { Provider } from "react-redux";
import store from "store"
import App from "view";
ReactDOM.render(
<Provider store={store}>
<App/>
</Provider>,
document.getElementById("#app")
)
页面注入数据
写法一:
import React from "react";
import {connect} from "react-redex";
class App extends React.Component{
render(){
const { datalist } = [];
return <div>
<Children datalist={datalist}></Children>
</div>
}
}
const mapStateToProps = (state)=>{
return state.reducer;
}
const mapDispatchToProps = (dispatch)=>{
return {
update(){
dispatch({type:"",payload})
}
}
}
export default connect(mapStateToProps,mapDispatchToProps)(App);
写法二:
通过 es7 Decorator 装饰器完成
import React from "react";
import {connect} from "react-redex";
const mapStateToProps = (state)=>{
return state.reducer;
}
const mapDispatchToProps = (dispatch)=>{
return {
update(){
dispatch({type:"",payload})
}
}
}
@connext(mapStateToProps,mapDispatchToProps)
class App extends React.Component{
render(){
const { datalist } = [];
return <div>
<Children datalist={datalist}></Children>
</div>
}
}
export default App;
装饰器
decorator(装饰器)是ES7里面的一个语法糖,作用于类、类属性\方法,为它们提供一个实现与业务逻辑无关的功能的接口。
安装
//babel 6.0
npm install babel-plugin-transform-decorator —save
//babel 7.0
npm install @babel/plugin-proposal-decorators
配置
.babelrc
{
"plugins": ["@babel/plugin-proposal-decorators"]
}
wepy-redux
安装
cnpm install redux redux-actions redux-promise wepy-redux —save
目录结构
创建 store
import { createStore, applyMiddleware } from "redux";
import Reducers from "./reducer";
import promiseMiddleware from "redux-promise";
//写法一:
export default function configStore() {
return createStore(Reducers, applyMiddleware(promiseMiddleware))
}
//写法二
export default createStore(Reducers, applyMiddleware(promiseMiddleware))
创建 reducer
rank.js
const defaultState = {
rankList: [1, 2, 3]
}
const rankReducer = (state = defaultState, action) => {
const { type, payload } = action;
switch (type) {
case "UPDATE":
return { ...state, rankList: payload }
default:
return state;
}
}
export default rankReducer;
index.js
import { combineReducers } from "redux";
import rankReducer from "./rank";
import topListReducer from "./toplist";
import searchReducer from "./search";
const Reducers = combineReducers({
rankReducer,
topListReducer,
searchReducer,
...
})
export default Reducers;
创建 action
import { RNAK_UPDATE } from "../type/rank";
import { createAction } from "redux-actions";
import axios from "@/utils/request";
//方式一:
export function update(payload) {
return {
type: RNAK_UPDATE,
payload
}
}
//方式二:
const getJson = async function(url) {
let result = await axios.get(url);
return result.data.data.slider;
}
export const update = createAction(RNAK_UPDATE, () => {
const url = "https://c.y.qq.com/musichall/fcgi-bin/fcg_yqqhomepagerecommend.fcg";
return getJson(url);
})
创建 type
export const RNAK_UPDATE = "UPDATE";
export const TOPLIST_UPDATE = "UPDATE";
绑定及监听
在app.wepy文件中,添加下面代码:
import { setStore } from ‘wepy-redux’
import store from ‘./store’
setStore(store)
setStore()是用来将仓库中的数据绑定到页面中
类似react-redux中的 <Provider store={store}></Provider>组件
Store
职责:
- action 描述“发生了什么”的一个动作,通过dispatch 来执行这个动作。
- reducers 来根据 action 更新 state 的用法。
- store 就是把它们联系到一起的对象
什么时候使用
``` -
基础
提供 createStore() 方法创建一个仓库;
- 提供 getState() 方法获取 state;
- 提供 dispatch(action) 方法更新 state;
- 通过 subscribe(listener) 注册监听器;
- 通过 unsubscribe(listener) 返回的函数注销监听器。
创建
当需要拆分数据处理逻辑时,你应该使用 reducer 组合 而不是创建多个 store。 根据已有的 reducer 来创建 store 是非常容易的。 使用 combineReducers() 将多个 reducer 合并成为一个。
现在我们将其导入,并传递 createStore()。
//reducers
import { combineReducers } from 'redux';
import ...Reducer from somepath;
import ...Reducer from somepath;
const Reducers = combineReducers({
...Reducer,
...Reducer,
...
})
import { createStore } from 'redux'
import Reducers from './reducers'
let store = createStore(Reducers)
配置初始化数据
createStore() 的第二个参数是可选的, 用于设置 state初始状态。这对开发应用时非常有用,服务器端 redux 应用的 state 结构可以与客户端保持一致, 那么客户端可以将从网络接收到的服务端 state 直接用于本地数据初始化。
let store = createStore(reducer, window.STATE_FROM_SERVER)
添加监听
当仓库状态改变时,页面数据也需要随着改变,通过subscribe监听数据变化,并绑定到视图
import store from "store";
import React from "react";
import ReactDOM from "react-dom";
const template = <div></div>;
const element = document.getElementById("#app");
const render = ()=>{
ReactDOM.render(template,element)
}
render()
//只能监听一个函数,所以对render方法改造
store.subcribe(render)
移除监听
创建监听后,store.subscribe() 将返回一个函数作为返回值,调用这个函数,即可移除对视图监听
const unsubscribe = store.subscribe(listener);
unsubscribe()
Action
Action 是把数据从应用传到 store 的有效载荷。 它是 store 数据的唯一来源。 一般来说你会通过 store.dispatch() 将 action 传到 store。
const ADD_TODO = 'ADD_TODO'
{
type: ADD_TODO,
text: 'Build my first Redux app'
}
Action 本质上是 JavaScript 普通对象。我们约定,action 内必须使用一个字符串类型的 type 字段来表示将要执行的动作。多数情况下,type 会被定义成字符串常量。当应用规模越来越大时,建议使用单独的模块或文件来存放 action。
import { PLUS, MINUS } from './store/action'
除了 type 字段外,action 对象的结构完全由你自己决定。参照 Flux 标准 Action 获取关于如何构造 action 的建议。
这时,我们还需要再添加一个 action index 来表示用户完成任务的动作序列号。因为数据是存放在数组中的,所以我们通过下标 index 来引用特定的任务。而实际项目中一般会在新建数据的时候生成唯一的 ID 作为数据的引用标识。
{
type: "PLUS",
index: 1
}
我们应该尽量减少在 action 中传递的数据。比如上面的例子,传递 index 就比把整个任务对象传过去要好。
最后,再添加一个 action type 来表示当前的任务展示选项。
{
type: SET_VISIBILITY_FILTER,
filter: SHOW_COMPLETED
}
发起Action
通过 dispatch()来发起一个Action, 接受一个对象,对象中有 type,payload
store.dispatch( { type:"PLUS",payload:[something]})
Reducer
指定了应用状态的变化如何响应 actions 并发送到 store 的 reducer 接收旧的 state 和 action,返回新的 state。
import {createStore} from "redux";
const defaultState = {
count:0
}
const reducer = (state = defaultState,action)=>{
const { type,payload } = action;
switch (type) {
case "PLUS":
//写法一:
return {...state, count: state.count+1}
case "MINUS":
//写法二:
return Object.assign({},state,{
count: state.count-1
})
default:
return state;
}
}
const store = createStore(reducer)
注意事项
永远不要在 reducer 里做这些操作:
- 修改传入参数
- 执行有副作用的操作,如 API 请求和路由跳转
调用非纯函数,如 Date.now() 或 Math.random()
纯函数
为什么要使用纯函数?
当我们的程序变得庞大的时候, 将不可避免地引发一些bugs。我们不能保证杜绝bug产生, 但是我们可以通过某些编程方式来减少一些错误的发生。
纯函数就是其中一种,它也是函数式编程中一部分。那它为什么可以起到减少bug的作用呢, 原因就在于能被称之为纯函数而制定的一些原则,我们来简单看下
3个原则:变量都只在函数作用域内获取, 作为的函数的参数传入
- 不会产生副作用(side effects), 不会改变被传入的数据或者其他数据
相同的输入 一定 保证相同的输出(same input -> same ouput)
纯函数的一些优点
容易测试(testable)
- 因为相同的输入必定是相同的输出,因此结果可以缓存(cacheable)
- 自我记录(Self documenting),因为需要的变量都是参数,参数命名良好的情况下即便很久以后再去看这个函数依旧可以很容易知道这个函数需要哪些参数
- 因为不用担心有副作用(side-effects),因此可以更好地工作
合并 reducer
需求
当我们的业务逻辑足够复杂的时候,一个全局的state 已经不能很好维持每个页面中的共享数据,迫切的需要进行作用域隔离 combineReducers 函数应用而生import { combineReducers } from 'redux';
const reducer1 = ()=>{};
const reducer2 = ()=>{};
const Reducers = combineReducers({
reducer1:reducer1,
reducer2,
...
})