Goto maarten hus header

后 MVC 时代

长期以来,模型 视图 控制器(MVC)设计模式是构建应用程序的黄金法则。可如今,在 JavaScript 的宇宙中,MVC 的光辉正逐渐黯淡。Angular 2.0,React 和 Ember 等库和框架正在拥抱一个以组件为中心的新的设计模式。一个组件把 模型,视图和控制器封装成一个独立的实体,这几乎完全违反了 MVC 的法则。诸如 Flux 和 Reactive Programming 的思潮颠覆了我们对应用程序状态的思考方式。它们从根本上就和 MVC 的原则分道扬镳了,重新引入了实体通信,状态变迁,状态维护的思想。在本次讲座的最后,你将会了解 Flux 和 Reactive Programming,以及它们和 MVC 的区别。


介绍

我的名字叫做 Maarten ,我是一家名为 42 的荷兰公司的开发人员。我们主要从事 Java 和 Angular 应用程序的开发。我演讲的名字叫做 “后 MVC 时代” 。我将介绍 component 架构,一种新的构建应用程序的方法。

这里是 Google Maps 组件的示例。你可以看到这里有四个城市:旧金山,阿姆斯特丹,纽约,东京。这些是 Google Maps 的地点,城市中心都有一个地图标记。当你把鼠标悬停在它上面的时侯,你会看到这四个城市的相关信息。 Google Map 背后的代码相对很简单。


<google-map latitude="37.77493"
            longitude="-122.41942"
            fit-to-markers>
  <google-map-marker
      latitude="37.779"
      longitude="-122.3892"
      draggable="true"
      title="Go Giants!">
  </google-map-marker>
</google-map>

你可以在 Google Polymer Framework 中创建自定义的 HTML 地址。在这种情况下,这里就会有一个 google-map 元素和一个 google-map-marker 元素。你可以增加自定义的元素并编写代码(在这种情况下,经度和纬度决定了地图的中心位置)。为了添加一个 google-map-marker ,我们将简单地设置经度和纬度,并将其设置为 draggable。这样, Google Map 就完成了。

我们也可以监听以下的事件:

var map = document.querySelector('google-map');

map.addEventListener('google-map-click',   
  function(e) {
    alert('The user clicked on the Map!');
  }
);

我们可以通过其属性配置输入数据和代码。我们也可以通过添加事件监听器,来监听输出的内容。这就是 Polymer 的设计思想。

组件特性

组件具有 隔离性。你可以将多个组件,同一组件的多个实例放在同一个屏幕上,它们之间不会彼此干扰。在上面的示例中,我们有四个 Google Maps 的组件,它们可以分开操作,而不会彼此干扰。

组件具有 非常清晰的输入和输出语义。操作组件的唯一方法是使用它的输入方法。组件操纵外部世界的唯一方法是清楚地定义输出语义。在这个例子中,输入是经度和纬度,输出是事件监听器。

组件具有 描述性。我们看到 Google Map 组件的行为是描述性的。通过阅读代码很容易就能知道组件在做什么。

组件具有 组合性。你可以使用多个较小的组件,例如,一个按钮组件和一个输入组件,把它们组合成一个搜索组件。组件就像 LEGO 模块,用其他的组件来定义新的组件。这是一个非常强大的机制。

这些特性使得组件非常容易理解。因为组件是相互隔离的,当你在页面/代码上看到它时,你知道该组件将要做什么。并且因为组件具有非常清楚的输入和输出语义(即它不会操纵外部世界),所以很容易理解它在应用程序中的作用。

结论:组件是非常厉害的。

使用组件构建应用程序

我使用 Angular 2.0 创建了一个非常简单的 “Todo” 应用程序 (你可以整理 Todo,你也可以把他们都划掉)。在应用底部有一个过滤器,你可以通过点击它来切换组件的状态,查看你需要的 Todo 的状态。例如,如果你点击活跃,你就能看到你需要处理的 Todo,当你点击完成,你只会看到你已经完成的 Todos。

这个应用程序是由以下组件构建的。我把它描述成一棵树 (这是基于组件的应用程序的一个有趣的东西,它是一个树,就像网页上的普通的 DOM 元素一样)。 最顶端的是 TodoApplication,称为 “root component”。它直接或间接地是所有其他组件的祖父母。这也是组合性的一个例子。整个应用程序就是一个应用程序组件。

