【问题标题】:Best way to hide 10000 dropdown menus隐藏 10000 个下拉菜单的最佳方法
【发布时间】:2017-01-12 04:47:40
【问题描述】:

上下文 -

我有一个聊天组件,每个单独的聊天消息都有一个下拉列表。

然后单击“更多选项图标”(3 个点)打开下拉菜单。

每条单独的聊天消息都是一个“主干项目视图”

一种解决方案是点击“body”,循环浏览所有菜单,然后通过删除其中的一个类来关闭下拉菜单。

$("body").on("click", function() {
  $(".drop-down-menu").each(function(idx, item) {
      $(item).removeClass("open"); // open class indicated it is open via CSS
  });
});

CSS -

.drop-down-menu {
    visibility: hidden;
    opacity: 0;
    &.open {
       opacity: 1;
       visibility: visible;
    }
}

如果有 10,000 条或更多消息,是否会对性能产生影响?

因此,如果用户单击屏幕上的任何位置,我正在寻找隐藏下拉列表的最佳解决方案。 谢谢。

【问题讨论】:

  • 10,000 条消息无法同时显示在屏幕上。当它们远离视口时,将它们从 DOM 中移除。
  • @Jordan 抱歉,现在我们无法将它们从 DOM 中删除并重新放入。需要解决方案将消息放在 DOM 中
  • 在打开新的下拉菜单时可以关闭之前的下拉菜单吗?
  • 10000 个元素太多了,不能指望对它们进行的 DOM 操作既便宜又快速。任何决定都意味着循环、隐藏或显式。 Backbone 功能强大、灵活且不拘一格,足以允许重新设计以动态创建、渲染和删除这些下拉菜单。
  • 单击 (...) 我添加了一个“打开”类并阻止了默认 & 在正文上附加了一个单击侦听器,该单击侦听器删除了“打开”类

标签: javascript jquery css backbone.js drop-down-menu


【解决方案1】:

您可以进行一些微不足道的更改,以提高代码的性能。第一件事是没有理由像你正在做的那样循环。 jQuery 对象是集合,而 jQuery 操作通常会遍历 jQuery 对象的元素。所以:

$("body").on("click", function() {
  $(".drop-down-menu").removeClass("open");
});

这将自动从选择器".drop-down-menu" 匹配的所有元素中删除类open。 jQuery 仍然会在内部循环,但让 jQuery 自己迭代比让.each 调用你自己的回调然后在回调中创建一个新的 jQuery 对象来调用.removeClass 更快。

此外,您从逻辑上知道从没有此类的元素中删除 open 类是没有意义的。因此,您可以将操作范围缩小到仅删除 open 有意义的那些元素:

$("body").on("click", function() {
  $(".drop-down-menu.open").removeClass("open");
});

这些原则应用广泛,实施成本微不足道。除此之外的任何事情都会进入可能有缺点的优化领域,并且应该通过实际分析您的代码来支持。您可以将 jQuery 代码替换为仅使用常用 DOM 调用的代码,但是如果您需要对旧浏览器的支持,那么处理这个问题的成本和这个怪癖可能不值得。如果您使用的是常用的 DOM 方法,则有不同的方法可能会产生不同的性能提升,但会以代码复杂性为代价。

【讨论】:

  • 感谢您的详细回答。但是如果有 10000 个下拉菜单项和你的解决方案 $(".drop-down-menu.open").removeClass("open");仍然会在内部循环。最好的办法是避免循环。仍在考虑/寻找替代方案
  • 您要问的是“可能会缩小尺寸的优化,并且应该通过实际分析您的代码来支持”的领域。其他答案之一建议维护打开菜单的索引。是的,理论上这可以工作。但是,您刚刚增加了代码的复杂性,现在必须维护这个索引。额外的维护代码是否值得性能优势?如果这个我的项目,我不会考虑添加手动维护的索引,直到我知道编写良好的 jQuery 代码(或股票 DOM 等效代码)是不够的。
【解决方案2】:

Louis 使用高效的 jQuery 选择器提供快速修复。

从长远来看,我建议将每条消息都设置为具有ContextMenuView 组件的MessageView 组件。这样,每个视图只有一个菜单需要处理。

捕捉元素外的点击

然后,使用以下ClickOutside 视图作为上下文菜单基本视图。它看起来很复杂,但它只包装了blurfocus DOM 事件,以了解您是否在视图之外单击。

它为视图本身提供了一个简单的onClickOutside 回调和一个在元素上触发的click:outside 事件。

菜单视图现在只需实现以下内容:

var ContextMenuView = ClickOutside.extend({
    toggle: function(val) {
        this.$el.toggleClass("open", val);
        this.focus(); // little requirement
    },
    // here's where the magic happens!
    onClickOutside: function() {
        this.$el.removeClass("open");
    }
});

查看演示

var app = {};

