前端技术 | 从Flux到Redux

栏目: 服务器 · 发布时间: 5年前

内容简介:上一篇分析了Flux出现的背景和原理,最核心的思想就是“组件化+单向数据流”。但是,Flux在设计上并非完美,具体来说主要存在以下2个不足:由于Flux采用多Store设计,各个Store之间可能存在数据依赖。以flux-chat为例:在这个聊天软件里,可能会有多个人给你发消息,比如Dave给你发了3条,Brian给你发了2条,当你点开某个人给你发的消息后,界面需要刷新,显示你目前还有几个人的未读消息没有查看:

上一篇分析了Flux出现的背景和原理,最核心的思想就是“组件化+单向数据流”。

但是,Flux在设计上并非完美,具体来说主要存在以下2个不足:

1. 多Store数据依赖

由于Flux采用多Store设计,各个Store之间可能存在数据依赖。以flux-chat为例:在这个聊天软件里,可能会有多个人给你发消息,比如Dave给你发了3条,Brian给你发了2条,当你点开某个人给你发的消息后,界面需要刷新,显示你目前还有几个人的未读消息没有查看:

前端技术 | 从Flux到Redux

为了解决这个需求,创建了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

和前一篇的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的完整流程应参见下面这张动图:

前端技术 | 从Flux到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机制,说白了就是所有页面都能访问的一个上下文对象:

前端技术 | 从Flux到Redux

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(高阶组件)技术,动态创建新组件及其实例:

前端技术 | 从Flux到Redux

那么这个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。

前端技术 | 从Flux到Redux

以上所述就是小编给大家介绍的《前端技术 | 从Flux到Redux》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Python for Everyone

Python for Everyone

Cay S. Horstmann、Rance D. Necaise / John Wiley & Sons / 2013-4-26 / GBP 181.99

Cay Horstmann's" Python for Everyone "provides readers with step-by-step guidance, a feature that is immensely helpful for building confidence and providing an outline for the task at hand. "Problem S......一起来看看 《Python for Everyone》 这本书的介绍吧!

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具

在线进制转换器
在线进制转换器

各进制数互转换器

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试