在右边 (见视频),我们可以看到这个 TodoList 组件,它有 N 个屏幕上可见的 Todos。 还有一个 AddTodo 组件,它是一个输入元素,当你点击 Enter 的时候,它将添加一个新的 Todo。最后是 ToggleAll 组件。当你点击它,你就能立即划掉你所有的 Todos.

在左侧,我们有 Filters 组件,它有三个 Filter 子组件:所有的,正在进行的和已完成的。

如果你仔细看看这个树 (或者任何基于组件的应用程序树),你可能会想:

  1. 应用程序的状态该存在哪里呢?(例如,所有 “Todo” 存储在哪里,“Filter” 当前处于哪种状态)?
  2. 所有这些组件之间如何通信呢?

关于 1, 与其说 “状态存储在哪里?当前 Filter 的状态在哪里?”,我们不如说:”让我们把当前有效的 Filter 放到 Filters 组件中” 因为这就是它们具备共性的地方。它们有相同的父组件。

但是,你之后会意识到在 TodoList中,我们使用了活动的 Filter。在该组件中,我们需要确定要应用哪个 Filter。这样你可能会想将它放在 TodoApplication 的根组件中,因为这是这两个组件共同的父级组件。我把它放在 TodoApplication中,最顶端的组件。

Receive news and updates from Realm straight to your inbox

但这个答案并不是非常令人满意。你可以找到把它放到这三个组件中任何一个里面的理由,这些理由都是有道理的(这不是一个非常令人满意的答案)。在我后面的演讲中,我会告诉你一种解决方法。但是现在呢,我们不太清楚状态应该放在一个应用程序树的哪个位置。

对于 2, “组件如何通信?”,我们可以问自己: “Filter 如何改变?” 我选择把 Filter 放在顶部,在 TodoApplication里面。当你以某种方式单击完成按钮,所有按钮,或者活动按钮时,我需要把这个消息一直传到顶部,因为这是存储数据的位置。点击完成后,我需要将其发送给其父组件。当 Filters 接收到时,它需要将其向上发送到 TodoApplication

因为组件不能和与它不直接相关的其他组件通信。你只能从孩子到父亲,从父亲到孩子。这意味着 组件之间的通信是一个麻烦。你和目标组件之间的距离越远 (例如,你想从蓝色的组件发出消息,一路到红色组件),你首先需要一直到顶部,然后再一直朝右。

这两个问题突出了这个架构的两个缺点。状态不清楚,组件之间的通信不直接相关。我们可以通过使用 __Redux__来减轻这些弱点的影响。

Redux

Redux 是 Dan Abramov 创建的一个库,被称为 _Flux__最受欢迎的架构。 Flux 是 Facebook 发明的,它是一种实现单向数据流的方法。 Flux 更是一种 *思路*,Redux 是这个思路的一个 *实现* (_因为它是最受欢迎的,我会着重讨论 Redux)。

在 Flux 和 Redux 背后,我们仍然有我们的组件树,但是我们将在该树旁,放置了另外一个概念,称为 store。Store 向组件树提供所有状态。我们不再将我们的状态存储在组件中;我们将该应用程序的每一个状态都存储在 Store 的独立变量中。

每当操作发生时 (例如,我们添加一个新的 Todo,检查一个 Todo,切换所有的 Todo,或者检查 Filter),一个操作被发送到 Store。这会改变 Store,然后我们再次将状态发送到组件树。如果我们稍微看一下 (见视频),你就能看到消息流来自哪里。

该 Store 向组件树提供一个状态,组件触发操作,操作改变 Store。这样的循环总是在一个方向上。 这就是 单向 的原因。

Redux 和 Flux 很好地回答了我们的问题:

  1. 所有的状态都只能存在 Store 里 我们不再需要担心在组件树中存储状态了。
  2. 所有的事件路线指向 Store 我们不再需要直接在两个组件之间通信。我们只需将所有事件发送到 Store,然后处理状态。

我们来看一下采用 Redux 的 counter 应用程序的代码。这里有两个按钮: - 和 + ,以及它们之间的数字。当点击 + 时,数字增加。当点击 - ,数字减少。很简单。

在 Redux 中,通常将所有的操作都定义为 const


 // Defines all possible actions as const's.
const INCREMENT_COUNTER = "INCREMENT_COUNTER";
const DECREMENT_COUNTER = "DECREMENT_COUNTER";

/*
   Action creator functions,
   functions that create Redux actions
*/
function incrementCounter() {
  return {
    type: INCREMENT_COUNTER
  };
}

function decrementCounter() {
  return {
    type: DECREMENT_COUNTER
  };
}

我们有两个操作, INCREMENT_COUNTERDECREMENT_COUNTER。 我们还有两个函数 incrementCounterdecrementCounter。这些函数被 Redux 称作 action 创建者函数。它们创建可以发送到 Store 的操作。

操作始终是一个具有 type 属性的对象。而且 type 属性标识操作。在这种情况下,每当我们调用 incrementCounter 函数时,就会得到一个简单的对象,这个对象说明该操作是什么类型的。

接下来,我们创建一个 store。首先我们将 initialCount 把它设置为零。然后我们说 Redux.createStore。第二个参数是 initialCount,第一个参数是这个 counterApp 函数。


 // The initial count starts at zero.
const initialCount = 0;

// Create the store with the reducer and the initial state.
let store = Redux.createStore(counterApp, initialCount);

// The reducer for the counter application.
function counterApp(state, action) {
  switch(action.type) {
    case INCREMENT_COUNTER:
      return state + 1;
    case DECREMENT_COUNTER:
      return state - 1;
  }

  return state;
};

counterApp 函数被称为 reducer。 reducer 是一个描述旧状态和旧状态上相关操作的函数。reducer 需要遵循下面的规则:

  1. reducer 必须是干净的,我们永远不能直接操纵旧状态。而是需要创建一个新的副本。
  2. 每当遇到你不知道的操作的时候,你只需返回状态。

在 reducer 中,我们处理我们的两个操作类型:INCREMENT_COUNTERDECREMENT_COUNTER。当我们增加时,增加一个数;当我们减少时,减少一个数。例如,如果我们的 counter 状态是五,我们发送增加操作,那将是五加一:六是新的状态。这是一个非常简单的 Redux 应用程序。

Redux 是框架无关的。你可以在 React,Angular 或任何其他类型的框架中使用 Redux。但它更适用于 React。这是一个非常简单的应用程序,其中状态是一个整数。我想把它变得复杂些,比如增加动态添加计数和动态删除的功能。我想先做 removeCounter

我们来看看初始状态。


 // Initially there will be one counter
const initialState = {
  nextCounterId: 1,
  counters: { 1: { count: 42, counter: 1 } }
};

function removeCounter(counter) {
  return {
    type: REMOVE_COUNTER,
    counter: counter
  }
}

// This case is inside the Reducer
case REMOVE_COUNTER:
  var nextState = Object.assign({}, state); // Copy state
  delete nextState.counters[action.counter];
  return nextState;

这次不是数字,是一个对象。此对象具有两个属性。

首先,一个 nextCounterId,你可以将其视为 SQL 表上的自动增量 ID。这意味着每增加一个新的计数器,我们都有唯一的键值。

第二个属性称为 counters,一个键值对,其中键是计数器的 ID,它指向计数器对象。在这种情况下,我们有一个 counter 对象,从42开始。

然后我们来看 removeCounter 的操作创建者。但是这次不一样:它有一个参数,counter。那就是我们要删除的 counter。

当有这些操作时,你总会有一个 type 属性,你也可以根据需要添加任何其他属性。在这种情况下,它就是 counter。在 reducer 里面有 REMOVE_COUNTER。而且我用 Object.assign 这个小技巧来创建一个当前状态的副本。记住,你不能操纵现有的状态。然后我们从操作中移除 counter,我们通过 Map 删除键值对。然后我们返回状态,这样我们就删除了 counter。如果你调用 removeCounter ,那么 count 42 将被删除。我们有 createCounter,它没有参数,因为我们不知道我们预先创建的是哪个 counter。


// Initially there will be one counter
const initialState = {
  nextCounterId: 1,
  counters: { 1: { count: 42, counter: 1 } }
};

function createCounter() {
  return {
    type: CREATE_COUNTER
  };
}

// This case is inside the Reducer
case CREATE_COUNTER:
  var nextState = Object.assign({}, state); // Copy state
  nextState.nextCounterId = state.nextCounterId + 1;
  nextState.counters[state.nextCounterId] = {
count: 0,
     counter: state.nextCounterId
  };
  return nextState;

在 reducer 中,我们再次创建一个副本,然后手动增加 nextCounterId ,这样下一次我们就有一个唯一的ID。我们简单地取得之前的 ID,并将其分配给一个新的计数器对象,它从零开始计数。它知道它是哪个计数器,因为它有存储的 counter 的键。

下面是 incrementCounter


// Initially there will be one counter
const initialState = {
  nextCounterId: 1,
  counters: { 1: { count: 42, counter: 1 } }
};

function incrementCounter(counter) {
  return {
    type: INCREMENT_COUNTER,
    counter: counter
  };
}

// This case is inside the Reducer
case INCREMENT_COUNTER:
  var nextState = Object.assign({}, state); // Copy state
  nextState.counters[action.counter].count += 1;
  return nextState;

你还需要在操作创建者中添加 counter ID,这样我们才可以确定需要增加哪个 counter。再强调一次,我们复制状态,我们从初始状态抽取 counter,并将其加一。减少的过程也类似,除了它将其减一。

我们在这里看到的是一个更复杂的 Redux 应用程序的例子。把你的状态放在这些对象里,这个例子是 initialState,这是惯例。你可能会认为,”如果我们有很多操作,那么 switch 语句将会变得巨大”…… 这是真的。但是我们可以为同一个应用程序创建多个 reducer。我们可以根据需要操作实体的领域来进行拆分。

Redux 的优点

Redux 的优点:对状态置于何处和如何与组件进行通信等问题有了明确的答案。

此外,Redux 使 Universal JavaScript 更容易。Universal JavaScript 是将你的应用程序渲染在 Node.js 服务器上的技术,并将其发送回给浏览器,这使你的应用程序渲染速度更快。

这在 Redux 中更容易,因为所有的状态都位于单个变量中。你只需要将该变量设置一次,然后将其赋给组件树。得到你的 UI,就可以把它发回给浏览器了。如果我们将状态放在多个单独的组件中,将需要做更多的工作来获取这些组件,并正确分配状态。必须写更多的 Boilerplate 代码。这是第一个好处。

Redux 还有一个令人惊讶的 很好的开发者体验。这是另一个 counter 的例子,但这次开发工具显示在右边。(见视频) 我们在这里看到的是,每次我们执行一个操作,操作发生在右边,黑色的条。我们甚至可以点击这些行为,假装它们从来没有发生过。

你还可以提交状态,从 JSON 中加载外部状态。如果你的客户说:”我在应用程序中发现了一个错误” 你可以要求他们发送当前状态给你,然后你可以将其加载到本地开发环境中,并确切地看到客户所看到的内容。

Redux 的限制

有一个__学习曲线__。你必须学习 reducer,虚函数,互斥性,thunk,sage。这是一个广泛运用的框架:不是很难学习,但有一个小小的学习曲线。

JavaScript 本质上不是 immutable 的。 JavaScript 允许你更改变量。如果不小心,你在 reducer 上做了一些事情意外地改变了状态,那么你的整个应用程序将会崩溃,因为 Redux 不会接受你的状态变化。编写 reducer 时必须遵守规定,以确保不会意外改变状态。

幸运的是,有一些库,如Facebook 的 Immutable.js ,它给你一个不可变的 collection 库。在这种情况下,我们有 maplist,它们表示对象和数组。每当你执行一个操作,或者 Map, 或者 List,你总会得到一个新的副本。你永远不会修改现有的对象。你可以使用 Immutable.js 来减轻这个弱点的影响。

但是使用这个库会产生一些开销。如果你必须与接收正常对象的某些第三方库进行通信,那么你将不得不做一些管道,来传给第三方库正常的对象,而不是 Immutable Maps 或 Immutable Lists。

