如何用可观察对象定义周期



我正尝试着为一款简单的游戏设置更新循环,并基于可观察性而构建。顶层组件是一个模型,它接受输入命令,并产生更新;还有一个视图,它显示接收到的更新,并产生输入。单独来看,两者都可以正常工作,问题在于将两者结合起来,因为两者都依赖于另一方。

组件被简化为以下内容:

var view = function (updates) {
  return Rx.Observable.fromArray([1,2,3]);
};
var model = function (inputs) {
  return inputs.map(function (i) { return i * 10; });
};

我把这些东西连在一起的方式是这样的:

var inputBuffer = new Rx.Subject();
var updates = model(inputBuffer);
var inputs = view(updates);
updates.subscribe(
    function (i) { console.log(i); },
    function (e) { console.log("Error: " + e); },
    function () { console.log("Completed"); }
);
inputs.subscribe(inputBuffer);

也就是说,我添加一个主题作为输入流的占位符,并将模型附加到该主题上。然后,在构造视图之后,我将实际输入传递给占位符主题,从而结束循环。

然而,我忍不住觉得这不是正确的做事方式。用一个主题来做这件事似乎有点过头了。是否有一种方法可以用publish()或defer()或其他类似的方法来做同样的事情?

UPDATE:这里有一个不太抽象的例子来说明我遇到的问题。下面是一个简单"游戏"的代码,玩家需要点击一个目标来击中它。目标可以出现在左边或右边,当它被击中时,它会切换到另一边。似乎很简单,但我还是觉得我错过了什么…

//-- Helper methods and whatnot
// Variables to easily represent the two states of the target
var left = 'left';
var right = 'right';
// Transition from one side to the other
var flip = function (side) {
  if (side === left) {
    return right;
  } else {
    return left;
  }
};
// Creates a predicate used for hit testing in the view
var nearby = function (target, radius) {
  return function (position) {
    var min = target - radius;
    var max = target + radius;
    return position >= min && position <= max;
  };
};
// Same as Observable.prototype.scan, but it also yields the initial value immediately.
var initScan = function (values, init, updater) {
  var initValue = Rx.Observable.return(init);
  var restValues = values.scan(init, updater);
  return initValue.concat(restValues);
};
//-- Part 1: From input to state --
var process = function (inputs) {
  // Determine new state based on current state and input
  var update = function(current, input) {
    // Input value ignored here because there's only one possible state transition
    return flip(current);
  };
  return initScan(inputs, left, update);
};
//-- Part 2: From display to inputs --
var display = function (states) {
  // Simulate clicks from the user at various positions (only one dimension, for simplicity)
  var clicks = Rx.Observable.interval(800)
      .map(function (v) {return (v * 5) % 30; })
      .do(function (v) { console.log("Shooting at: " + v)})
      .publish();
  clicks.connect();
  // Display position of target depending on the model
  var targetPos = states.map(function (state) {
    return state === left ? 5 : 25;
  });
  // Determine which clicks are hits based on displayed position
  return targetPos.flatMapLatest(function (target) {
    return clicks
        .filter(nearby(target, 10))
        .map(function (pos) { return "HIT! (@ "+ pos +")"; })
        .do(console.log);
  });
};
//-- Part 3: Putting the loop together 
/**
 * Creates the following feedback loop:
 * - Commands are passed to the process function to generate updates.
 * - Updates are passed to the display function to generates further commands.
 * - (this closes the loop)
 */
var feedback = function (process, display) {
  var inputBuffer = new Rx.Subject(),
      updates = process(inputBuffer),
      inputs = display(updates);
  inputs.subscribe(inputBuffer);
};
feedback(process, display);

我想我明白你想要达到的目的。

  • 我怎么能得到一个输入事件序列在一个方向馈送到一个模型
  • 但是有一个从模型到视图的另一个方向的输出事件序列
我相信这里的答案是你可能想要翻转你的设计。假设是MVVM风格的设计,而不是让模型知道输入序列,它就变成了不可知的。这意味着您现在拥有一个模型,该模型具有inputrecireceived/OnInput/ExecuteCommand方法,View将使用输入值调用该方法。现在,处理"一个方向上的命令"one_answers"另一个方向上的事件"模式应该会容易得多。这是对CQRS的一种致敬。

在过去的4年里,我们在WPF/Silverlight/JS中的Views+Models中广泛使用了这种风格。

可能是这样的;

