九、高级反应式

现在我们的博客基本完成了,我们可以创建和编辑条目了。 在本章中,我们将使用 Meteor 的响应式模板来更新我们的接口时间戳。 我们将构建一个响应式对象,该对象将重新运行模板帮助器,该帮助器将显示创建博客条目的时间。 这样,它们将始终显示正确的相对时间。

在本章中,我们将涵盖以下主题:

反应式编程

正如我们已经在整本书中看到的,Meteor 使用了一些叫做反应的东西。

开发人员在构建软件应用时必须解决的一个问题是接口中表示的数据的一致性。 大多数现代应用使用一种叫做模型-视图-控制器(MVC)的东西,其中视图的控制器确保它总是代表模型的当前状态。 该模型主要是浏览器内存中的服务器 API 或 JSON 对象。

保持接口一致性的最常见方法如下(courtesy:http://manual.meteor.com):

  • 轮询和 diff:周期性(如每秒一次),获取事物的当前值,查看是否改变,如果改变,执行更新。
  • 事件:当发生变化时,会触发事件。 程序的另一部分(通常称为控制器)负责监听此事件,获取当前值,并在事件触发时执行更新。
  • 绑定:值由实现某些接口的对象表示,例如BindableValue。 然后,使用“bind”方法将两个BindableValues绑定在一起,这样当一个值改变时,另一个值就会自动更新。 有时,作为设置绑定的一部分,可以指定转换函数。 例如,Foo可以通过toUpperCase转换函数与Bar结合。

这些模式很好,但它们仍然需要大量代码来维护所表示的数据的一致性。

另一种模式,虽然还不常用,是反应式编程。 此模式是绑定数据的声明性方式。 这意味着当我们使用活性数据源如Session变量或Mongo.Collection,我们可以肯定,无功函数或模板帮手,使用这些就会重新运行它的价值变化,始终保持接口或计算基于这些值更新。

Meteor 手册给了我们一个例子用例,反应式编程来了:

反应式编程是构建用户界面的完美方法,因为程序员可以表达在特定的变化中应该发生什么,而不是试图在单个内聚代码块中建模所有的交互。 响应变更的范例比显式地建模哪些变更会影响程序的状态更容易理解。

例如,假设我们正在编写一个带有项目列表的 HTML5 应用,用户可以点击一个项目来选择它,或者按 ctrl 键选择多个项目。 我们可能有一个

标签,并希望标签的内容等于当前选中的项目的名称,大写,或者如果多个项目被选中,则为“Multiple selection”。 我们可能会有一组标签,希望每个标签是“选择”,如果这一行对应的项目在选择项的设置,或空字符串。

为了使这个例子在上述模式中发生,我们可以很快地看到它与响应式编程相比有多复杂。

  • 如果我们使用 poll 和 diff, UI 将会出现不可接受的滞后。 用户单击之后,屏幕实际上不会更新,直到下一个轮询周期。 此外,我们必须存储旧的选择集,并将其与新选择集进行差异,这有点麻烦。
  • 如果我们使用事件,我们必须编写一些相当复杂的控制器代码,手动将更改映射到选择或被选择项的名称,映射到 UI 的更新。 例如,当选择发生变化时,我们必须记住更新<h1>标签和(通常)两个受影响的<tr>标签。 而且,当选择发生变化时,我们必须在新选择的项上自动注册一个事件处理程序,以便记住更新<h1>。 构建干净的代码并维护它是很困难的,特别是当 UI 被扩展和重新设计时。
  • 如果我们使用绑定,我们将不得不使用复杂的域特定语言(DSL)来表达变量之间的复杂关系。 DSL 必须包括间接(将<h1>的内容不绑定到任何固定项目的名称,而是绑定到当前选择所指示的项目)、转换(将名称大写)和条件(如果选择了多个项目,则显示一个占位符字符串)。

使用 Meteor 的反应式模板引擎 Blaze,我们可以简单地使用{{#each}}块助手来迭代元素列表,并根据用户交互为每个元素添加一些条件,或者根据物品的属性添加选定的类。

如果用户现在更改了数据,或者来自服务器的数据发生了更改,那么界面将相应地更新自身以表示数据,这为我们节省了大量时间,并避免了不必要的复杂代码。

失效循环

理解反应依赖性的一个关键部分是无效循环。

当我们在响应式函数中使用响应式数据源时,例如Tracker.autorun(function(){…}),响应式数据源本身会看到它在响应式函数中,并将当前函数作为依赖项添加到它的依赖项存储中。

然后,当数据源的值发生变化时,它将使所有依赖函数失效(重新运行),并将它们从依赖存储区中删除。

在响应式函数的重新运行中,它将响应式函数添加回其依赖项存储中,以便它们将在下一次失效(值更改)时再次重新运行。

这是理解反应式概念的关键,我们将在下面的示例中看到。

假设我们有两个Session变量设置为false:

Session.set('first', false);
Session.set('second', false);

此外,我们还有Tracker.autorun()函数,它使用了这两个变量:

Tracker.autorun(function(){
    console.log('Reactive function re-run');
    if(Session.get('first')){
        Session.get('second');
    }
});

我们现在可以调用Session.set('second', true),但响应函数不会重新运行,因为在第一次运行中从未调用过它,因为first会话变量被设置为false

如果我们现在调用Session.set(first, true),函数将重新运行。

此外,如果我们现在设置Session.set('second', false),它也将重新运行,因为在第二次重新运行时,Session.get('second')可以添加这个响应函数作为依赖项。

因为响应式数据源总是会在每次失效时从其存储中删除所有依赖项,并在响应式函数的重新运行时将它们添加回来,所以我们可以设置Session.set(first, false)并尝试将其切换到Session.set('second', true)。 函数将再次运行而不是,因为在这次运行中从未调用Session.get('second')!

一旦我们理解了这一点,我们就可以做出更细粒度的反应式,将反应式更新保持在最低限度。 这个解释的控制台输出类似于下面的截图:

The invalidating cycle

构建一个简单的反应式对象

正如我们所看到的,反应式对象是一个在反应式函数中使用的对象,当其值发生变化时将重新运行该函数。 Meteor 的Session物体是反应式物体的一个例子。

在本章中,我们将构建一个简单的响应对象,该对象将在一定时间间隔内重新运行{{formatTime}}模板助手,以便正确更新所有的相对时间。

Meteor 的反应是可能的,通过Tracker包。 这个包是所有反应式的核心,允许我们跟踪依赖关系,并在需要的时候重新运行它们。

执行以下步骤来构建一个简单的响应性对象:

  1. To get started, let's add the following code to the my-meteor-blog/main.js file:

    if(Meteor.isClient) { ReactiveTimer = new Tracker.Dependency; }

    这将在客户端上创建一个名为ReactiveTimer的变量,并创建一个新的Tracker.Dependency实例。

  2. Below the ReactiveTimer variable, but still inside the if(Meteor.isClient) condition, we will add the following code to rerun all dependencies of our ReactiveTimer object every 10 seconds:

    Meteor.setInterval(function(){ // re-run dependencies every 10s ReactiveTimer.changed(); }, 10000);

    Meteor.setInterval将每 10 秒运行一次函数。

    注释

    Meteor 自带了setIntervalsetTimeout的实现。 尽管它们的工作原理与原生 JavaScript 完全相同,但 Meteor 需要这些参数来为服务器端的特定用户引用正确的超时/间隔。

Meteor 自带了setIntervalsetTimeout的实现。 尽管它们的工作原理与原生 JavaScript 完全相同,但 Meteor 需要这些参数来为服务器端的特定用户引用正确的超时/间隔。

在区间内,我们称之为ReactiveTimer.changed()。 这将使每个相关函数失效,导致它重新运行。

重新运行功能

到目前为止,我们还没有创建依赖项,所以让我们这样做。 添加以下代码Meteor.setInterval:

Tracker.autorun(function(){
    ReactiveTimer.depend();
    console.log('Function re-run');
});

如果我们现在回到浏览器控制台,我们应该看到函数每 10 秒重新运行,因为响应对象重新运行该函数。

我们甚至可以在浏览器控制台中调用ReactiveTimer.changed(),函数也会重新运行。

这些都是很好的例子,但是不要让我们的时间戳自动更新。

要做到这一点,我们需要打开my-meteor-blog/client/template-helpers.js,并在formatTimehelper 函数的顶部添加以下行:

ReactiveTimer.depend();

这将使我们的应用中的每个{{formatTime}}helper 每 10 秒重新运行一次,在它经过时更新相对时间。 要查看这一点,请转到浏览器并创建一个新的博客条目。 如果你现在保存博客条目并查看创建的时间文本,你会看到它在一段时间后发生了变化:

Rerunning functions

创建高级定时器对象

前面的示例是一个自定义响应式对象的简单演示。 为了使它更有用,最好创建一个单独的对象,隐藏Tracker.Dependency函数并添加额外的功能。

Meteor 的反应式和依赖性跟踪允许我们创建依赖性,甚至当depend()函数从另一个函数内部调用时。 这个依赖关系链允许更复杂的响应性对象。

在下一个示例中,我们将使用我们的timer对象,并向其添加startstop函数。 此外,我们还将使选择计时器重新运行的时间间隔成为可能:

  1. First, let's remove the previous code examples from the main.js and template-helpers.js files, which we added before, and create a new file named ReactiveTimer.js inside my-meteor-blog/client with the following content:

    ``` ReactiveTimer = (function () {

    // Constructor
    function ReactiveTimer() {
        this._dependency = new Tracker.Dependency;
        this._intervalId = null;
    };
    
    return ReactiveTimer;
    

    })(); ```

    这在 JavaScript 中创建了一个经典的原型类,我们可以使用new ReactiveTimer()实例化它。 在其构造函数中,我们实例化一个new Tracker.Dependency,并将其附加到该函数中。

  2. Now, we will create a start() function, which will start a self-chosen interval:

    ``` ReactiveTimer = (function () {

    // Constructor
    function ReactiveTimer() {
        this._dependency = new Tracker.Dependency;
        this._intervalId = null;
    };
    ReactiveTimer.prototype.start = function(interval){
        var _this = this;
        this._intervalId = Meteor.setInterval(function(){
            // rerun every "interval"
            _this._dependency.changed();
        }, 1000 * interval);
    };
    
    return ReactiveTimer;
    

    })(); ```

    这与我们之前使用的代码相同,不同之处在于我们将 interval ID 存储在this._intervalId中,以便稍后在stop()函数中停止它。 传递给start()函数的间隔必须以秒为单位;

  3. 接下来,我们将添加stop()函数到类中,它将简单地清除间隔:

  4. Now we only need a function that creates the dependencies:

    ReactiveTimer.prototype.tick = function(){ this._dependency.depend(); };

    我们的反应计时器准备好了!

  5. 现在,要实例化timer并以我们喜欢的任何间隔开始它,在文件末尾的ReactiveTimer类之后添加以下代码:

  6. 最后,我们需要回到template-helper.js文件中的{{formatTime}}helper 和add time.tick()函数,界面中的每一个相对时间都会随着时间的推移而更新。
  7. 要查看响应式计时器的运行情况,请在浏览器的控制台中运行以下代码片段:
  8. We should now see Timer ticked! logged every 10 seconds. If we now run time.stop(), the timer will stop running its dependent functions. If we call time.start(2) again, we will see Timer ticked! now appearing every two seconds, as we set the interval to 2:

    Creating an advanced timer object

正如我们所看到的,我们的timer对象现在是相当灵活的,我们可以创建任何数量的时间间隔,以用于整个应用。

活性计算

Meteor 的反应式和Tracker包是一个非常强大的功能,因为它允许类似事件的行为被附加到每个功能和每个模板助手。 这个反应式使我们的界面保持一致。

虽然到目前为止我们只接触了Tracker包,但它还有一些我们应该看一看的属性。

我们已经学习了如何实例化响应式对象。 我们可以调用new Tracker.Dependency,可以使用depend()changed()创建并重新运行依赖项。

停止反应式功能

当我们在一个响应函数中,我们也可以访问当前的计算对象,我们可以使用它来停止进一步的响应行为。

为了看到它的实际效果,我们可以使用我们已经运行的timer,并在浏览器的控制台使用Tracker.autorun()创建以下响应函数:

var count = 0;
var someInnerFunction = function(count){
    console.log('Running for the '+ count +' time');

    if(count === 10)
        Tracker.currentComputation.stop();
};
Tracker.autorun(function(c){
    timer.tick();

    someInnerFunction(count);

    count++;
});

timer.stop();
timer.start(2);

在这里,我们创建了someInnerFunction()来展示如何从嵌套函数访问当前的计算。 在这个内部函数中,我们使用Tracker.currentComputation进行计算,它给出当前的Tracker.Computation对象。

我们使用在Tracker.autorun()函数之前创建的count变量进行计数。 当达到 10 时,我们调用Tracker.currentComputation.stop(),它将停止内部函数和Tracker.autorun()函数的依赖,使它们不具有反应式。

为了更快地看到结果,我们在示例结束时以两秒的间隔停止并启动timer对象。

如果我们复制并粘贴之前的代码片段到浏览器的控制台并运行它,我们应该看到运行 xx 次出现 10 次:

Stopping reactive functions

当前计算对象对我们控制依赖函数内部的响应依赖非常有用。

防止启动时运行

Tracker``.Computation对象还带有firstRun属性,我们在前面的章节中使用过。

例如,使用Tracker.autorun()创建的响应函数在 JavaScript 第一次解析时也会运行。 如果我们想要防止这种情况,我们可以简单地在执行任何代码之前停止函数,检查firstRun是否为true:

Tracker.autorun(function(c){
    timer.tick();

    if(c.firstRun)
        return;

    // Do some other stuff
});

注释

我们不需要使用Tracker.currentComputation来获取当前的计算,因为Tracker.autorun()已经将其作为它的第一个参数。

同样,当我们停止一个Tracker.autorun()函数时,正如下面的代码所描述的,它将永远不会为会话变量创建依赖关系,因为Session.get()在第一次运行时从未被调用:

Tracker.autorun(function(c){
  if(c.firstRun)
    return;

  Session.get('myValue');
}):

为了确保我们使函数依赖于myValue会话变量,我们需要将它放在return语句之前。

高级反应对象

Tracker包有一些更高级的属性和函数,允许您控制依赖项何时失效(Tracker.flush()Tracker.Computation.invalidate()),并允许您在其上注册额外的回调(Tracker.onInvalidate())。

这些属性允许您构建复杂的响应性对象,这超出了本书的范围。 如果你想对Tracker包有更深入的了解,我建议你看看 Meteor 手册http://manual.meteor.com/#tracker

小结

在本章中,我们学习了如何构建我们自己的自定义响应对象。 我们学习了Tracker.Dependency.depend()Tracker.Dependency.changed(),并看到了响应依赖是如何有自己的计算对象的,这些计算对象可以用来停止其响应行为,并防止在启动时运行。

为了更深入地挖掘,请查看Tracker包的文档,并在以下资源中查看Tracker.Computation对象的详细属性描述:

你可以在https://www.packtpub.com/books/content/support/17713或在 GitHub 上https://github.com/frozeman/book-building-single-page-web-apps-with-meteor/tree/chapter9找到本章的代码示例。

现在我们已经完成了我们的博客,我们将在下一章中看看如何在服务器上部署我们的应用。