响应式编程

你可以使用响应式编程来增加组件的异步事件处理能力。这真的很强大。

什么是响应式编程?在 “正常编程” 或 被动编程,当有两个组件时,一个组件负责另一个组件的操作。


Car.prototype.turnKey = function() {
  this.engine.fireUp();
}

在这种情况下,我们有一个 Car,我们有一个 engine,每当车钥匙转动时,引擎都会起动。从某种意义上说,engine 组件不能控制自己的命运。它总是被告知什么时候开始。Car 对象有一个 engine 属性,并且当钥匙被转动时它会引发引擎。

响应式编程反转了这种关系:


Engine.listenToKey = function(car) {
  car.onKeyTurned(() => {
    this.fireUp();
  });
}

现在,Engine 知道什么时候启动。而 car 不知道,它通过回调机制来操纵 Engine.每当 car.onKeyTurned 事件触发时 engine 将自动启动。

这与组件的优势相匹配。如果你反复编写组件,你的组件可以控制自己的命运,你可以通过查看组件源代码就能轻松地了解组件的反应。当它被触发,操纵或执行某些操作时,它会在源代码中告诉你。

RxJS

回调是非常糟糕的机制。它们比较底层。有一些库,例如 RxJS,它会强化这种回调机制,使其更加强大。

RxJS 来自一个名为 __ReactiveX__的家族库,它是一个协议(所有操作,功能,方法的描述)。它有多种不同语言的实现:C#,RxJS,JavaScript,Scilab,Java等。通过学习一套操作和规则,你可以将该知识移植到各种其他语言中去。

RxJS 背后的主要概念是什么?

  1. Observable: 可被监听的对象。例如,键盘点击,鼠标事件,Ajax 请求,WebSockets 通过线路发送的数据。任何你可以监听的东西,你可以把它变成一个 RxJS 的 Observable。
  2. Operations: 操纵 Observable。你可以对 observable 执行操作,并获取新的 observable。

为了演示 Operations 和 Observable 如何工作,我创建了这个伪代码示例。 (见幻灯片) 有一个 Observable 叫做 A,它是一个字符串数组,字符串形式的数字 “1” 到 “10”。

我们可以对该字符串进行 Operations,在这个 Observable 上,例如 toNumbertoNumber 取字符串数字,并将其变成一个整数。响应式的部分是每当 Observable 的 A 发生变化时 (例如,如果我们在末尾添加字符串数字 “2” ),Observable 的 B 将被更新,我们将在最后添加数字 2 。它对 Observable 的 A 的变化做出了响应。

我们可以做更多的 Operations,例如,做一个 Even Operations,它将该集合过滤到只包含偶数。再说一次,它是响应式的:如果 A 改变,B 将得到更新;如果 B 更新,C 将被更新。而且我们可以做一个最后的操作叫做 Aggregate,在这种情况下,这些偶数被相加起来。

响应式编程的优点在于你定义 D,而且知道它在输入变化时会更新。你不必手动执行。如果我们在 observable A 中添加字符串数字 “2” ,则 B 将在结尾处添加数字 2 ,然后 C 将得到 2 ,因为它是一个偶数,最后聚合为 32

案例分析

我想通过研究一个案例,即自动完成的例子,将伪代码转移到真正的 RxJS 代码。

你在搜索框中键入一个查询,将其发送到后端,后端将会为你提供查询建议。(这里我们搜索 “American”,它会给你一些以 “American” 为开头的提示。) 自动完成想要做到完全正确是非常困难的。

首先,我想展示一个自动完成的不完美的例子,然后我想使用 RxJS 逐一改进它。


var $input = $('#search-input');

Rx.Observable.fromEvent($input, 'keyup')
  .map(e => e.target.value) // Project the text from the input
  .map(search);             // Search does an 'ajax' request
  .subscribe(function(data) {
    // Update the ui here with the data
  }, function(error) {
    // Update the ui here saying what the error was.
});

我们需要做的第一件事就是获取一个 input 元素的引用 (这里就是你可以在其中输入的查询元素)。然后我们加入些 RxJS 神奇的地方:我们为每次输入元素的 “keyup” 事件创建一个 observable。每当用户开始打字时,observable 都会触发,我们会收到事件。但是我们得到的是 JavaScript 事件,它将告诉你哪个键是活动的,如果按下的是 Shift 键,它会告诉你输入的键码。

