You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
随着 JavaScript 单页应用开发日趋复杂,JavaScript 需要管理比任何时候都要多的 state (状态)。 这些 state 可能包括服务器响应、缓存数据、本地生成尚未持久化到服务器的数据,也包括 UI 状态,如激活的路由,被选中的标签,是否显示加载动效或者分页器等等。
管理不断变化的 state 非常困难。如果一个 model 的变化会引起另一个 model 变化,那么当 view 变化时,就可能引起对应 model 以及另一个 model 的变化,依次地,可能会引起另一个 view 的变化。直至你搞不清楚到底发生了什么。state 在什么时候,由于什么原因,如何变化已然不受控制。 当系统变得错综复杂的时候,想重现问题或者添加新功能就会变得举步维艰。
这里的复杂性很大程度上来自于:我们总是将两个难以厘清的概念混淆在一起:变化和异步。 我称它们为曼妥思和可乐。如果把二者分开,能做的很好,但混到一起,就变得一团糟。一些库如 React 试图在视图层禁止异步和直接操作 DOM 来解决这个问题。美中不足的是,React 依旧把处理 state 中数据的问题留给了你。Redux就是为了帮你解决这个问题。
Unfortunately, we will not launch any mixin support for ES6 classes in React. That would defeat the purpose of only using idiomatic JavaScript concepts.
所以官方也放弃了在 ES6 class 中对 mixin 的支持。
函数式(FP):高阶组件 High Order Component(下称 hoc)才是终极解决方案~~
零、环境搭建
参考资料
首先要明确一点,虽然 redux 是由 flux 演变而来,但我们完全可以并且也应该抛开 react 进行学习,这样可以避免一开始就陷入各种细节之中。
所以推荐使用 jsbin 进行调试学习,或者使用 create-react-app 作为项目脚手架。
一、Redux 是什么?
先不要在意那些细节
1.1. 为什么选择 redux?
简单总结就是使用 Redux 我们就可以
没有蛀牙(大雾)1.2. 三大原则(哲♂学)
1.2.1. 单一数据源(Single source of truth)
整个应用的 state 被储存在一棵 object tree 中,并且这个 object tree 只存在于唯一一个 store 中。
1.2.2. State 是只读的(State is read-only)
惟一改变 state 的方法就是触发 action,action 是一个用于描述已发生事件的普通对象。
因为所有的修改都被集中化处理,且严格按照一个接一个的顺序执行,(dispatch 同步调用 reduce 函数)因此不用担心 race condition 的出现。 Action 就是普通对象而已,因此它们可以被日志打印、序列化、储存、后期调试或测试时回放出来。
1.2.3. 使用纯函数来执行修改(Changes are made with pure functions)
为了描述 action 如何改变 state tree ,你需要编写 reducer。
Reducer 只是纯函数,它接收先前的 state 和 action,并返回新的 state。刚开始你可以只有一个 reducer,随着应用变大,你可以把它拆成多个小的 reducers,分别独立地操作 state tree 的不同部分。
二、Redux 基础
2.1. action
Action 就是一个普通的 JavaScript Object。
redux 唯一限制的一点是必须有一个 type 属性用来表示执行哪种操作,值最好用字符串,而不是 Symbols,因为字符串是可被序列化的。
其他属性用来传递此次操作所需传递的数据,redux 对此不作限制,但是在设计时可以参照 Flux 标准 Action。
简单总结 Flux Standard action 就是
2.2. reducer
Reducer 的工作就是接收旧的 state 和 action,返回新的 state。
之所以称作 reducer 是因为它将被传递给
Array.prototype.reduce(reducer, ?initialValue)
方法。保持 reducer 纯净非常重要。永远不要在 reducer 里做这些操作:2.3. store
Store 就是用来维持应用所有的 state 树的一个对象。
在 redux 中只有一个 store(区别于 flux 的多个 store),在 store 中保存所有的 state,可以把它当成一个封装了 state 的类。而除了对其 dispatch 一个 action 以外无法改变内部的 state。
在实际操作中我们只需要把根部的 reducer 函数传递给 createStore 就可以得到一个 store。
redux 中提供了这几个 api 操作 store
2.3.1. getState
返回当前的整个 state 树。
2.3.2. dispatch(action)
分发 action 给对应的 reducer。
该函数会调用 getState() 和传入的 action 以【同步】的方式调用 store 的 reduce 函数,然后返回新的 state。从而 state 得到了更新,并且变化监听器(change listener)会被触发。(对于异步操作则将其放到了 action creator 这个步骤)
2.3.3. subscribe(listener)
为 store 添加一个变化监听器,每当 dispatch 的时候就会执行,你可以在 listener(回调函数)中使用 getState() 来得到当前的 state。
这个 api 设计的挺有意思,它会返回一个函数,而你执行这个函数后就可以取消订阅。
2.3.4. replaceReducer(nextReducer)
替换 store 当前用来计算 state 的 reducer。
这是一个高级 API。只有在你需要实现代码分隔,而且需要立即加载一些 reducer 的时候才可能会用到它。在实现 Redux 热加载机制的时候也可能会用到。
2.4. createStore
忽略各种类型判断,实现一个最简的 createStore 可以用以下代码。参考资料
2.5. 计数器例子
{% iframe http://jsbin.com/kejezih/edit?js,console 100% 600 %}
{% iframe http://jsbin.com/jihara/edit?html,js,output 100% 600 %}
三、与 React 进行结合
3.1. 通过 script 标签导入 react
实现同样功能的 Counter
{% iframe http://jsbin.com/qalevu/edit?html,js,output 100% 800 %}
3.2. 用 Redux 和 React 实现 TodoApp
在添加 react-redux 之前,为了体会下 react-redux 的作用,首先来实现一个比计数器更复杂一点儿的 TodoApp 栗子~
3.2.1. 分析与设计
1. 容器组件 V.S. 展示组件
组件一般分为
最佳实践一般是由容器组件负责一些数据的获取,进行 dispatch 等操作。而展示组件组件不应该关心逻辑,所有数据都通过 props 传入。
这样才能达到展示组件可以在多处复用,在具体复用时就是通过容器组件将其包装,为其提供所需的各种数据。
2. 应用设计
3.2.2. 编码实现
1. action 部分
2. reducer 部分
拆分 reducer
目前代码看着比较冗长,其实在逻辑上 todos 的处理和 filter 的处理应该分开,所以在 state 没有互相耦合时,可以将其拆分,从而让 reducer 精细地对于对应 state 的子树进行处理。
注意观察最后的 rootReducer 函数,返回的是一个经过各种 reducer 处理过并合并后的新 state。
然鹅,注意这里
todos: todos(state.todos, action),
传入 state.todos,返回的一定也是 todos(因为都是 state 树上的节点)。所以 redux 提供了很实用的
combineReducers
api,用于简化 reducer 的合并。并且如果 reducer 与 state 节点同名的话(即 todosReducer -> todos)还能通过 es6 的语法更进一步地简化
随着应用的膨胀,我们还可以将拆分后的 reducer 放到不同的文件中, 以保持其独立性并用于专门处理不同的数据域。
3. view 部分
1. 只有根组件
首先只写一个根组件
<TodoApp />
,store 通过 props 传入 TodoApp,并在生命周期的 componentDidMount 和 componentWillUnmount 时分别订阅与取消订阅。TodoApp 只有根组件
{% iframe http://jsbin.com/bodise/edit?js,output 100% 800 %}
2. 组件拆分
将所有界面内容全写在 TodoApp 中实在是太臃肿了,接下来根据之前的分析结果将其分为以下子组件(全是展示组件)
所以 TodoApp 精简后是这样~
3. 增加容器组件
现在我们仍然是以 TodoApp 作为容器组件,其中各个子组件都是展示组件。
但是这样做的话一旦子组件需要某个属性,就需要从根组件层层传递下来,比如 FilterLink 中的 filter 属性。
所以下面我们增加容器组件,让展示组件通过容器组件获得所需属性。
通过观察重构后的代码可以发现有三点麻烦的地方
_getVisibleTodos
函数4. Provider
让我们先来解决第一个麻烦~,利用 React 提供的 context 特性
自顶向下地看一下如何使用到 TodoApp 中
现在中间的非容器组件完全不用为了自己的孩子而费劲地传递 store={store}
所以以上我们就实现了简化版的由 react-redux 提供的第一个组件
<Provider />
。然鹅,有木有觉得老写 contextTypes 好烦啊,而且 context 特性并不稳定,所以 context 并不应该直接写在我们的应用代码里。
5. connect
恭喜你
面向对象的思想学的很不错虽然 JavaScript 底层各种东西都是面向对象,然而在前端一旦与界面相关,照搬面向对象的方法实现起来会很麻烦...
作为 react 亲生的 mixin 确实在多组件间共享方法提供了一些便利,然而使用 mixin 的组件需要了解细节,从而避免状态污染,所以一旦 mixin 数量多了之后会越来越难维护。
所以官方也放弃了在 ES6 class 中对 mixin 的支持。
如上所示 hoc 的构造函数接收一个 W(代表 WrappedComponent)返回一个 E(代表 Enhanced Component),而 E 就是这个高阶组件。
假设我们有一个旧组件 Comp,然鹅现在接收参数有些变动。
当然你可以复制粘贴再修改旧组件的代码...(大侠受窝一拜)
也可以这么写,返回一个新组件来包裹旧组件。
然鹅,如果有同样逻辑的更多的组件需要适配呢???总不能有几个抄几遍吧...
所以骚年你听说过高阶组件么~?
可以看到借助高阶组件我们将 mapFn 和 Comp 解耦合,这样就算需要再嵌套多少修改逻辑都没问题~~~天黑都不怕~~~
ok,扯了这么多的淡,终于要说到 connect 了
是哒,你木有猜错,react-redux 提供的第二个也是最后一个 api —— connect 返回的就是一个高阶组件。
使用的时候只需要
connect()(WrappedComponent)
返回的 component 自动就完成了在 componentDidMount 中订阅 store,在 componentWillUnmount 中取消订阅和声明 contextTypes。这样就只剩下最后一个麻烦
其实 connect 函数的第一个参数叫做 mapStateToProps,作用就是将 store 中的数据提前处理或过滤后作为 props 传入内部组件,以便内部组件高效地直接调用。这样最后一个麻烦也解决了~
然鹅,我们问自己这样就够了么?并没有...
还有最后一个细节,以 FilterLink 为例。
除了从 store 中获取数据(filter),我们还从中获取了 dispatch,以便触发 action。如果将回调函数 onClick 的内容也加到 props 中,那么借助 connect 整个 FilterLink 的逻辑岂不是都被我们抽象完了?
是哒,connect 的第二个参数叫做 mapDispatchToProps,作用就是将各个调用到 dispatch 的地方都抽象成函数加到 props 中的传给内部组件。这样最后一个麻烦终于真的被解决了~
TodoApp 使用 react-redux
{% iframe http://jsbin.com/fumihi/edit?js,output 100% 800 %}
The text was updated successfully, but these errors were encountered: