作为一名JavaScript开发者学习Redux的原因

123 阅读15分钟

Redux已经伴随我们一段时间了。2015年已经公开的东西--由Dan Abramov在他臭名昭著的关于时间旅行的演讲中演示--突然变成了许多JavaScript开发者的日常事务。尤其是React开发者受到了这种现象的严重冲击,因为它给大家提供了一条如何处理状态管理的清晰路径。

一年后,Dan Abramov对Redux最初的成功原因进行了总结。这两个讲座都是超级有见地的,讲述了如何用一项技术来解决一个问题,以及是什么让这项技术毕竟持久。很多JavaScript库来了又走。但Redux却成功地留在了我们身边。

然而,我相信成功的库不仅仅只有一个。Redux对JavaScript社区的许多人来说是一个整体的思维方式的转变,他们实际上是在只有Web开发的情况下长大的,但以前从未听说过函数组合或不可变性。无论Redux是继续陪伴我们几年,还是为其他状态管理库腾出位置,它都为我们开发现代Web应用留下了一笔巨大的财富。

一切都有明确的目的

如果有人问我关于Redux的一个简短表述,那就是:

State => View

如果需要更多的解释,我会将其扩展为:

Action => Reducer(s) => Store => View

如果需要更多的上下文,可以把它扩展为一个重复的循环:

Action => Reducer(s) => Store => View => User Interaction => Action ...

这就是Redux(状态)在React(视图)这样的库中的全部内容。链上的每一个部分都有它的任务。每件事都清楚地相互分离,并为更大的目标服务:状态管理。

然而,太多的人将Redux与React紧密联系起来。一旦他们开始学习React,他们就会从一开始就React和Redux结合起来,这让很多开发者因其复杂性而失去动力。然而,如果只是考虑Redux的话,简而言之,Redux并不复杂,因为它毕竟只是一个保存状态的状态容器(对象);有一个API,可以让人

  • 操作状态
  • 接收状态
  • 听取状态变化

让我们用JS简单地回顾一下Redux的所有部分。这是一个Redux Reducer,作用于两个Redux Actions,完全不依赖Redux库。

function reducer(state, action) {  switch(action.type) {    case 'TODO_ADD' : {      return applyAddTodo(state, action);    }    case 'TODO_TOGGLE' : {      return applyToggleTodo(state, action);    }    default : return state;  }}
function applyAddTodo(state, action) {  return state.concat(action.todo);}
function applyToggleTodo(state, action) {  return state.map(todo =>    todo.id === action.todo.id      ? { ...todo, completed: !todo.completed }      : todo  );}

Redux商店,它知道Redux Reducer的情况。

import { createStore } from 'redux';
const store = createStore(reducer, []);

然后,Redux商店提供了一个小的API表面与之互动 -- 例如,调度一个Redux Action。

store.dispatch({  type: 'TODO_ADD',  todo: { id: '0', name: 'learn redux', completed: false },});

最后,在JavaScript中,你可以监听Redux商店的变化。

store.subscribe(() => {  console.log(store.getState());});

这就是Redux的简明扼要的所有片段。Action、Reducer、Store。现在还没有React,也没有View。视图可以被认为是console.log 。如果你还没有学习Redux,请随时查看这个长篇阅读的React + Redux教程,它在Redux整合到React之前就已经教过了。

Redux的Actions、Reducers、Store在Redux工具链中都有其强制性的位置。如果需要在上面添加语法糖,可以添加动作创建器和选择器。所有你需要开始的是redux库来创建Redux商店。其他一切都只是JavaScript。另外,目前还没有看到像React这样的库。它显然与自己的库 --react-redux-- 和生态系统分开。

我相信Redux已经教会了我们很多关于将事物分离成原子部分的方法。在库内--包括它的Actions、Reducers和Store--每样东西都有它的用途和清晰的API,但在库外也有所有的绑定,如React和Angular的不同框架。它给每个为生态系统做出贡献的人提供了总体规划,告诉他们应该如何用清晰的约束和简单的API来做事情。

不变性

在Redux之前,不变性在JavaScript领域并不是一个大问题。对变量进行突变是每个人的日常工作。然而,随着现代前端框架的引入和网络应用的扩展,许多人感受到了传递可变信息的痛苦。在一个地方改变一个变量意味着在你的应用程序的另一个地方产生不可预知的副作用。