但是我们对此并不感兴趣。我们执行一个 map 操作来获取目标的值,这是触发事件时,当前输入元素的实际内容。然后我们再做一个 map 操作,做的事情与我们对数组的 ES5 映射的操作完全相同。它对输入执行了一个函数。

然后,我们将其提供给搜索函数,该函数对后端执行 Ajax 请求。后端将给我们一些自动完成的结果。

在底部,我们看到 subscribe function 函数,这是 observable 的起点。每当我们订阅对象发生变化时,该订阅的 operator 将被执行,并且订阅的 operator 有两个函数作为参数。

第一个功能是正常处理,比如后端为我们提供了自动完成的结果。第二个函数,error,每当发生错误时都会调用它。比如后端不可用或无法为我们提供结果。每当我们自动完成成功时,我们更新 UI; 每当出错时,我们忽略它。

Subscribe 会使你想到 Promises:JavaScript 中的 Promise 只能执行一次,而 Observable 可以执行多次。有时在网络上,你会看到它们被称之为 observables 的 “Promises++” ,因为它们在 Promises 之上有许多额外的功能。在 RxJS 中,从 Promise 创建一个 observable 非常简单。有直接可以调用的函数。

但这个例子是不完美的。有几件事情会导致出错,如果你把这段代码放在产品中。会看到这样的结果:

[2016-06-14 10:00:01] Query: “I” [2016-06-14 10:00:02] Query: “I “ [2016-06-14 10:00:03] Query: “I l” [2016-06-14 10:00:04] Query: “I lo” [2016-06-14 10:00:05] Query: “I lov” [2016-06-14 10:00:06] Query: “I love” [2016-06-14 10:00:07] Query: “I love “ [2016-06-14 10:00:08] Query: “I love G” [2016-06-14 10:00:09] Query: “I love GO” [2016-06-14 10:00:10] Query: “I love GOT” [2016-06-14 10:00:11] Query: “I love GOTO”

你在这里看到的是我们发送给后端的时间和一些查询的内容。每次你在查询框里输入的时候,比如,”I love GOTO” ,我们将查询的所有排列都发送到了后端。我们创建了一个我们自己服务器的垃圾前端。

我们想要做的只是在用户停止打字一段时间后,再将查询发送到后端。有一个叫做 debounce 的操作符。 这种效果在RxJS 和 ReactiveX 系列框架里称之为 “marble diagram”. (参见幻灯片)

marble diagram 显示了一个输入流,中间的操作 (the bounce),以及如果应用了该运算符,产生的流是什么。这里,最上面的流有 1-6 作为事件。但是 2-5 是在一起的。这样,所得到的流将过滤掉 2,3 和 4,它只会保留 5。

我们可以使用这个运算符来限制我们发送查询的数量,只需添加这个 debounce 运算符。然后我们说 500,这意味着如果用户停止输入半秒钟,那么我们会将其发送到后端。这样我们就可以防止我们发送垃圾查询了。


var $input = $('#search-input');

Rx.Observable.fromEvent($input, 'keyup')
  .map(e => e.target.value) // Project the text from the input
  .debounce(500) // Wait for 500 milliseconds after the last event.
  .map(search) // Search does an 'ajax' request
  .subscribe(function(data) {
    // Update the ui here with the data
  }, function(error) {
    // Update the ui here saying what the error was.
  })

但是还有另一个问题。如果你的查询字符很少,你通常会收到很多与该用户无关的结果。

Query: “G” -> 100000 bad results Query: “Go” -> 10000 bad results Query: “Got” -> 1000 bad results Query: “Goto” -> 10 good results

如果我们的查询长于三个字符,我们就能获得很好的结果。如果查询少于这个数,我们想忽略这些查询。还有一个操作符:filter.

filter 接收一个断言函数,一个可以根据根据输入返回 false 的函数。我们可以在我们的例子中使用它,在我们做 debounce 之前简单地调用 filter 。我们接受查询,我们说:”只有当查询长度大于 3 时才允许查询。”


