前端技术 | 从Flux到Redux

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

内容简介:上一篇分析了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》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

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

JavaScript开发王

JavaScript开发王

张亚飞 / 2008-6 / 65.00元

《网站开发专家·JavaScript开发王》详细介绍使用JavaScript脚本开发应用程序,并融合在网页中实现动态网页,内容也包含DHTML和AJAX。这《网站开发专家·JavaScript开发王》有严格的逻辑性,不论是JavaScript语言、DHTML,还是AJAX,《网站开发专家·JavaScript开发王》首先帮助用户建立首要的、基础的正确认知,然后通过实例,说明如何利用JavaScri......一起来看看 《JavaScript开发王》 这本书的介绍吧!

URL 编码/解码
URL 编码/解码

URL 编码/解码

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具