在Redux中,状态容器中的所有东西都被视为不可变的数据结构--但这并没有被强制执行。如果你要向一个数组添加一个条目,在Redux中,人们已经习惯了将数据结构视为不可变的JavaScript函数。

// doconst newState = state.concat(action.todo);
// don'tstate.push(action.todo);

有各种数组和对象函数可以返回新的数组/对象--保持它们的不可变性--而不是变异它们。此外,最近的语言添加对促进这种新的思维方式有很大帮助。

const toggledTodo = { ...todo, completed: !todo.completed };

人们开始思考这些关于JavaScript中的不可变数据结构的细微差别--这对整个JavaScript的开发体验有很大的好处。不再有泄漏的变量,这些变量在一个人的应用程序中的各个地方被突变了。

纯函数

与不可变性几乎相同的是,在Redux被引入到JavaScript生态系统之前,纯函数并没有被大量讨论。它更像是一个建议,即函数应该是纯的,但从来没有被网络开发者们认真对待过。

// action creator returning an action
function addTodo(todo) {  return {    type: 'TODO_ADD',    todo,  };}
const action = addTodo({  id: '0',  name: 'learn redux',  completed: false});
store.dispatch(action);

随着Redux思想的转变,人们开始避免在他们的函数中出现副作用,以满足Redux的理念,同时也是为了确保更好的可测试性,避免他们的函数在长期内出现不可预见的漏洞。

(Input) => Output

Redux Action只是对当前状态的一个操作,而Redux Reducer则是通过这个操作来修改状态,从一个表示到下一个表示。这中间没有远程API调用或其他任务。它总是遵循一个函数签名。

(Current State, Action) => New State

这就是使Redux Reducer和一般Redux状态管理高度可预测的秘密。一个动作导致一个新的状态,基于动作的信息和当前的状态。Redux存储只是这个状态的容器。

用函数思考

随着Redux的普及,函数被认为是JavaScript中比以往任何时候都重要的一等公民。不仅纯函数的概念在开发者之间流传,而且其他概念如高阶函数和函数组合也得到了普及。

function toggleTodo(action) {  return function(todo) {    return todo.id === action.todo.id      ? { ...todo, completed: !todo.completed }      : todo;  };}
function applyToggleTodo(state, action) {  return state.map(toggleTodo(action));}

所有这些概念都使JavaScript开发者越来越多地被引入到函数式编程的范式中。显然,这些概念并不是源于Redux,但它们被许多刚开始学习JavaScript的开发者或迄今为止在职业生涯中使用过JavaScript的开发者所注意到。

JavaScript ES6

这只是一个时间上的巧合,JavaScript ES6是在Redux获得青睐的同时被引入的。JavaScript ES6给开发者带来了新的功能,而这些功能恰恰是Redux的优势所在。例如,函数可以用箭头函数来表达,而不是庞大的函数语句和主体。

const toggleTodo = action => todo =>  todo.id === action.todo.id    ? { ...todo, completed: !todo.completed }    : todo;
const applyToggleTodo = (state, action) =>  state.map(toggleTodo(action));

JavaScript ES6使许多表达式更加简洁。从另一个对象中创建一个新的对象,同时保持数据结构的不可改变,这可以通过JavaScript新的传播操作符来实现。

const toggledTodo = {  ...todo,  completed: !todo.completed,};

这只是JavaScript的一个奇妙的补充,它使许多库,如Redux,也包括React,蓬勃发展。

单向的数据流

Redux已经为现代状态管理增加了很多可预测性,它只是把所有的片段(执行状态变化时必须的)拆开,并赋予它们明确的目的和API。

Action => Reducer(s) => Store

然而,另一个伟大的因素是单向数据流,它主要被引入React及其之前的状态管理库(见Flux),但也被Redux接纳为可预测的状态管理流。

View => Interaction => Action => Reducer(s) => Store => Updated View

有一个清晰的单向数据流。人们可以看到谁在负责。

  • 谁启动了状态操作链(如用户交互)。
  • 谁用哪些信息(行动,当前状态)来操纵状态(reducer)。
  • 谁受到了状态操作的影响(例如用户界面的重新渲染)。
1) Interaction in View =>2) State Manipulation =>3) Updated View =>1) Interaction in View =>...

