前端技术栈:从Flux到Redux
Posted TurkeyCock
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了前端技术栈:从Flux到Redux相关的知识,希望对你有一定的参考价值。
上一篇分析了Flux出现的背景和原理,最核心的思想就是“组件化+单向数据流”。
但是,Flux在设计上并非完美,具体来说主要存在以下2个不足:
1. 多Store数据依赖
由于Flux采用多Store设计,各个Store之间可能存在数据依赖。以flux-chat为例:在这个聊天软件里,可能会有多个人给你发消息,比如Dave给你发了3条,Brian给你发了2条,当你点开某个人给你发的消息后,界面需要刷新,显示你目前还有几个人的未读消息没有查看:
为了解决这个需求,创建了3个Store:
- ThreadStore用来存储消息组状态
- MessageStore用来存储每个组里的消息的状态
- UnreadThreadStore用来计算目前还有几个消息组没有查看
当你点开某个消息组时,显然你需要先更新ThreadStore和MessageStore,然后再更新UnreadThreadStore。由于Store的注册顺序是不确定的,为了应付这种依赖,Flux提供了waitFor()机制,每个Store在注册之后都会生成一个令牌(dispatchToken),通过等待令牌的方式确保其他Store被优先更新。
因此UnreadThreadStore的代码会写成下面这个样子:
Dispatcher.waitFor([
ThreadStore.dispatchToken,
MessageStore.dispatchToken
]);
switch (action.type)
case ActionTypes.CLICK_THREAD:
UnreadThreadStore.emitChange();
break;
...
虽然可以工作,但是总觉得不是很优雅,在一个Store中需要显示地包含其他Store的调用。当然你会说,干脆把这3个Store的代码糅到一起,搞成一个Store不就行了?但是这样又会导致代码结构不够清晰,不利于多模块分工协作。
为了兼顾这两个方面,Redux使用全局唯一Store,外部可以使用多个reducer来修改Store的不同部分,最后会把所有reducer的修改再组合成一个新的Store状态。
2.状态修改不是纯函数
所谓纯函数,是指输出只和输入相关,相同的输入一定会得到相同的输出。用专业一点的术语来说,纯函数没有“副作用”。我们先来看看Flux中是怎么修改状态的:
Dispatcher.register(action =>
switch(action.type)
case ActionTypes.CLICK_THREAD:
_currentID = action.threadID;
ThreadStore.emitChange();
break;
...
可以看到,是直接修改变量值,然后显式发送一个change事件来通知View。
我们再来看看Redux中是怎么修改状态的:
export default function threadReducer(state = , action)
switch (action.type)
case ActionTypes.CLICK_THREAD:
return ...state, _currentID: action.threadID ;
...
细心的人可能已经看出来了,主要有3点区别:
- 前面的函数里只有一个action参数,而这里多了一个state参数
- 不是直接修改state中的字段,而是需要返回一个新的state对象
- 不需要显式发送事件通知View,实际上,Redux内部会检测state对象的引用是否发生了变化,然后自动通知View进行刷新
那么有人会说了,为啥要这么做,好像也没看到啥好处嘛?当然是有好处的,这样可以支持“时间旅行调试(Time Travel Debugging)”。所谓时间旅行调试,指的是可以支持状态的无限undo / redo。由于state对象是被整体替换的,如果想回到上一个状态重新执行,那么直接替换成上一步的state对象就可以了。
3.什么是Redux?
首先我们要搞清楚,Redux解决了哪些问题?主要是以下3点:
1.如何在应用程序的整个生命周期内维持所有数据?
Redux是一个“状态容器”。写过React或者ReactNative的同学可能会有感受,如果多个页面需要共享数据时,需要把数据一层层地传递下去,非常繁琐。如果能有一个全局统一的地方存储数据,当数据发生变化时自动通知View刷新界面,是不是很美好呢?因此,我们需要一个“状态容器”。
2.如何修改这些数据?
Redux借鉴了分布式计算中的map-reduce的思想,把Store中的数据分割(map)成多个小的对象,通过纯函数修改这些对象,最后再把所有的修改合并(reduce)成一个大的对象。修改数据的纯函数被称为reducer。
3.如何把数据变更传播到整个应用程序?
通过订阅(subscribe)。如果你的View需要跟随数据的变化动态刷新,可以调用subscribe()注册回调函数。在这一点上,Redux是非常粗粒度的,每次只要有新的action被分发,你都会收到通知。显然,你需要对通知进行过滤,这意味着你可能会写很多重复代码。不过,这也是出于通用性和灵活性考虑,实际上Redux不仅可以用于React,也可以用在Vue.js或者Angular上。可以搭配特定框架相关的适配层比如react-redux来规避这些重复代码。
说了这么多,我们来看一下Redux的基本框架:
和前一篇的Flux框架图对比一下可以发现,Redux去除了dispatcher组件(因为只有一个Store),增加了recuder组件(用于更新Store的不同部分)。下面详细介绍各个部分的作用。
4.Redux基本概念
4.1 Store
首先我们需要创建一个全局唯一的Store,Redux提供了辅助函数createStore():
import createStore from 'redux'
var store = createStore(() => )
你可能注意到了,createStore()需要提供一个参数,这个参数就是reducer。
4.2 Reducer
前面介绍过,reducer就是一个纯函数,输入参数是state和action,输出新的state。一般的代码模板如下:
var reducer = (state = , action) =>
switch (action.type)
case 'MY_ACTION':
return ...state, message: action.message
default:
return state
需要注意的是,default分支一定要返回state,否则会导致状态丢失。
好了,现在我们有了reducer,可以作为参数传递给4.1节中的createStore()函数了。
createStore()只能接受一个reducer参数,如果我们有多个reducer怎么办?这时需要使用另一个辅助函数combineReducers():
import combineReducers from 'redux'
var reducer = combineReducers(
first: firstReducer,
second: secondReducer
)
combineReducers()会把多个reducer组合成一个,当有action过来时会依次调用每个子reducer,所以实际上你可以组织成一个树状结构。
4.3 Action
所谓action,其实就是一个普通的javascript对象,一般会包含一个type属性用于标识类型,以及一个payload属性用于传递参数(名字可以随便取):
var action =
type: 'MY_ACTION',
payload: message: 'hello'
那么如何发送action呢?store提供了一个dispatch()函数:
store.dispatch(action)
4.4 Action Creator
所谓action creator,其实就是一个用来构建action对象的函数:
var actionCreator = (message) =>
return
type: 'MY_ACTION',
payload: message: message
所以4.3节发送action的代码也可以写成这样:
store.dispatch(actionCreator('hello'))
4.5 状态读取和订阅
当你发送了一个action,reducer被调用并完成状态修改,那么前端视是怎么感知到状态变化的呢?我们需要通过subscribe()进行订阅:
store.subscribe(() =>
let state = store.getState()
... ...
)
store的getState()函数可以获得当前状态的一个副本,然后就可以刷新界面了,以React为例,可以调用this.setState()或者this.forceUpdate()触发重新渲染。
当视图组件比较多时,每次都要写这段订阅代码会比较繁琐,后面会介绍通过react-redux来简化这一过程。
4.6 Middleware
第3章的那张图其实还少画了个东西,叫做middleware(中间件)。那么这个middleware是干什么用的呢?
在Web应用中经常会有异步调用,比如请求网络、查询数据库什么的。我们首先发送一个action启动异步任务,并希望在异步任务完成以后再更新状态,应该如何实现呢?在Flux中,我们可以在dispatcher里完成:首先启动异步任务,然后在回调函数中再发送一个新的action去更新Store。但是Redux中去除了dispatcher的概念,你能调用的只有store的dispatch()函数而已,那我们该怎么办呢?答案就是middleware。
所以,Redux的完整流程应参见下面这张动图:
我们先来看一个简单的middleware的例子:
var thunkMiddleware = ( dispatch, getState ) =>
return (next) =>
return (action) =>
return typeof action === 'function' ?
action(dispatch, getState) :
next(action)
可以发现,其实middleware就是一个三层嵌套的函数:
- 第一层向其余两层提供dispatch和 getState 函数
- 第二层提供 next 函数,它允许你显式的将处理过的输入传递给下一个middleware或 reducer
- 第三层提供从上一个中间件或从 dispatch 传递来的 action
所以,实际上middleware可以理解在action进入reducer之前进行了一次拦截。在这个例子里,如果action是一个函数,我们就不会把action继续传递下去,而是调用这个函数去执行异步任务。当异步任务执行完毕后,我们可以调用dispatch()函数发送一个新的action,用于调用reducer更新状态。
那么我们如何注册一个中间件呢?Redux提供了一个工具函数applyMiddleware(),可以直接作为createStore()的一个参数传递进去:
const store = createStore(
reducer,
applyMiddleware(myMiddleware1, myMiddleware2)
)
预告一下,后面一篇要介绍的redux-saga,其实就是一个Redux中间件。
5.使用react-redux
Redux的设计主要考虑的是通用性和灵活性,如果想更好的配合React的组件化编程习惯,你可能需要react-redux。
Redux使用全局唯一的Store,另外当你需要发送action的时候,必须通过store的dispatch()函数。这对于一个有很多页面的React应用来说,意味着只有两种选择:
- 在所有页面中import全局store对象
- 通过props把store对象一层一层地传递下去
这显然极其繁琐,幸运的是,React提供了Context机制,说白了就是所有页面都能访问的一个上下文对象:
react-redux利用React的Context机制进行了封装,提供了<Provider>组件和connect()函数来实现store对象的全局可访问性。
5.1 <Provider>
这是一个React组件,使用时需要把它包裹在应用层根组件的外面,然后把全局store对象赋值给它的store属性:
import Provider from 'react-redux'
import store from './mystore'
export default class Application extends React.Component
render ()
return (
<Provider store= store >
<Home />
</Provider>
)
5.2 connect()
Provider组件只是把store对象放进了Context中,如果你需要访问它,还需要一些额外的代码,react-redux提供了一个connect()函数来帮你完成这些工作。
实际上,connect()就帮你做了两件事:
- 在你的组件外面包装了<Context.Consumer>组件,获取Context中的store对象
- 根据你提供的selector函数,帮你把state中的值以及store.dispatch()函数映射到props中,这样在代码中你就可以直接通过this.props.xxx进行访问了
实现层面上,connect()采用了React的HOC(高阶组件)技术,动态创建新组件及其实例:
那么这个connect()怎么用呢?我们通过3个应用场景依次介绍。
1.你只是希望能在组件中使用dispatch()直接派发action
这是最简单的情况,你只需要在导出组件的时候加上connect()就可以了:
export default connect()(MyComponent)
当你需要派发action的时候,可以直接调用this.props.dispatch()。
2.你不想直接使用dispatch(),希望能够自动派发action
实际上你会发现,如果action很多的话,你需要不停地调用dispatch()函数。为了使我们的实现更加“声明式”,最好是把派发逻辑封装起来。实际上Redux中有一个辅助函数bindActionCreators()来完成这项工作,它会为每个action creator生成同名的函数,自动调用dispatch()函数:
const increment = () => ( type: "INCREMENT" );
const decrement = () => ( type: "DECREMENT" );
const boundActionCreators = bindActionCreators( increment, decrement , dispatch);
// 返回值:
//
// increment: (...args) => dispatch(increment(...args)),
// decrement: (...args) => dispatch(decrement(...args)),
//
这样你就可以直接调用boundActionCreators.increment()派发action了。那么如何跟connect()联系起来呢?这里需要用到它的第2个参数(第1个参数后面再介绍)mapDispatchToProps,举个例子:
const mapDispatchToProps = (dispatch) =>
return bindActionCreators( increment, decrement , dispatch);
export default connect(null, mapDispatchToProps)(MyComponent)
这样,你就可以在组件中直接调用this.props.increment()函数了。
你以为这样就结束了?还有更简单的方法,连bindActionCreators()都不用写!你可以直接提供一个对象,包含所有的action creator就行了(这被称为“对象简写”方式):
const mapDispatchToProps = increment, decrement
export default connect(null, mapDispatchToProps)(MyComponent)
注意:如果你提供了mapDispatchToProps参数,那么默认情况下dispatch就不会再注入到props中了。如果你还想使用this.props.dispatch(),可以在mapDispatchToProps的返回值对象中加上dispatch属性。
3.你希望访问store中的数据
这应该是使用最多的场景,组件访问store中的数据并刷新界面。根据“无状态组件”设计原则,我们不应该直接访问store,而需要通过一个“selector函数”把store中的数据映射的props中进行访问,这个“selector函数”就是conntect()的第1个参数mapStateToProps。举个例子:
const mapStateToProps = (state = , ownProps) =>
return
xxx: state.xxx
export default connect(mapStateToProps)(MyComponent)
这样你在组件中就可以通过this.props.xxx进行访问了。另外,它还会帮你自动订阅store,任何时候store状态数据发生变化,mapStateToProps都会被调用并导致界面重新渲染。除了第一个参数state之外,还有一个可选参数ownProps,如果你的组件需要用自身的props数据到store中检索数据,可以通过这个参数获取。
当然,你可以同时提供mapStateToProps和mapDispatchToProps参数,这样你就可以获得两方面的功能:
export default connect(mapStateToProps, mapDispatchToProps)(MyComponent)
最后,以一张思维导图结束本篇文章,下一篇介绍redux-saga。
更多文章欢迎关注“鑫鑫点灯”专栏:https://blog.csdn.net/turkeycock
或关注飞久微信公众号:
以上是关于前端技术栈:从Flux到Redux的主要内容,如果未能解决你的问题,请参考以下文章