var $input = $('#search-input');

Rx.Observable.fromEvent($input, 'keyup')
  .map(e => e.target.value) // Project the text from the input
  .filter(query => query.length > 3) // Three or more characters
  .debounce(500) // Wait for 500 milliseconds after the last event.
  .map(search) // Search does an 'ajax' request
  .subscribe(function(data) {
    // Update the ui here with the data
  }, function(error) {
    // Update the ui here saying what the error was.
  })

现在我们已经解决了这个问题,但是我们还没有走出困境。

有时,我们会连续发送两次相同的查询,比如用户按 Enter 键,这种情况可能就会发生。如果第一个和第二个查询之间有一个五秒钟的窗口。我们可能在五秒钟内不需要新的结果。

我们想要做的是只在有变化的时候才使用查询。还有一个 operator 可以帮我们实现这个功能。它叫做 distinctUntilChanged


var $input = $('#search-input');

Rx.Observable.fromEvent($input, 'keyup')
  .map(e => e.target.value) // Project the text from the input
  .filter(query => query.length > 3) // Three or more characters
  .debounce(500) // Wait for 500 milliseconds after the last event.
  .distinctUntilChanged(); // Only if the query has changed
  .map(search) // Search does an 'ajax' request
  .subscribe(function(data) {
    // Update the ui here with the data
  }, function(error) {
    // Update the ui here saying what the error was.
  })

这只允许和之前不同的查询事件通过。这是我们以前没有看到过的:具有内部状态的 operator。该 operator 记录了它进行的上一个条目。你可以让 operator 拥有状态。这很强大。

在结果流中,我们只有可选值。我们应用这个运算符 distinctUntilChanged ,现在我们只在查询已经改变了的时候发送查询。

我们还有一个大问题需要解决。而这个问题有些棘手。

假设用户搜索 “Goto Rotterdam” ,但很快意识到 Goto 不在鹿特丹,而在阿姆斯特丹。他将他的查询改为 “Goto Amsterdam”。但现在我们在空中同时有两个异步请求。 “Goto Amsterdam” 的结果可能会在 “Goto Rotterdam”的结果之前显示。在你的查询框中,你将看到 “Goto Amsterdam”,但你会看到 “Goto Rotterdam” 自动完成的结果。

相反的,我们想表达的想法是,我们只想使用最后一个通过 Ajax 请求发送给后端的结果。有一个名为 __flatMapLatest__的运算符。如果你有多个异步请求,则只会使用最后一次请求的结果。我们看到的这个 bug 不再是一个问题。


var $input = $('#search-input');

Rx.Observable.fromEvent($input, 'keyup')
  .map(e => e.target.value) // Project the text from the input
  .filter(query => query.length > 3) // Three or more characters
  .debounce(500) // Wait for 500 milliseconds after the last event.
  .distinctUntilChanged() // Only if the query has changed
  .flatMapLatest(search) // Use only the Resp from the last query.
  .subscribe(function(data) {
    // Update the ui here with the data
  }, function(error) {
    // Update the ui here saying what the error was.
  })

这是完整的例子。它比较小,但它做了很多工作。

通过使用这些标准 operations 并巧妙地组合它们,你将获得一个强大的机制。如果我在没有RxJS的情况下,纯粹用 JavaScript 来完成,我的代码可能会是 500 行长,代码中也会出现错误。站在巨人的肩膀上,RxJS 可以在相对较少的代码行中完成伟大的事情。

RxJS 的优点

生产者和消费者是解耦的。 在这种情况下,生产者是发起初始事件的 “keyup” 事件,消费者是 subscribe。由于 subscribe 有自动完成功能,所以它很高兴; 它不在乎我们在那里进行了多少次 operations,我们不必对消费者或生产者做任何修改,不必修改它们的代码。

Operations 是一个强大的构建块。 你可以将它们以多种有趣的方式组合起来,再次像乐高块一样,你可以获得强大的结果。

RxJS 的缺点

有一个陡峭的学习曲线 (我喜欢称之为”学习悬崖”)。那是因为你必须放弃你的命令式的模式。你习惯于操纵变量,而不是在流中思考,并且学习需要时间。但是一旦这样做了,这是非常有益的,因为你看到你以前通过强制性编程才能解决的问题,现在可以通过 observables 解决得更加干净。正如你从自动完成的例子中看到的那样,它是强大的。