学习信息流被认为是每个开发者的巨大财富。在一个清晰的架构中,没有不可预测的副作用--由于纯函数和不可变的数据结构--也没有难以遵循的双向/多向数据流--这在以前是其他框架失败的一个话题。一切都朝着一个方向发展,最终形成一个可预测的状态管理循环。

在事件中思考,而不是在设置器中思考

人们常常误以为Redux是一个简单的setter/getter概念。UI派发一个动作;该动作经过一个还原器;最终在Redux商店中设置一个新的状态。被订阅的UI从Redux商店接收更新,并根据新的状态重新渲染。

// that's not Redux
store.setState({  id: '0',  name: 'learn redux',  completed: false});

然而,这并没有充分肯定Redux的作用,因为它是一个复杂的事件驱动的概念(见事件源或CQRS)。它在两者之间设置了还原器,由它们自己决定是否受到传入动作的影响。它将视角从

  • 显式状态操作到隐式状态操作
  • 设置器到事件
  • 单一用途的还原器到多用途的还原器
  • 狭隘的还原器到开放的还原器

尤其是最后两个事实,每个开发者都应该考虑到,以利用Redux的全部潜力,因为突然间,减速器在一个比普通设置器更高的抽象水平上操作,并与你的应用程序中的其他减速器一样行动。

import { createStore, combineReducers } from 'redux';
function todoReducer(state, action) {  switch(action.type) {    case 'TODO_ADD' : {      return applyAddTodo(state, action);    }    case 'TODO_TOGGLE' : {      return applyToggleTodo(state, action);    }    default : return state;  }}
function statisticReducer(state, action) {  switch(action.type) {    case 'TODO_ADD' : {      return applyCalculateTodos(state, action);    }    default : return state;  }}
const rootReducer = combineReducers({  todos: todoReducer,  statistics: statisticReducer,});
const store = createStore(rootReducer, []);

注意:请记住,这里给出的例子并不完美,因为任何基于todo实体计算的各种统计数据都可以通过从状态中获得所有todos并及时用正确的选择器计算它们的统计数据来进行计算。

Redux为任何以前没有见过事件驱动系统的网络开发者提供了一个很好的示范。仅仅通过观察行动、还原器和商店是如何一起工作的,它就能让人们了解到其他应用中的事件驱动系统是如何工作的。

领域驱动设计

领域驱动设计(DDD)并不是Redux本身的东西,在这里可能有点牵强,但一旦你超过一个小规模的应用程序,每个开发人员或开发人员团队都必须考虑如何在使用Redux时将状态/还原器分割到他们的领域。

你最终可能会为(A)从远程API获取的各种实体(如todos,用户),(B)过滤器(如显示所有未完成的todos,显示所有活跃用户)和(C)统计(如计算活跃用户完成的todos数量)使用还原器。

