React 升级:Redux
前言近期接触React项目,学到许多新知识点,网上教程甚多,但大多都把知识点分开来讲,初学者容易陷入学习的误区,摸不着头脑,本人在学习中也遇到许多坑。此篇文章是笔者看过的写得比较详细的具体的,同时能把所有的知识点统一串联起来,非常适合初学者学习。由于文档是英文版,考虑到大伙英语水平各不相同,故做此次翻译,一来深化自己对Redux的体系认知,二来方便大家理解阅读。 由于文中出现大量技术名词,应适当结合原文进行阅读,原文连接: 此篇教程是原文作者一系列教程的最后一篇,这里只对该篇进行翻译,剩余的几篇有时间会继续进行翻译,对于文中出现的翻译错误,欢迎大家积极指正。
系列文章
Redux 是一个用来管理JavaScript应用中 data-state(数据状态)和UI-state(UI状态)的工具,对于那些随着时间推移状态管理变得越来越复杂的单页面应用(SPAs)它是比较理想的,同时,它又是和框架无关的,因此,尽管它是提供给React使用的,但它也可以结合Angular 或者 jQuery来使用。 另外,它的设想来自一个叫做“时间旅行”的实验,这是真实的,我们后面会讲到。 正如我们前面的教程所提到的,React 在组件之间流通数据.更确切的说,这被叫做“单向数据流”——数据沿着一个方向从父组件流到子组件。由于这个特性,对于没有父子关系的两个组件之间的数据交流就变得不是那么显而易见。
React 不推荐组件对组件直接交流的这种方式,尽管它确实有一些特征可以支持这个方法,但在许多组件之间进行直接的组件对组件的交流被认为是不好的做法,因为这样会容易出错,并且导致spaghetti code —— 过时的代码,很难维护。 React 提供了一个建议,但是他们希望你能自己来实现它。这里是React官方文档里的一段话:
这里 Redux 就排上用场了。Redux提供了一个解决方案,通过将应用程序所有的状态都存储在一个地方,叫做“store”。然后组件就可以“dispatch”状态的改变给这个store,而不是直接跟另外的组件交流。所有的组件都应该意识到状态的改变可以“subscribe”给store。
可以把store想象成是应用程序中所有状态改变的中介。随着Redux的介入,所有的组件不再相互直接交流,而是所有的状态改变必须通过store这个单一的真实来源。 这和那些应用程序中不同的部分直接交流的策略有很大的不同。有时,那些策略被认为是容易出错和混乱的原因: 有了Redux,所有的组件都从store中来获取他们的状态,变得非常清晰。同样,组件状态的改变都发送给了store,也很清晰。组件初始化状态的改变只需要关心如何派发给store,而不用去关心一系列其它的组件是否需要状态的改变。这就是Redux如何使数据流变得更简单的原因。 使用store来协调应用之间状态改变的概念就是Flux模式。它是一种倾向单向数据流(比如 React)的设计模式。Redux像Flux,但是他们又有多少关系呢? Redux is "Flux-like"Flux 是一种模式,不像Redux那样是可以下载的工具,Redux 是受Flux模式,此外,它比较像Elm。这里有许多有关于Redux和Flux之间比较的指南。它们中的大多数都会得出Redux就是Flux,或者Redux和Flux比较类似的结论,这取决于给Flux定义的规则到底有多严格。然而说到底,这些都无关紧要。Facebook 非常喜欢并且支持Redux,这从它们雇佣了Redux的主要开发者 Dan Abramov 就可以看出。 这篇文章假设你一点都不熟悉Flux的设计模式。不过如果你熟悉,你会注意到许多微小的不同,尤其考虑到Redux的三大指导原则: 1. 单一真实源Redux只使用一个store来处理应用的状态。因为所有的状态都驻留在同一个地方,Redux称这个为单一真实源。 store中数据的结构完全取决于你,但通常都是针对应用的一个深层嵌套的对象。 Redux的单一store方法是区分Flux多个store方法的最主要区别。 2. 状态是只读的Redux的文档指出,唯一改变状态的方法就是发出一个action,一个用来描述发生了什么的对象。 这意味着应用不能直接改变状态,相反,“actions” 被派发给store,用来描述一个改变状态的意图。 store对象自己有几个小型的API,对应4个方法:
所以你可以看到,这里没有设置状态的方法。因此,派发一个action是处理应用状态更改的唯一办法。 var action = { type: 'ADD_USER',user: {name: 'Dan'} }; // Assuming a store object has been created already store.dispatch(action);
3. 所有的状态改变使用的都是纯函数就像刚才所描述的,Redux不允许应用直接改变状态,而是用被分派的action来“描述”状态改变或者改变状态的意图。而一个个Reducer就是你自己写的函数,用来处理分派的action,事实上是它真正改变了状态。 一个reducer接受当前的状态(state)作为参数,而且必须返回一个新的状态才能改变之前的状态。 // Reducer Function var someReducer = function(state,action) { ... return state; } reducer 必须使用 “纯”函数 , 一个可以用以下这些特征来描述的术语:
它们被称为“纯”函数是因为它们什么都不做仅仅返回一个基于参数的值。它们在系统的任何其他部分都没有副作用。 第一个 Redux Store开始之前,需要先用 // Note that using .push() in this way isn't the // best approach. It's just the easiest to show // for this example. We'll explain why in the next section. // The Reducer Function var userReducer = function(state,action) { if (state === undefined) { state = []; } if (action.type === 'ADD_USER') { state.push(action.user); } return state; } // Create a store by passing in the reducer var store = Redux.createStore(userReducer); // Dispatch our first action to express an intent to change the state store.dispatch({ type: 'ADD_USER',user: {name: 'Dan'} }); 上面的程序干了些什么呢:
*在这个例子里reducer实际上被调用了两次 —— 一次是在创建store的时候,一次是在分派action之后。 当store被创建之后,Redux立即调用了所有的reducer,并且将它们的返回值作为初始状态。第一次调用reducer传递了一个 所有的reducer在每次action被分派之后都会被调用。因为reducer返回的状态将会成为新的状态存储在store中,所以 Redux总是希望所有的reducer都要返回一个状态。 在这个例子中,reducer第二次的调用发生在分派之后。记住,一个被分派的action描述了一个改变状态的意图,而且通常携带有数据用来更新状态。这一次,Redux将当前的状态(仍旧是空数组)和action对象一起传递给了reducer。这个action对象,现在有了一个值为 我们很容易就能将reducers和漏斗联想起来,允许状态通过他们。这是因为reducers总是接受和返回状态用来更新store。
基于这个例子,我们的store将会变成一个只有一个user对象的数组: store.getState(); // => [{name: 'Dan'}] 不要改变状态,复制它在我们上面的例子中这个reducer从技术上来讲是可行的,但是它改变了状态,这是一种不好的做法。尽管reducers 负责改变状态,但是不应该直接改变“现有的状态”。所以我们不应该在reducer的state这个参数上使用 传递给reducer的参数应该被视为不可改变的。换句话说,他们不应该被直接改变。我们可以使用不变异的方法比如 var userReducer = function(state = [],action) { if (action.type === 'ADD_USER') { var newState = state.concat([action.user]); return newState; } return state; } 在这个新的reducer中,我们添加了一个新的user对象作为state参数的副本被改变和返回。当没有添加新的用户的时候,注意返回的是原始的state而不是它的拷贝。 有一大节关于不可变数据结构的最佳尝试,我们应该更多的去了解
多个reducer上一个例子是一个很好的入门,但是大多数的应用都需要更复杂的state来满足整个应用。因为Redux仅使用一个store,所以我们需要使用嵌套的对象来组织不同模块的state。假设我们的想要我们的store类似于这种样子: { userState: { ... },widgetState: { ... } } 整个应用对应的还是 “一个store = 一个对象”,但是它嵌套了 为了创建具有嵌套对象的store,我们需要定义每一块的reducer: import { createStore,combineReducers } from 'redux'; // The User Reducer const userReducer = function(state = {},action) { return state; } // The Widget Reducer const widgetReducer = function(state = {},action) { return state; } // Combine Reducers const reducers = combineReducers({ userState: userReducer,widgetState: widgetReducer }); const store = createStore(reducers);
有些非常重要的点需要注意,现在每一个reducer中所传递的只是全部状态中各自的部分,不再像之前只有一个reducer时传递的是整个store的状态。然后每个reducer返回的状态应用于它们各自的部分。 在分派之后调用的是哪一个Reducer?当我们考虑每次action被分派的时候,把上面全部的reducer想想成一个个漏斗会变得更加明了,所有的reducer都会被调用,都将有机会来更新各自的状态: 我很小心地说“它们的”状态是因为reducer的“当前状态”参数和它的返回“更新”状态仅仅影响到store中reducer里面的部分。记住,像前面所说的,每一个reducer只获得它们各自的状态,而不是整个状态。 Action 策略实际上有大量的关于创建和管理action及其类型的策略。虽然它们都很棒,但是它们不像本文中的其他一些信息那样重要。为了减少文章的篇幅,我们整理了这些基本的action策略,你可以在 GitHub repo上获得这一系列的策略。 不可变的数据结构
上面的陈诉说了很多,我们已经在本教程中提到了这一点。如果我们开始讨论什么是可变的什么是不可变的的来龙去脉和利弊,我们可以在 《blog article's worth of information》找到更有价值的信息。所以事实上,我只是想突出一些要点。 开始前:
有人说数据结构的可变性容易产生问题。因为我们的store是有state对象和数组所组成,我们需要实施一种策略来保持状态不可变。 让我们假设需要改变一个 // Example One state.foo = '123'; // Example Two Object.assign(state,{ foo: 123 }); // Example Three var newState = Object.assign({},state,{ foo: 123 }); 第一个和第二个例子都改变了state对象。第二个例子是因为 第三个例子将 对象的“扩展运算符”是保持state不可变的另一种方式: const newState = { ...state,foo: 123 }; 有关于上述代码究竟发生了什么,为什么它对Redux是友好的详细解释,可以参考这个主题的文档
总结来说,有许多方法可以明确地保持对象和数组不可变。许多开发者使用第三方库比如 seamless-immutable、Mori 甚至Facebook自己的Immutable.js 来达到这个目的。
初始化状态 和 时间旅行如果你读过文档,你也许会注意到 想象一下一个用户刷新了你的单页面应用,store中的状态被重置为reducer中的初始状态,这样可能是不理想的。 相反,想象一个你可以使用一种策略来保持store,然后在刷新的时候重新将它化合到Redux中。这就是传送一个初始化状态到 这带来了一个有趣的概念,如果重新化合老的状态变得这么容易,我们可以将app中的状态想象成是时间旅行。这可以被用来进行调试或者撤销/重做某些特性。所以将所有的状态存储在一个store中变得很有意义。这就是为什么不可变的状态能够帮助我们的其中一个原因。 在一次面谈中,Dan Abramov 被问到“为什么你要开发Redux?”
Redux with React就像我们已经讨论过的,Redux与框架无关。在我们开始考虑Redux跟React怎么结合之前,明白Redux的核心概念是非常重要的。但是现在我们已经准备好从上一篇文章中拿一个容器组件,然后将Redux应用在它上面了。 首先,这是没有使用Redux的原始组件代码: import React from 'react'; import axios from 'axios'; import UserList from '../views/list-user'; const UserListContainer = React.createClass({ getInitialState: function() { return { users: [] }; },componentDidMount: function() { axios.get('/path/to/user-api').then(response => { this.setState({users: response.data}); }); },render: function() { return <UserList users={this.state.users} />; } }); export default UserListContainer;
它所做的就是发送一个Ajax请求,然后更新它的本地状态。但是,如果该应用的其它区域也要根据这个新获取到的用户列表进行改变呢,这个策略是不够的。 有了Redux策略,我们可以在Ajax请求的时候分派一个action而不是进行 我想我可以提供几个例子来手动的连接一些组件到Redux store。你也可以想象一下用你的方法会怎么做。但是最终,在这些例子的最后我会解释有一个更好的办法,然后忘掉这些手动的例子。然后我会介绍官方的连接React和Redux的模块,叫做react-redux,所以还是直接跳到那一步吧。 使用 react-redux 进行连接为了说明白, 下面给出例子: import React from 'react'; import { connect } from 'react-redux'; import store from '../path/to/store'; import axios from 'axios'; import UserList from '../views/list-user'; const UserListContainer = React.createClass({ componentDidMount: function() { axios.get('/path/to/user-api').then(response => { store.dispatch({ type: 'USER_LIST_SUCCESS',users: response.data }); }); },render: function() { return <UserList users={this.props.users} />; } }); const mapStateToProps = function(store) { return { users: store.userState.users }; } export default connect(mapStateToProps)(UserListContainer); 这里面有许多的新东西: 1、我们从
3、 4、根据第3点中所说的,我们将不再需要 5、Ajax的返回现在变成了一个action的分派,而不是本地状态的更新。为了更简单明了的展示,我们没有使用action构造器和action type常量 下面的代码提供了一种在用户自定义的reducer没有出现的时候也可以工作的假设。注意store的 const mapStateToProps = function(store) { return { users: store.userState.users }; } 这个名字来自我们合并所有的reducer的时候: const reducers = combineReducers({ userState: userReducer,widgetState: widgetReducer });
在这个例子中,我们并没有展示一个实际的reducer(因为它会出现在另一个文件中),reducer决定了它所负责状态的子属性。为了确保 const initialUserState = { users: [] } const userReducer = function(state = initialUserState,action) { switch(action.type) { case 'USER_LIST_SUCCESS': return Object.assign({},{ users: action.users }); } return state; } 在 Ajax 不同生命周期进行分派在我们Ajax的例子中,我们仅仅分派了一个action。它被特意叫做“USER_LIST_SUCCESS”,因为我们同时也希望在Ajax调用开始的时候分派一个“USER_LIST_REQUEST”的action,在Ajax调用失败的时候分派一个“USER_LIST_FAILED”的action。请确保读取异步操作的文档 分派事件在之前的文章中,我们看到事件应该通过容器组件传递到表现组件。原来 ... const mapDispatchToProps = function(dispatch,ownProps) { return { toggleActive: function() { dispatch({ ... }); } } } export default connect( mapStateToProps,mapDispatchToProps )(UserListContainer); 在表现组件中,就像我们之前做过的,可以通过 容器组件省略有时,一个容器组件只需要订阅store,不需要任何像 import React from 'react'; import { connect } from 'react-redux'; import UserList from '../views/list-user'; const mapStateToProps = function(store) { return { users: store.userState.users }; } export default connect(mapStateToProps)(UserList); 是的,父老乡亲们,这就是新的容器组件的整个文件。但是等一下,容器组件在哪里?为什么我们在这里没有用到任何的 事实证明, 所以是不是意味着上面的例子中其实有两个容器组件包裹着一个表现组件?当然,你可以这样子认为。但这并没有什么问题,只有当我们的容器组件需要除了 想象这两个容器组件是具有不同但是相关服务的角色: 嗯,也许这就是为什么React的logo看起来这么像原子的原因吧 Provider为了保证任何 import React from 'react'; import ReactDOM from 'react-dom'; import { Provider } from 'react-redux'; import store from './store'; import router from './router'; ReactDOM.render( <Provider store={store}>{router}</Provider>,document.getElementById('root') ); 通过 Redux with React Router这个不做要求,但是有另一个npm项目叫做 react-router-redux ,因为从技术上来说,路由是UI-state的一部分,而且React Router不认识Redux,所以这个项目帮助我们连接这两个东西。 你看到我做了什么吗?我们走了一圈,又回到了第一篇文章! 项目最后遵照这一系列教程,最终你可以实现一个叫做“用户控件”的单页面应用。
与本系列其他文章一样,每个都有相关指导文档,在Github上也都有相关代码指导你怎么做。 总结我真的希望你能喜欢我写的这一系列文章,我意识到有许多关于React的主题我们都没有覆盖到,但我试图在保持真实的前提下,给新用户一种跨越React基础知识的认知,以及制作一个单页面应用所带来的感受。 系列文章
翻译文献:Leveling Up with React: Redux By Brad Westfall On March 28,2016 翻译作者:coocier (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |