本篇文章主要内容是介绍 Redux 的历史背景、实战和概念,目标读者设定为 redux 初级玩家。javascript
既然定位为初级玩家,那么就不会讲源码、实现、设计原则这些东西。我会带你站在历史的角度俯览 redux 的传奇一辈子,并经过代码示例掌握基本用法,经过图示把道理捋明白。html
redux 官方自述是 A Predictable State Container for JS Apps,通俗理解,是一个用于 JavaScript 的状态管理库。注意:它特别强调了 Predictable State(可预测状态)。前端
这个很好理解,万物皆有状态,能够把人类简单的分红清醒/睡眠/昏迷等状态。还能够继续细化,好比情绪,分为生气/冷静/愤怒/开心等状态。身体的某个部位,好比眼睛,失明/睁开/闭眼等状态。vue
为了更加容易理解状态,这里拿最典型的 TodoList 举例。java
最原始的 web 应用,在没有状态的状况下,应该怎么作呢?react
<!-- todolist 原始版 -->
<div id="todo-list">
<div>
学英语<span> 未完成 </span><button onclick="complete(this)">完成</button>
</div>
<div>
学数学<span> 已完成 </span ><button onclick="complete(this, true)">取消完成</button>
</div>
<div>
学语文<span> 未完成 </span><button onclick="complete(this)">完成</button>
</div>
</div>
<script> function complete(target, cancel = false) { if (cancel) { target.textContent = "完成"; target.onclick = () => complete(target); target.previousElementSibling.textContent = " 未完成 "; } else { target.textContent = "取消完成"; target.onclick = () => complete(target, true); target.previousElementSibling.textContent = " 已完成 "; } } </script>
复制代码
能够看到,代码比较简单,这种代码在四五年前是很是流行的,可是如今已经不多有人这么写代码了。git
若是加入状态的概念,应该怎么作?github
<!-- todolist 状态版 -->
<div id="todo-list"></div>
<script> var todoEl = document.getElementById("todo-list"); var state = { todoList: [ { id: 1, name: "学英语", complete: false }, { id: 2, name: "学数学", complete: true }, { id: 3, name: "学语文", complete: false } ] }; function render() { const todoHtml = state.todoList .map( item => `<div>${item.name} ${ item.complete ? `已完成 <button onclick='complete(${item.id}, true)'>取消完成</button>` : `未完成 <button onclick='complete(${item.id})'>完成</button>` }</div>` ) .join(""); todoEl.innerHTML = todoHtml; } function complete(id, cancel = false) { state.todoList.find(item => item.id === id).complete = !cancel; render(); } render(); </script>
复制代码
能够看到,比起原始版实现,状态版的代码好像更多。web
两份代码的显示效果是彻底一致的,那么,到底哪一个版本更好呢?数据库
若是功能肯定下来,只有目前的这么点功能,而且不会有任何需求的变更,那么无疑,第一种实现是更优选择。但这种状况仍是在少数,更多状况下,应用老是会存在各类不肯定性,随时均可能变更。
之前的 web 应用,不能称之为应用,只能叫网页,大一点的叫网站。如今为何叫 web 应用了?由于这么叫高大上吗?并非,而是如今的 web 应用更大,更复杂了。
你能够仔细观察,原始版 button 的文字和 click 绑定的函数,与前面的“学数学”、“学英语”彻底无关。若是要给页面添加新的元素,好比何时完成的,点击每一条 item 会弹出详情等。这样扩展下去,应用会愈来愈混乱和复杂。
状态版的实现,比原始版多了一个 state
变量和一个 render
函数。
其中,state
就是应用的状态,当页面发生操做时,修改 state
。当应用的 state
发生变化时,就会调用 render
方法,从新渲染页面。
这就是有状态和无状态的区别。
是否是有点 react 和 vue 的味道了?
react 和 vue 都有 state
和 render
。react 的 state
,vue 的 data
,都是能够响应数据变化而自动重绘页面的。只不过实现方式不一样。react 是在 this.setState
后发生重绘,vue 是经过对对象和数组进行数据劫持实现的,但它们同时也带来了新的问题,好比 react 中异步的渲染,vue 在 data
声明后新添加的属性没法自动响应等。更为细节的部分很少说,不在该文章范围之内。
如今已经明白了状态的做用,那么,既然 react 和 vue 自身都有状态系统,为何还须要状态管理库呢?
缘由是由于在组件化的探索中出现了问题。
一个正常的组件树像下面这张图。
使用组件化的应用,可能存在几百上千个组件,若是不使用状态管理库,状态会散落在每一个组件内部。一些须要共享的状态,可能要传给父组件,祖辈组件,也可能要传给子组件、子孙组件,还可能要传给兄弟组件,祖辈的兄弟组件等等。这些场景虽然仍能经过 props 的机制完成,可是很是不直观,会让人感到错综复杂,这好像又找到了以直接修改 DOM 的方式来编写代码的感受,这很危险。
为此,react 没有提供什么特殊的方案,它建议直接使用 flux 模式来解决。而 vue 没有放弃,它提供了不少种解决方案,v-model
、sync
、$attrs
、$listeners
等等,但没有什么实质性的改变。
跨组件通讯,是一个必需要解决的问题。
真正有实质性变化的,是 react 的 context API 和 vue 的 vue event bus 模式。
它们很像,又有所区别。它们存在一样的问题,改变状态的过程不够直观,虽然能够跨组件修改某个状态,但很难对这个操做进行跟踪、定位和预测。
这样虽然给咱们极大的自由任意操做全局状态,但让咱们难以快速找到到底是谁在何时改变了某个全局状态。
因此,还须要更加规范细致的解决方案,能够追踪数据什么时候变化的状态管理库,即本节开头所讲的可预测状态,这也是 redux 一再强调的特性。
解决问题的路上老是会出现更多新的问题,这个过程就像是俄罗斯套娃,一步步无限接近真理,可世界上根本没有真理。
redux 在 github 上的第一次提交记录是 2015 年的 5 月 31 日,提交者是 gaearon,名字翻译过来你可能很是熟悉,叫盖伦。这个盖伦就是大名鼎鼎的 Facebook 工程师 Dan Abramov,国内俗称 Dan 大神。dan 大神是一个很是真实的人,不掩饰问题,不故做高深。不少人都很是喜欢他,这里是他的博客。
redux 并非凭空出现的,在它以前,facebook 还有一个叫作 flux 的库,flux 是 2014 年 7 月 24 日开源的。对于如今的前端开发者来说,flux 可能比较陌生。由于在它那个时代,前端领域尚未特别重视应用的状态,因此 flux 在当时并不流行。2014 年是什么时代?要知道如今所谓的前端三大框架资历最老的 Angular 出现的时间也才是 2014 年 9 月 19 日,比 flux 还要晚出现 2 个月。那个时代,仍是 jQuery 和 Bootstarp 横行的时代。flux 在最开始的一段时间里,不少人不解和困惑,甚至有人提出 flux 是事件编程的倒退。其实当时不少人没有正确地看到 flux 想作什么,只是停留在表面的 API 的用法上,没体会到 flux 真正的核心是一个单向数据流的状态机。通过一段时间,flux 逐渐被人理解和承认。不巧的是以后不到一年的时间里,mobx 和 redux 相继出现,它们都在 flux 的基础上作了大量改进,因此它们比 flux 更加优秀。flux 没有机会大放异彩就被埋没在了历史的长河中,属于一个昙花一现的库。如今出现最频繁的地方,大概就是相似我这篇文章同样介绍 redux 历史的文章或书中。
因此,前端的应用状态这一律念在业界成型的时间大概是 2013 年到 2014 年左右。
若是以一个过来人的身份回答,redux 很简单,它不难。
毕竟它的 js 源码仅有 712 行,包括注释和换行符,若是愿意认真读的话,半天时间就能读完一遍。
但是若是把时间回放到几年前我刚开始接触 redux 的时候,我也是很懵的。
如今让我以一个初学者的身份来回答这个问题的话,应该是这样,redux 自己很是简单,但学习它有些难度。
昨天看阮一峰老师最新写的《科技爱好者周刊:第 99 期》中说了这么一件事。
两天前,ZDNet 发表了新文章《认识 iPad:提升你生产力的 10 个应用》。这一类的科普文章,每周都会出现,这难道不是一件很奇怪的事情吗?
iPad 已经发布 10 年了,但是人们还必须看这种文章,说明你们还没找到办法,到底怎样才能在 iPad 上进行实际工做!
这让我想到了如今的 redux。其实到如今,仍是有不少人在写关于 redux 的文章,也有不少人在问关于 redux 的疑惑。这说明你们须要 redux,但至今仍未找到学习 redux 的最好方式。因此我尝试把我这几年使用 Redux 的心得体会写一写,或许会对你们有所帮助。
dan 大神在 redux 发布 3 年后的某一天,提交了一条commit。
标题是“Remove "Redux itself is very simple"”,意思是删除了一段文字,“Redux 自己很是简单”。
同时,dan 大神还在该条 commit 中提到:
Reflecting a few years later this was a bit of a silly thing to write in the docs. Of course it's not simple to people learning it.
翻译成中文的意思是:
几年后,仍在文档中强调“react 自己很是简单”是一件很愚蠢的事情。 固然,要学习它并不容易。
因而可知,Redux 对新手而言确实不怎么友好。
至于怎么学习,推荐三条路。
第一条,英语好的同窗,去看官方文档,这是最佳学习方法。也能够看一些优秀的资源。好比dan 大神的博客、dan 大神的视频、Redux 官方推荐学习资源等。
第二条,技术很是强的同窗,大致翻阅下文档,写两个 demo,而后去读源码吧。
第三条,技术通常,英语也挺差的同窗。看一些中文资料也不错,好比如今你正在看的这篇文章。
学习这件事,尽可能仍是要去源头看看。“取乎其上,得乎其中。取之于中,而求之于下。“。
但也不用过分强求,总之学会才是目的,具体怎么学,仍是要看你习惯哪一种方式。
redux 和相似的框架都在解决 web 应用中状态难以管理的问题。
在早期,facebook 的 web 网站常常会碰到数据和视图不一致的现象。好比消息图标莫名其妙的亮起,当点击图标后,又发现没有消息。facebook 的工程师们不止一次地解决这个 BUG,但每次修复后的一段段时间里都会重复出现。
形成这个现象的缘由是数据和视图的复杂关系。数据的流向很难预测,因此也很难理清它们之间具体的关系是怎样的。
借用一张网图来看一下 jQuery 时代的应用数据流向。
这很是糟糕。
facebook 的工程师在探索这个问题时,给出的第一个答案就是 flux。
flux 不只仅是一种库或框架,更是一种模式或架构。这种模式或架构的名字也叫做单向数据流。
flux 很是好理解。
好比页面初始化加载的这个动做,是一个 Action, dispatcher 会把 action 传递给 store,dispatcher 会修改应用的 store,store 的改变会重绘视图 view。一个界面就加载出来了,很是简单的原理。
视图 view 上有一个按钮,点击按钮的动做,又是一个 Action, action 又会告诉 dispatcher 该去通知 store 了,而后 store 会发生改变,重绘 view。如此循环往复,愈来愈简单了。
从上面两张图中能够看出,不管应用程序多么复杂,数据变化的流向老是一致的。
若是再加上 api 的调用,流程是这样的。
注意:这是 flux 的数据流向图,redux 和它有所区别。但不用在乎,这里只是大概演示下流程。
事实证实,flux 是对的。
在以后的探索中,facebook 又作出了更让人满意的答案,redux 和 mobx。尤为是 redux。
虽然 flux 和 react 在设计原则和思想的细节上有较大的差别,但解决的问题是相同的。
react 解决的问题就是经过单向数据流的架构方式使应用的状态按照必定的模式来变化,从而可以预测应用的状态。
Redux 自己是彻底独立运行的库,不会基于某个库或框架、也不会依附于某个库或框架。因此,react 虽然能够直接使用 redux,可是没法和自身的响应式结合。
为了解决这一问题,facebook 又开发了 react-redux。
二者是有区别的,redux 的责任是单纯的状态管理,react-redux 更像一个胶水,把 react 应用程序和 redux 状态仓库粘在一块儿。让 redux 中数据的变化能够触发 react 中的数据响应视图。
下面这段话是来自于 redux 官网:
Keep in mind that Redux is only concerned with managing the state. In a real app, you'll also want to use UI bindings like react-redux.
翻译成中文意思是:
请记住,Redux 仅与管理状态有关。在真正的应用程序中,您还须要使用 UI 绑定的库,例如react-redux。
不少人在学习和理解 redux 时,常常会出现概念混淆的问题,我以为这是学习 redux 的一大屏障。事实上,概念越多的库或框架,越难学习,好比 rx.js。
我认为先学习用法,再去理解概念相对更友好一些。由于这样更加直观。
我一直在强调 redux 是能够独立运行的,从某种程度上,redux 和 react 没有任何瓜葛,记住这一点,这很重要。
下面用代码演示如何在原生 js 中使用 redux,仍然是那个 todolist 示例,拿以前写的状态版进行重构。
<!-- todolist redux版 -->
<script src="https://unpkg.com/redux@4.0.5/dist/redux.js"></script>
<div id="todo-list"></div>
<script> let todoEl = document.getElementById("todo-list"); // 1. 定义 action types,它描述了你的应用程序有几种改变数据的操做 let COMPLETE = "COMPLETE"; let CANCEL_COMPLETE = "CANCEL_COMPLETE"; // 2. 定义 reducers // reducer 默认会有 2 个参数,第一个是初始状态,第 2 个是 dispatch 传递进来的 action let initialState = [ { _id: 1, name: "学英语", complete: false }, { _id: 2, name: "学数学", complete: true }, { _id: 3, name: "学语文", complete: false } ]; function todoReducer(state = initialState, action) { // 经过判断 action 的 type 属性,来进行不一样的 state 变化。 switch (action.type) { case COMPLETE: state.find(item => (item.id = action.id)).complete = true; return state; case CANCEL_COMPLETE: state.find(item => (item.id = action.id)).complete = false; default: return state; } } // 3. 调用 createStore 建立 store,todoReducer 是必传参数 let store = Redux.createStore(todoReducer); // 4. 定义 actions creator,它们是一个函数,返回一个简单对象 let completeAction = id => ({ type: COMPLETE, id }); let cancelCompleteAction = id => ({ type: CANCEL_COMPLETE, id }); function render() { // 5. 使用状态时,调用 store 的 getState 方法能够获取最新的状态 const todoHtml = store .getState() .map( item => `<div>${item.name} ${ item.complete ? `已完成 <button onclick='complete(${item._id}, true)'>取消完成</button>` : `未完成 <button onclick='complete(${item._id})'>完成</button>` }</div>` ) .join(""); todoEl.innerHTML = todoHtml; } function complete(id, cancel = false) { // 6. complete 函数再也不直接修改 state 中的数据,而是调用 store 对象的 dispatch 方法传递 action 的方式来建立新的 state store.dispatch(cancel ? cancelCompleteAction(id) : completeAction(id)); // render(); 再也不这里重绘,而是使用 store 的 subscribe } // 7. 使用 store 的 subscribe 监听 state 的变化,它的参数是一个回调函数,每次 state 变化,都会自动调用该函数 store.subscribe(render); render(); </script>
复制代码
代码中有详尽的注释,这几乎是一个 redux 应用的最简版本。看明白这个例子,就搞懂了 redux 最基本的使用。
虽然代码的注释中标注了各个步骤的序号,但你能够不按照这个顺序来写代码。标注只是为了方便理解。
如今来回顾一下,上面的代码都作了什么。
首先要有一个 store,建立 store 须要调用 Redux.createStore()。 createStore 接受一个 reducer 函数做为参数。reducer 默认有 2 个参数,第 1 个是 state,它是当前状态树,第 2 个是 action,这个参数其实就是 store 对象的 dispatch 方法传递的参数 action。
action 是一个结构简单的对象,它有一个 type 属性,用于标记这个 action 是作什么的,与之对应的 reducer 函数会经过 switch 来处理这个 action。
建立 action 对象的函数叫作 action creator,它也很是简单,就是返回一个 action 对象。
reducer 函数是处理数据变动的地方,它会返回一个新的对象,这个对象就是新的状态。这和 Array 的 reduce 的运行机制很是相像。
store 的 getState 方法用于获取当前状态树对象 state;subscribe 方法用于监听 state 的变化,它接受一个函数做为参数,每次数据发生变化时,调用改回调函数。
不少人在刚开始学习 redux 时,被各个概念和它们之间的关系弄的云里雾里,我认为只要把这些概念之间的关系梳理清楚,学习 redux 的一大门槛就算跨过去了,为此我特地画了一张简单的关系图。
若是你历来没有使用过 redux,那你必定会以为这里面的各类参数传来传去,函数调来调去,就像变戏法同样。为何不直接修改 state 呢?state 的本质不就是一个全局对象吗?
确实是这样,state 就是一个全局对象。
若是直接修改 state 会有几个问题。
当一个数据没达到预期时,很难找到究竟是在哪里修改了这个数据。
虽然你可使用注释来在必定程度上解决这个问题。
JavaScript 的对象是很是松散的,你能够随意修改,也能够把它弄丢。好比在某个不起眼的角落,写了一行 state = null;
处理数据最规范的手段就是经过某种模式来变动它们,而不是直接用=来修改。最典型的例子是数据库。
在 2017 年 8 月份,有一篇文章曾经很是火爆,shape your store like your database。像数据库同样设计你的 Redux,你能够读一下。
redux 是一个 JavaScript 数据容器,其实它更像一个数据库。
而咱们所作的一切和 redux 中那些看似繁琐的 API 都是为了让数据的更新是可预测可追踪的,若是使用 redux 的方式来处理数据,你能够立刻找到此次状态变动是由于什么,是在哪一个地方让数据发生了变化。这是 redux 的惟一好处。
再来思考一个问题,一个简单的 todoList 应用把代码弄的这么复杂,有必要吗?
事实上,不管如何都找不到任何须须使用 redux 的理由。
redux 带给咱们的不只仅是学习成本,还会让咱们多写不少代码。
这是付出,同时还要看收益。正常状况下,收益要高于付出,至少也要持平,咱们才会考虑付出。没有人会傻到本身给本身刨坑吧?
不少初学者在学一门框架或库时就想把全家桶全用上,这是绝对不可取的。
redux 的开发动机在官网上写的很明白,就一句话:our code must manage more state than ever before.(咱们的代码变必须管理比以往更多的状态)
换句话说,咱们的应用中存在大量状态时,才应该考虑使用 redux,而不是在一开始就优先考虑使用 redux。
上面的例子使用了大部分 redux 的核心 API,但没介绍combineReducers
、applyMiddleware
、bindActionCreators
、compose
这几个更高级的 API,由于它们都不是最核心的 API,而是为了解决某项更高级的问题而存在的。这些不会在这里讲,但会在下一篇文章中提到。
dan 大神在 2018 年曾经发表过一篇文章,you might not need redux(你可能不须要 redux),你能够读一读。而后认真思考,到底需不须要 redux。我所指的不是到底需不须要学习 redux,而是在你的应用程序中需不须要使用 redux。redux 是一个优秀的库,做为前端工程师,不管怎样老是要见识一下的。
虽然 redux 能够在任何环境下使用,但 facebook 开发它的最初目的仍是为了解决大型 react 应用的状态管理问题。
在 react 中使用 redux,通常都会用到 react-redux 这个库。文章前面有提到,react-redux 自己就像是一个胶水,并不复杂。
它的用法大概是这样。
首先导出一个叫作 Provider 的组件,而后在 Provider 组件中注入 store。再用 Provider 把应用的根组件包裹起来。这样就可使用 store 了。
import React from "react";
import ReactDOM from "react-dom";
import { Provider } from "react-redux";
import store from "./store";
import App from "./App";
const rootElement = document.getElementById("root");
ReactDOM.render(
<Provider store={store}> <App /> </Provider>,
rootElement
);
复制代码
react-redux 是基于 react 中的 context 来实现的,因此这一步是必须的。
须要使用 store state 的组件,使用 connect 函数将 store 和 react 的组件链接起来。
import { connect } from "react-redux";
import { increment, decrement, reset } from "./actionCreators";
const Counter = props => <div> {props.counter} </div>;
const mapStateToProps = (state /*, ownProps*/) => {
return {
counter: state.counter
};
};
const mapDispatchToProps = { increment, decrement, reset };
export default connect(mapStateToProps, mapDispatchToProps)(Counter);
复制代码
若是不须要 store 的组件,在写法遵循 react 的正常写法便可,不须要变更。
代码中多了两个新的概念,mapStateToProps 和 mapDispatchToPorps,其实它们很是好理解。
mapStateToProps 是将 redux 中 state 映射到 react 组件的 props 中,其实就是 getState 的做用。
mapDispatchToProps 是将 redux 中的 dispatch 映射到 react 组件的 props 中,这样就可使用props.increment
来调用 dispatch。
react-redux 的原理就是将数据提高至最高组件,而后在组件中经过 props 层层传递。
react-redux 的使用就这么简单,是的,很是简单。
什么是 hooks?
hooks 是 react 16.8 推出的新特性,一个替换 component 组件的方案,react 将来的发展方向。
在 hooks 出现以后,咱们再也不须要 connect。
react-redux 最经常使用的 hooks 有 3 个,useSelector、useDispatch 和 useStore。
useSelector 取代的是 mapStateToProps,useDispatch 取代的是 mapDispatchToProps。useStore 是对 store 的引用。
一样是上面那段计数器代码,用 hooks 会这样写。
import { useSelector, useDispatch } from 'react-redux'
import { increment, decrement, reset } from './actionCreators'
export default const Counter = () => {
const counter = useSelector(state=>state.counter);
const dispatch = useDispatch();
// 若是你要调用 dispatch
// dispatch(increment());
return (
<div> {counter} </div>
)
}
复制代码
能够看到,使用 hooks 后,代码变得很是优雅。
hooks 已经出现 3 年,如今很是稳定,若是还认为 hooks 是新特性,那真是有点跟不上时代的节奏了。我很是推荐使用 hooks,如今我开发的 react 项目中几乎所有都是函数式组件和 hooks。
须要注意的是,hooks 不能在 class 组件中使用,它只能在函数组件中使用,并且只能在函数的最外层中使用,这些都取决于 hooks 的实现方式。
经过这篇文章的学习,你应该已经掌握了 react 最基本的使用,若是文中所讲述的东西你都可以掌握并理解,那么恭喜你,已经成为一个合格的 redux 初级玩家了!
不过,游戏才刚刚开始。
接下来我会再写两篇关于 Redux 的文章,读者群体定位分别是中级玩家和高级玩家,敬请期待。
若是这篇文章对你有所帮助,而且你也喜欢个人文章风格,请关注个人微信公众号。