var model = function()
{
    var self = this;
    self.output = //Create observable sequence here
    self.filter = function(input) {
        //peform some command with input here
    };
}
var viewModel = function (model) {
    var self = this;
    self.filterText = ko.observable('');
    self.items = ko.observableArray();
    self.filterText.subscribe(function(newFilterText) {
        model.filter(newFilterText);
    });
    model.output.subscribe(item=>items.push(item));
};

感谢张贴完整的样品。看起来不错。我喜欢你的新initScan操作符,这似乎是Rx中明显的遗漏。

我把你的代码重组成我可能会写的方式。我希望能有所帮助。我所做的主要事情是将逻辑封装到模型中(翻转,附近等),并让视图将模型作为参数。然后我还需要向模型中添加一些成员而不仅仅是一个可观察序列。然而,这确实允许我从视图中删除一些额外的逻辑,并将其放在模型中(点击逻辑)

//-- Helper methods and whatnot
// Same as Observable.prototype.scan, but it also yields the initial value immediately.
var initScan = function (values, init, updater) {
  var initValue = Rx.Observable.return(init);
  var restValues = values.scan(init, updater);
  return initValue.concat(restValues);
};
//-- Part 1: From input to state --
var process = function () {
  var self = this;
  var shots = new Rx.Subject();
  // Variables to easily represent the two states of the target
  var left = 'left';
  var right = 'right';
  // Transition from one side to the other
  var flip = function (side) {
    if (side === left) {
      return right;
    } else {
      return left;
    }
  };
  // Determine new state based on current state and input
  var update = function(current, input) {
    // Input value ignored here because there's only one possible state transition
    return flip(current);
  };
  // Creates a predicate used for hit testing in the view
  var isNearby = function (target, radius) {
    return function (position) {
      var min = target - radius;
      var max = target + radius;
      return position >= min && position <= max;
    };
  };
  self.shoot = function(input) { 
    shots.onNext(input); 
  };
  self.positions = initScan(shots, left, update).map(function (state) {
    return state === left ? 5 : 25;
  });
  self.hits = self.positions.flatMapLatest(function (target) {  
    return shots.filter(isNearby(target, 10));
  });
};
//-- Part 2: From display to inputs --
var display = function (model) {
  // Simulate clicks from the user at various positions (only one dimension, for simplicity)
  var clicks = Rx.Observable.interval(800)
      .map(function (v) {return (v * 5) % 30; })
      .do(function (v) { console.log("Shooting at: " + v)})
      .publish();
  clicks.connect();
  model.hits.subscribe(function(pos)=>{console.log("HIT! (@ "+ pos +")");});
  // Determine which clicks are hits based on displayed position
  model.positions(function (target) {
    return clicks
        .subscribe(pos=>{
          console.log("Shooting at " + pos + ")");
          model.shoot(pos)
        });
  });
};
//-- Part 3: Putting the loop together 
/**
 * Creates the following feedback loop:
 * - Commands are passed to the process function to generate updates.
 * - Updates are passed to the display function to generates further commands.
 * - (this closes the loop)
 */
var feedback = function (process, display) {
  var model = process();
  var view = display(model);
};
feedback(process, display);

我假定,因为您在创建模型后没有"分配"输入,所以您的目标是使用非变化的方法来实例化您的模型和视图。然而,你的模型和视图似乎是相互依赖的。为了解决这个问题,您可以使用第三方来促进两个对象之间的关系。在这种情况下,你可以简单地使用一个函数来进行依赖注入…

var log           = console.log.bind(console),
    logError      = console.log.bind(console, 'Error:'),
    logCompleted  = console.log.bind(console, 'Completed.'),
model(
    function (updates) {
      return view(updates);
    }
  )
  .subscribe(
    log,
    logError,
    logCompleted
    );

通过给模型提供一个工厂来创建视图,你给模型提供了通过实例化它的视图来完全实例化它自己的能力,但是不知道如何实例化视图。

根据我对问题本身的评论,这里是你在Windows中使用调度程序编写的相同类型的代码。我希望在RxJS中也有类似的接口。

var scheduler = new EventLoopScheduler();
var subscription = scheduler.Schedule(
    new int[] { 1, 2, 3 },
    TimeSpan.FromSeconds(1.0),
    (xs, a) => a(
        xs
            .Do(x => Console.WriteLine(x))
            .Select(x => x * 10)
            .ToArray(),
        TimeSpan.FromSeconds(1.0)));
我得到的输出,每秒有三个新数字,是:
1
2
3
10
20
30
100
200
300
1000
2000
3000
10000
20000
30000

相关内容

  • 没有找到相关文章

最新更新