import { createStore, combineReducers } from 'redux';
...
const rootReducer = combineReducers({  todos: todoReducer, // (A)  users: userReducer, // (A)  filter: filterReducer, // (B)  statistics: statisticReducer, // (C)});
const store = createStore(rootReducer, []);

当人们看到这种领域聚类时,是否会想到领域驱动的设计并不重要,重要的是他们不自觉地开始用领域来思考,以及如何将它们封装在自己的地方,并有明确的API向外传递。

尽管它可能不是书本上所教的领域驱动设计,但它为这些主要在其他编程语言中出现的概念打开了开发者的思路。

创新的生态系统

一旦你开始使用Redux,你可能也会遇到选择器和动作创建者。

Action Creator => Action => Reducer(s) => Store => Selector => View

这只是Redux的另外两个概念,让它的每一部分在整个工具链中都有更明显的作用。行动创造者创建了一个行动对象,而选择者只选择你的状态的一个片断,使其可用于你的用户界面。

// action creator
function addTodo(todo) {  return {    type: 'TODO_ADD',    todo,  };}
// selector
function getCompletedTodos(state) {  return state.filter(todo => todo.completed);}

除了这些概念,你很可能会遇到Redux生态系统中其他流行的库,如Redux Saga或Redux Observables--它们都是作为中间件处理Redux的副作用。它们中的每一个都为Redux引入了一个新的概念,这些概念在JavaScript中根本没有大量使用:生成器可观察变量

// Redux Saga
function* fetchUser(action) {  try {    const user = yield call(Api.fetchUser, action.payload.userId);  yield put({ type: 'USER_FETCH_SUCCEEDED', user: user });  } catch (e) {    yield put({ type: 'USER_FETCH_FAILED', message: e.message });  }}
function* userWatcher() {  yield takeEvery('USER_FETCH_REQUESTED', fetchUser);}

这是使Redux成功的另一个方面:它的生态系统。Redux的概念只是核心,但它的API设计和仅仅使用JavaScript的简单性给其他开发者留下了很多选择进入其世界的机会。这导致库的作者探索新的概念,如生成器或可观察变量,并将它们带到更多开发者的注意中。

// Redux Observable
const pingEpic = action$ => action$.pipe(  filter(action => action.type === 'PING'),  delay(1000),  mapTo({ type: 'PONG' }));
dispatch({ type: 'PING' });

Redux及其生态系统拓宽了许多JavaScript开发者的视野;给他们提供了探索他们所选择的编程语言的可能性的工具。同时,其他状态管理库的作者也从Redux的生态系统中汲取灵感,使其成为一个繁荣的生态系统的完美蓝图。

KISS

这是一个共同的主题。学习Redux时,如果一下子开始学习所有的东西,会让人不知所措。有...

  • 行动
  • 减速器
  • Redux存储
  • 连接到React
  • 结合还原器
  • 中间件
  • 动作创建者
  • 选择器
  • 生成器/观测器

然而,所有这些都取决于Redux的新人如何构建他们的学习经验。当你把这篇文章恢复到最开始的时候,人们可以看到Redux的核心内容只有以下几点。

Action => Reducer(s) => Store

这就是它的全部内容。Redux就是保持简单,愚蠢。没有隐藏的魔法,99%是用动作和还原器表达的纯JavaScript。只有Redux的存储API提供了一个小的表面区域,用于...

// dispatching actionsstore.dispatch(myAction);
// subscribing to state updatesstore.subscribe(() => {  // do something, e.g. re-render UI});
// getting the statestore.getState();

Redux没有更多的东西。KISS也应该应用于学习Redux。从它的核心概念开始,不要担心选择器、sagas和React的问题。一旦你感到舒适,就从那里开始前进。如果你觉得负担太重,就不要在上面扔太多东西。

毕竟,KISS是每个使用Redux的人的重要一课。如果一个人决定创建自己的库,那么KISS。如果一个人决定建立一个React组件,那么KISS。如果一个人决定开放一个API,那么KISS。毕竟,这是使Redux流行的原因。它只解决了一个问题,但它解决得非常好。

不要吹嘘;要谦虚

每个关注Redux背后的创造者和团队的人都可以看到他们是超级谦虚的。丹-阿布拉莫夫(Dan Abramov)有一篇博文建议,的应用程序可能不需要Redux。所有这些库背后的人都是JavaScript社区的伟大榜样。

我认为在非技术层面上,每个人都可以从这些人格特质中学习到一些东西。当有人问你的时候,给出有用的建议。不要吹嘘你的东西。考虑其他人的意见。不要把你最喜欢的框架扔到另一个人的脸上。我们都是普通人,所以让我们互相支持,以便在JavaScript中开发出令人惊奇的东西!

Redux让人成为更好的JavaScript开发者

考虑到前面所有的观点,我相信Redux让每个人都成为更好的JavaScript开发者。人们开始用函数来思考,用高阶函数或可组合函数或简明函数来思考,在他们的应用中考虑不可变的数据结构、纯函数和域,并通过追随他们的榜样,在为生态系统做贡献时保持在巨人的肩膀上。也许这种不吹牛和谦虚的态度也会影响到一个或另一个人 :-)总的来说,它使每个人都成为更好的开发者。

我相信Redux的遗产在很大程度上是通过时机影响的。有很多人*"只 "*知道JavaScript是他们的编程语言,也许最近才开始把它作为他们的第一门语言,而从来没有被介绍过更广泛的编程概念和技术,如不可变性、函数式编程、领域驱动设计或生成器。在Redux的帮助下,他们学到了很多关于所有这些东西的知识。即使Redux在未来可能会消失,我也会建议每个刚接触JavaScript的人学习它,只是为了学习它带来的所有好处。