淘汰赛中的比赛条件

Race Conditions in Knockout

我最近 运行 在我的代码中发现了一个竞争条件的小问题。我正在使用 Knockout.Js 来收集一些要显示给用户的信息。

当需要在选择值之前生成下拉列表时出现问题。 通常下拉列表比其他请求小,将赢得比赛,因此不会造成问题。然而,我在我的应用程序中看到了一些情况,在较慢的互联网连接上,列表会在其次加载。下面是一个例子。如果列表中没有该值的选项,则无法选择它,并且对用户来说就好像没有被选中一样。

使用setTimeout我模拟了这种体验。您可以交换这两个值以查看 "success" 场景。

function ViewModel() {
  var self = this;

  self.UserName = ko.observable();
  self.UserGroup = ko.observable();
  self.GroupList = ko.observableArray();

  self.LoadUserGroups = function() {
    //Ajax call to populate user groups 

    setTimeout(function() {
      response = "Red Team,Blue Team,Green Team".split(",");

      self.GroupList(response)
    }, 2000) /// SWAP ME 
  }

  self.LoadUserInformation = function() {
    setTimeout(function() {
      response = {
        UserName: "John Pavek",
        UserGroup: "Blue Team"
      };

      self.UserName(response.UserName);
      self.UserGroup(response.UserGroup);

    }, 1000) // SWAP ME
  }

  self.Load = function() {
    self.LoadUserGroups();
    self.LoadUserInformation();
  }

  self.Load();

}

ko.applyBindings(new ViewModel())
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>

User Name:
<input data-bind="value: UserName" /> User Group:
<select data-bind="options: GroupList, optionsCaption: '--Pick a Team--', value: UserGroup"></select>

我可以在我的代码中添加什么,Vanilla 或 knockout 来防止这个问题的发生而不减慢整个体验?

您别无选择,只能等待 LoadUserInformation 应用信息,直到 LoadUserGroups 获取信息之后。但这根本不会减慢体验。

您需要将加载显示信息分开。在非承诺环境中,我会让每个函数接受一个回调,然后只有在我得到两个结果后才继续(调用 "show" 函数),请参阅 *** 评论:

function ViewModel() {
  var self = this;

  self.UserName = ko.observable();
  self.UserGroup = ko.observable();
  self.GroupList = ko.observableArray();

  // *** Only loads, doesn't show
  self.LoadUserGroups = function(callback) {
    //Ajax call to populate user groups 

    setTimeout(function() {
      callback("Red Team,Blue Team,Green Team".split(","));
    }, 2000);
  }
  
  // *** Shows
  self.ShowUserGroups = function(groups) {
      self.GroupList(groups);
  };

  // *** Only loads, doesn't show
  self.LoadUserInformation = function(callback) {
    setTimeout(function() {
      callback({
        UserName: "John Pavek",
        UserGroup: "Blue Team"
      });
    }, 1000);
  };
  
  // *** Shows
  self.ShowUserInformation = function(info) {
      self.UserName(info.UserName);
      self.UserGroup(info.UserGroup);
  };

  self.Load = function() {
    var groups = null, userInfo = null;
    self.LoadUserGroups(function(g) {
      groups = g;
      checkDone();
    });
    self.LoadUserInformation(function(u) {
      userInfo = u;
      checkDone();
    });
    function checkDone() {
      if (groups && userInfo) {
        // *** We have both, show them
        self.ShowUserGroups(groups);
        self.ShowUserInformation(userInfo);
      }
    }
  }

  self.Load();

}

ko.applyBindings(new ViewModel())
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>

User Name:
<input data-bind="value: UserName" /> User Group:
<select data-bind="options: GroupList, optionsCaption: '--Pick a Team--', value: UserGroup"></select>

当然,从技术上讲,如果群组在用户信息之前到达,没有什么可以阻止您显示这些群组,您只需使用状态标志即可:

function ViewModel() {
  var self = this;

  self.UserName = ko.observable();
  self.UserGroup = ko.observable();
  self.GroupList = ko.observableArray();

  // *** Only loads, doesn't show
  self.LoadUserGroups = function(callback) {
    //Ajax call to populate user groups 

    setTimeout(function() {
      callback("Red Team,Blue Team,Green Team".split(","));
    }, 2000);
  }
  
  // *** Shows
  self.ShowUserGroups = function(groups) {
      self.GroupList(groups);
  };

  // *** Only loads, doesn't show
  self.LoadUserInformation = function(callback) {
    setTimeout(function() {
      callback({
        UserName: "John Pavek",
        UserGroup: "Blue Team"
      });
    }, 1000);
  };
  
  // *** Shows
  self.ShowUserInformation = function(info) {
      self.UserName(info.UserName);
      self.UserGroup(info.UserGroup);
  };

  self.Load = function() {
    var haveGroups = false, userInfo = null;
    self.LoadUserGroups(function(groups) {
      // *** No need to wait for the user info
      self.ShowUserGroups(groups);
      haveGroups = true;
      checkDone();
    });
    self.LoadUserInformation(function(u) {
      userInfo = u;
      checkDone();
    });
    function checkDone() {
      if (haveGroups && userInfo) {
        // *** Show the user info
        self.ShowUserInformation(userInfo);
      }
    }
  }

  self.Load();

}

ko.applyBindings(new ViewModel())
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>

User Name:
<input data-bind="value: UserName" /> User Group:
<select data-bind="options: GroupList, optionsCaption: '--Pick a Team--', value: UserGroup"></select>

在 promise 环境中,我会有 "load" 函数 return promises,然后使用它们看起来像这样而不需要 checkDone 调用:

Promise.all([
    self.LoadUserGroups().then(self.ShowUserGroups),
    self.LoadUserInfo()
]).then(function(results) {
    self.ShowUserInfo(results[1]);
});

...使用 ES2015+ 参数解构语法变得更清晰:

Promise.all([
    self.LoadUserGroups().then(self.ShowUserGroups),
    self.LoadUserInfo()
]).then(function([_, userInfo]) {
    self.ShowUserInfo(userInfo);
});

LoadUserInformation 试图设置的值不在选项列表中时,select 正在覆盖 UserGroup。但是您可以使用 valueAllowUnset 告诉它不要担心未知值。然后它不会覆盖。

function ViewModel() {
  var self = this;

  self.UserName = ko.observable();
  self.UserGroup = ko.observable();
  self.GroupList = ko.observableArray();

  self.LoadUserGroups = function() {
    //Ajax call to populate user groups 

    setTimeout(function() {
      response = "Red Team,Blue Team,Green Team".split(",");

      self.GroupList(response)
    }, 2000) /// SWAP ME 
  }

  self.LoadUserInformation = function() {
    setTimeout(function() {
      response = {
        UserName: "John Pavek",
        UserGroup: "Blue Team"
      };

      self.UserName(response.UserName);
      self.UserGroup(response.UserGroup);

    }, 1000) // SWAP ME
  }

  self.Load = function() {
    self.LoadUserGroups();
    self.LoadUserInformation();
  }

  self.Load();

}

ko.applyBindings(new ViewModel())
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>

User Name:
<input data-bind="value: UserName" /> User Group:
<select data-bind="options: GroupList, optionsCaption: '--Pick a Team--', value: UserGroup, valueAllowUnset: true"></select>