(function() {

  var $body = Backbone.$(document.body);
  /**
   * Backbone view mixin that enables the view to catch simulated
   * "click:outside" events (or simple callback) by tracking the 
   * mouse and focusing the element.
   *
   * Additional information: Since the blur event is triggered on a mouse
   * button pressed and the click is triggered on mouse button released, the
   * blur callback gets called first which then listen for click event on the
   * body to trigger the simulated outside click.
   */
  var ClickOutside = app.ClickOutside = Backbone.View.extend({
    events: {
      "mouseleave": "_onMouseLeave",
      "mouseenter": "_onMouseEnter",
      "blur": "_onBlur",
    },
    /**
     * Overwrite the default constructor to extends events.
     */
    constructor: function() {

      this.mouseInside = false;

      var proto = ClickOutside.prototype;
      this.events = _.extend({}, proto.events, this.events);

      ClickOutside.__super__.constructor.apply(this, arguments);
      this.clickOnceEventName = 'click.once' + this.cid;
    },

    /**
     * Hijack this private method to ensure the element has
     * the tabindex attribute and is ready to be used.
     */
    _setElement: function(el) {
      ClickOutside.__super__._setElement.apply(this, arguments);

      var focusEl = this.focusEl;

      if (focusEl && !this.$focusElem) {
        this.$focusElem = focusEl;
        if (!(focusEl instanceof Backbone.$)) {
          this.$focusElem = Backbone.$(focusEl);
        }
      } else {
        this.$focusElem = this.$el;
      }
      this.$focusElem.attr('tabindex', -1);
    },

    focus: function() {
      this.$focusElem.focus();
    },

    unfocus: function() {
      this.$focusElem.blur();
      $body.off(this.clickOnceEventName);
    },

    isMouseInside: function() {
      return this.mouseInside;
    },

    ////////////////////////////
    // private Event handlers //
    ////////////////////////////
    onClickOutside: _.noop,
    _onClickOutside: function(e) {
      this.onClickOutside(e);
      this.$focusElem.trigger("click:outside", e);
    },

    _onBlur: function(e) {
      var $focusElem = this.$focusElem;
      if (!this.isMouseInside() && $focusElem.is(':visible')) {
        $body.one(this.clickOnceEventName, this._onClickOutside.bind(this));
      } else {
        $focusElem.focus(); // refocus on inside click
      }
    },

    _onMouseEnter: function(e) {
      this.mouseInside = true;
    },
    _onMouseLeave: function(e) {
      this.mouseInside = false;
    },

  });

  var DropdownView = app.Dropdown = ClickOutside.extend({
    toggle: function(val) {
      this.$el.toggle(val);
      this.focus();
    },
    onClickOutside: function() {
      this.$el.hide();
    }
  });


})();


var DemoView = Backbone.View.extend({
  className: "demo-view",
  template: $("#demo-template").html(),
  events: {
    "click .toggle": "onToggleClick",
  },
  initialize: function() {
    this.dropdown = new app.Dropdown();
  },
  render: function() {
    this.$el.html(this.template);
    this.dropdown.setElement(this.$(".dropdown"));
    return this;
  },
  onToggleClick: function() {
    this.dropdown.toggle(true);
  },

});

$("#app")
  .append(new DemoView().render().el)
  .append(new DemoView().render().el);
html,
body {
  height: 100%;
  width: 100%;
}

.demo-view {
  position: relative;
  margin-bottom: 10px;
}

.dropdown {
  z-index: 2;
  position: absolute;
  top: 100%;
  background-color: gray;
  padding: 10px;
  outline: none;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.8.3/underscore-min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/backbone.js/1.3.3/backbone-min.js"></script>

<div id="app"></div>

<script type="text/template" id="demo-template">
  <button type="button" class="toggle">Toggle</button>
  <div class="dropdown" style="display:none;">
    This is a drop down menu.
  </div>
</script>

检测元素外点击的替代方法

如果您不想或不能使用blurfocus 事件,请查看How do I detect a click outside an element? 以了解替代技术。

视图的延迟初始化

另一种提高 SPA 效率的方法是将新视图的创建延迟到您需要它的那一刻。而是创建 10k 上下文菜单视图,等待用户第一次单击切换按钮并创建一个新视图(如果它尚不存在)。

toggleMenu: function(){
    var menuView = this.menuView;
    if (!menuView) {
        menuView = this.menuView = new ContextMenuView();
        this.$('.dropdown').html(menuView.render().el);
    }
    menuView.toggle();
}

分页

在网页中超过了一定的 HTML 阈值,浏览器开始滞后,影响用户体验。与其将 10k 视图转储到 div 中,不如仅显示 100 或覆盖可见空间的最小值。

然后,当滚动到边缘(顶部或底部)时,按需添加或添加新视图。就像任何基于网络的聊天应用程序中的消息列表一样,例如messenger.com

【讨论】:

    【解决方案3】:

    由于您一次只能打开一个下拉菜单,也许您可​​以保留指向它所附加到的元素或元素的索引的指针,而不是遍历所有菜单。

    【讨论】:

    • 您可以使用全局变量(将其命名为“parentOfOpenMenu”或其他名称)来保留当前展开菜单的父级的 dom 元素。如果所有菜单都关闭,则将其设置为 null。然后,您可以在所有涉及打开/关闭菜单的功能中更新parentOfOpenMenu。我不是网络开发人员,所以这就是我能想到的。希望对您有所帮助。
    • 这很有意义。至少可以开始。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2017-08-30
    • 1970-01-01
    • 2018-08-18
    • 2014-03-31
    • 1970-01-01
    • 1970-01-01
    • 2021-07-01
    相关资源
    最近更新 更多