文档是高度概念化和抽象的。 “返回一个observable 的序列,根据 keySelector 和 comparer 的结果,只包含不同的连续元素。” 什么是 keySelector?什么是comparer ?连续元素?有没有人知道这个操作符是什么,这个操作符的描述是什么? 这是 distinctUntilChanged

当我在 RxJS 中编程的时候,我碰到了这个文档,然后我想: “这不是我需要的。” 我在 Stack Overflow上发现一些东西,事实证明我一直是对的,但这个文档并没有帮助。文档里有许多不实际的例子 (例如,1到10的数组,然后对其进行操作)。

建议

如果你的应用程序很小,不要使用 Redux 或 RxJS, 因为这增加了开销,可能不值得。

对于 Todo 应用程序,对于 counter 用程序,使用 RxJS 或 Redux 是过度的。但是,如果你的应用程序开始感觉太大,你思考这个应用,然后突然发现大脑不够用了,你不知道状态是什么样的了,而且新同事也不知道程序是怎么运行的,那么这可能是开始使用 Redux 的好时机。它有一个小小的学习曲线,但它足够小,你可以相对快速地教会团队的新成员。

如果开始感觉太大,请使用 Redux. 这是一个很好的方式来存储你的 state。

当需要协调事件(例如自动完成示例)时,使用异步事件执行更高级别的事件; 组件超负荷,开始使用RxJS。 它有一个非常陡峭的学习曲线。预期新的开发人员会遇到一些问题,因为必须花一些时间来解释它的工作原理,以及如何使用 RxJS。

结语

有很多关于 Redux 和 ReactiveX 的视频。 Redux 的创造者 Egghead.io上有自己的课程。另外,请注意 dontpanic.42.nl 系列。我会开始每周更新。

Q & A

问:如果状态变大,Redux 的性能如何?

Maarten: 它会降级,因为你不能将无限数量的东西保存在内存中。但是,如果你使用分页,或者你只把当前使用的内容放在 Store 中,那么情况会好点。它是常规的 JavaScript,你不能一直使用内存,当然性能会差一点。

Q: 如何处理 Redux 中的内存泄漏?

Maarten: 我不认为泄露会那么容易,因为每个应用程序都有一个 Redux Store。它不容易泄漏,只有一个 Store,当用户关闭它的标签时,该 Store 需要被删除。这样,内存泄漏在 Redux 中并不是一个问题,因为只有一个 Store,当你关闭标签时,它将被清理。

Q: 你可以谈谈 Model-View-Controller 与组件架构的比较吗?

Maarten: 我没有发明组件架构;我被打脸了。我们在 42 使用了 Angular, Angular1.0,Angular2.0,它们进化到了这个新的架构。但是这个趋势已经有一段时间了。

React 是第一个接受组件架构的地方,然后 Angular 也这样做了,然后 Polymer 跟上了。许多框架正在向组件进化。我认为如果你升级到 Angular 2,你几乎没有选择,你会开始使用组件架构。

但是组件和 Model-View-Controller 之间的区别在于组件是视图和控制器的组合。但是它并不试图把它们区分开。模型 - 视图 - 控制器,通常有些演讲会谈到同一视图拥有多个控制器,和同一控制器拥有多个视图。我们都知道这不是真的。

我从来没有为多个视图编写过同一个控制器,也没有为一个控制器写过多个视图。这些组件是一种说法:”我们承认他们已经结婚了,我们不要再假装他们是两个独立的实体。” 迁移的成本并不高。而且 Angular 2 有关于相关的升级指南。

About the content

This talk was delivered live in June 2016 at goto; Amsterdam. The video was transcribed by Realm and is published here with the permission of the conference organizers.

Maarten Hus

Maarten is a developer who knows many programming languages and paradigms: from object oriented programming in Objective-C and Java, to functional programming in Clojure. Maarten has made iOS Apps written in Objective-C and HTML5 Apps written in modern frameworks such as Ember.js and Angular.js

4 design patterns for a RESTless mobile integration »

close