2015-09-02 5 views
3

Мое приложение имеет сортировочный и фильтруемый список и несколько входов и флажков. Проблема возникает, если список содержит более 500 элементов, тогда каждый элемент с пользовательским вводом (флажки, поля ввода, меню) начинает отставать примерно на полсекунды, увеличиваясь с количеством элементов в списке. Сортировка и фильтрация списка выполняется достаточно быстро, но отставание от входных элементов слишком велико.Проблемы с большим количеством элементов в Mithril.js

Вопрос в том, как можно отделить список и элементы ввода?

Вот код списка:

var list = {} 
list.controller = function(args) { 
    var model = args.model; 
    var vm = args.vm; 
    var vmc = args.vmc; 
    var appCtrl = args.appCtrl; 

    this.items = vm.filteredList; 
    this.onContextMenu = vmc.onContextMenu; 

    this.isSelected = function(guid) { 
     return utils.getState(vm.listState, guid, "isSelected"); 
    } 
    this.setSelected = function(guid) { 
     utils.setState(vm.listState, guid, "isSelected", true); 
    } 
    this.toggleSelected = function(guid) { 
     utils.toggleState(vm.listState, guid, "isSelected"); 
    } 
    this.selectAll = function() { 
     utils.setStateBatch(vm.listState, "GUID", "isSelected", true, this.items()); 
    }.bind(this); 
    this.deselectAll = function() { 
     utils.setStateBatch(vm.listState, "GUID", "isSelected", false, this.items()); 
    }.bind(this); 
    this.invertSelection = function() { 
     utils.toggleStateBatch(vm.listState, "GUID", "isSelected", this.items()); 
    }.bind(this); 

    this.id = "201505062224"; 
    this.contextMenuId = "201505062225"; 

    this.initRow = function(item, idx) { 
     if (item.online) { 
      return { 
       id : item.guid, 
       filePath : (item.FilePath + item.FileName).replace(/\\/g, "\\\\"), 
       class : idx % 2 !== 0 ? "online odd" : "online even", 
      } 
     } else { 
      return { 
       class : idx % 2 !== 0 ? "odd" : "even" 
      } 
     } 
    }; 

    // sort helper function 
    this.sorts = function(list) { 
     return { 
      onclick : function(e) { 
       var prop = e.target.getAttribute("data-sort-by") 
       //console.log("100") 
       if (prop) { 
        var first = list[0] 
        if(prop === "selection") { 
         list.sort(function(a, b) { 
          return this.isSelected(b.GUID) - this.isSelected(a.GUID) 
         }.bind(this)); 
        } else { 
         list.sort(function(a, b) { 
          return a[prop] > b[prop] ? 1 : a[prop] < b[prop] ? -1 : 0 
         }) 
        } 
        if (first === list[0]) 
         list.reverse() 
       } 
      }.bind(this) 
     } 
    }; 

    // text inside the table can be selected with the mouse and will be stored for 
    // later retrieval 
    this.getSelected = function() { 
     //console.log(utils.getSelText()); 
     vmc.lastSelectedText(utils.getSelText()); 
    }; 
}; 

list.view = function(ctrl) { 

    var contextMenuSelection = m("div", { 
     id : ctrl.contextMenuId, 
     class : "hide" 
    }, [ 
    m(".menu-item.allow-hover", { 
     onclick : ctrl.selectAll 
    }, "Select all"), 
    m(".menu-item.allow-hover", { 
     onclick : ctrl.deselectAll 
    }, "Deselect all"), 
    m(".menu-item.allow-hover", { 
     onclick : ctrl.invertSelection 
    }, "Invert selection") ]); 

    var table = m("table", ctrl.sorts(ctrl.items()), [ 
    m("tr", [ 
      m("th[data-sort-by=selection]", { 
       oncontextmenu : ctrl.onContextMenu(ctrl.contextMenuId, "context-menu context-menu-bkg", "hide") 
      }, "S"), 
      m("th[data-sort-by=FileName]", "Name"), 
      m("th[data-sort-by=FileSize]", "Size"), 
      m("th[data-sort-by=FilePath]", "Path"), 
      m("th[data-sort-by=MediumName]", "Media") ]), 
    ctrl.items().map(function(item, idx) { 
     return m("tr", ctrl.initRow(item, idx), { 
      key : item.GUID 
     }, 
     [ m("td", [m("input[type=checkbox]", { 
      id : item.GUID, 
      checked : ctrl.isSelected(item.GUID), 
      onclick : function(e) {ctrl.toggleSelected(this.id);} 
     }) ]), 
     m("td", { 
      onmouseup: function(e) {ctrl.getSelected();} 
      }, item.FileName), 
     m("td", utils.numberWithDots(item.FileSize)), 
     m("td", item.FilePath), 
     m("td", item.MediumName) ]) 
    }) ]) 

    return m("div", [contextMenuSelection, table]) 
} 

И это, как список и все остальные компоненты инициализируются из приложений основного вида:

// the main view which assembles all components 
var mainCompView = function(ctrl, args) { 
    // TODO do we really need him there? 
    // add the main controller for this page to the arguments for all 
    // added components 
    var myArgs = args; 
    myArgs.appCtrl = ctrl; 

    // create all needed components 
    var filterComp = m.component(filter, myArgs); 
    var part_filter = m(".row", [ m(".col-md-2", [ filterComp ]) ]); 

    var listComp = m.component(list, myArgs); 
    var part_list = m(".col-md-10", [ listComp ]); 

    var optionsComp = m.component(options, myArgs); 
    var part_options = m(".col-md-10", [ optionsComp ]); 

    var menuComp = m.component(menu, myArgs); 
    var part_menu = m(".menu-0", [ menuComp ]); 

    var outputComp = m.component(output, myArgs); 
    var part_output = m(".col-md-10", [ outputComp ]); 

    var part1 = m("[id='1']", { 
     class : 'optionsContainer' 
    }, "", [ part_options ]); 

    var part2 = m("[id='2']", { 
     class : 'menuContainer' 
    }, "", [ part_menu ]); 

    var part3 = m("[id='3']", { 
     class : 'commandContainer' 
    }, "", [ part_filter ]); 

    var part4 = m("[id='4']", { 
     class : 'outputContainer' 
    }, "", [ part_output ]); 

    var part5 = m("[id='5']", { 
     class : 'listContainer' 
    }, "", [ part_list ]); 

    return [ part1, part2, part3, part4, part5 ]; 
} 

// run 
m.mount(document.body, m.component({ 
    controller : MainCompCtrl, 
    view : mainCompView 
}, { 
    model : modelMain, 
    vm : modelMain.getVM(), 
    vmc : viewModelCommon 
})); 

я начал, чтобы обойти эту проблему путем добавления m.redraw.strategy («none») и m.startComput/endComputation - щелкнуть события, и это решает проблему, но является ли это правильным решением? В качестве примера, если я использую компонент Mithril от третьей стороны вместе с моим компонентом списка, как мне это сделать для внешнего компонента без изменения его кода?

С другой стороны, может ли мой компонент списка использовать что-то вроде флага «Сохранить»? Таким образом, список не перерисовывается по умолчанию, если только это не сказано? Но проблема с сторонним компонентом будет сохраняться.

Я знаю, что есть другие стратегии для решения этой проблемы, такие как разбиение на страницы для списка, но я хотел бы знать, какие лучшие практики со стороны Мифрила.

Спасибо заранее, Стефан

+0

Обратите внимание, что вы всегда можете надежно обеспечить соблюдение 'm.redraw.strategy ('none')' в такте между триггером перерисовывания (т. Е. Обработчиком событий DOM с использованием Mithril) и отложенным перерисованием, даже если компонент, запускающий перерисовку является третьей стороной. – Barney

+0

Вы правы, я могу сделать это для компонента в целом. Но, например, У меня есть компонент фильтра, который работает в списке. Он имеет поле ввода, кнопку запуска и партии, если флажки для параметров фильтра. Если установлен флажок, перерисовать не нужно, только кнопка запуска должна запускать один. Для этого можно сделать стратегию перерисовывания конфигурацией через аргументы для компонента. –

+0

Да, конечно. Я говорю, что 'm.redraw.strategy' работает на протяжении одного глобального' m. redraw' - он ортогонален компонентам. Поэтому, если вложенный (сторонний) компонент содержит флажки с связанными с ними событиями перерисовывания, вы можете создать прослушиватель в конфигурации содержащего (1-го участника) компонента для установки стратегии перерисовывания при каждом нажатии. – Barney

ответ

1

Благодаря комментарию от Барни я нашел решение: отбор окклюзии. Исходный пример можно найти здесь http://jsfiddle.net/7JNUy/1/. Я адаптировал код для своих нужд, особенно необходимо было активировать события прокрутки, чтобы количество перерисовок было достаточно хорошим для плавной прокрутки. Посмотрите на функцию obj.onScroll.

var list = {} 
list.controller = function(args) { 
    var obj = {}; 

    var model = args.model; 
    var vm = args.vm; 
    var vmc = args.vmc; 
    var appCtrl = args.appCtrl; 

    obj.vm = vm; 
    obj.items = vm.filteredList; 
    obj.onContextMenu = vmc.onContextMenu; 

    obj.isSelected = function(guid) { 
     return utils.getState(vm.listState, guid, "isSelected"); 
    } 
    obj.setSelected = function(guid) { 
     utils.setState(vm.listState, guid, "isSelected", true); 
    } 
    obj.toggleSelected = function(guid) { 
     utils.toggleState(vm.listState, guid, "isSelected"); 
     m.redraw.strategy("none"); 
    } 
    obj.selectAll = function() { 
     utils.setStateBatch(vm.listState, "GUID", "isSelected", true, obj.items()); 
    }; 
    obj.deselectAll = function() { 
     utils.setStateBatch(vm.listState, "GUID", "isSelected", false, obj.items()); 
    }; 
    obj.invertSelection = function() { 
     utils.toggleStateBatch(vm.listState, "GUID", "isSelected", obj.items()); 
    }; 

    obj.id = "201505062224"; 
    obj.contextMenuId = "201505062225"; 

    obj.initRow = function(item, idx) { 
     if (item.online) { 
      return { 
       id : item.GUID, 
       filePath : (item.FilePath + item.FileName).replace(/\\/g, "\\\\"), 
       class : idx % 2 !== 0 ? "online odd" : "online even", 
       onclick: console.log(item.GUID) 
      } 
     } else { 
      return { 
       id : item.GUID, 
       // class : idx % 2 !== 0 ? "odd" : "even", 
       onclick: function(e) { obj.selectRow(e, this, item.GUID); 
        m.redraw.strategy("none"); 
        e.stopPropagation(); 
       } 
      } 
     } 
    }; 

    // sort helper function 
    obj.sorts = function(list) { 
     return { 
      onclick : function(e) { 
       var prop = e.target.getAttribute("data-sort-by") 
       // console.log("100") 
       if (prop) { 
        var first = list[0] 
        if(prop === "selection") { 
         list.sort(function(a, b) { 
          return obj.isSelected(b.GUID) - obj.isSelected(a.GUID) 
         }); 
        } else { 
         list.sort(function(a, b) { 
          return a[prop] > b[prop] ? 1 : a[prop] < b[prop] ? -1 : 0 
         }) 
        } 
        if (first === list[0]) 
         list.reverse() 
       } else { 
        e.stopPropagation(); 
        m.redraw.strategy("none"); 
       } 
      } 
     } 
    }; 

    // text inside the table can be selected with the mouse and will be stored 
    // for 
    // later retrieval 
    obj.getSelected = function(e) { 
     // console.log("getSelected"); 
     var sel = utils.getSelText(); 
     if(sel.length != 0) { 
      vmc.lastSelectedText(utils.getSelText()); 
      e.stopPropagation(); 
      // console.log("1000"); 
     } 
     m.redraw.strategy("none"); 
     // console.log("1001"); 
    }; 

    var selectedRow, selectedId; 
    var eventHandlerAdded = false; 

    // Row callback; reset the previously selected row and select the new one 
    obj.selectRow = function (e, row, id) { 
     console.log("selectRow " + id); 
     unSelectRow(); 
     selectedRow = row; 
     selectedId = id; 
     selectedRow.style.background = "#FDFF47"; 
     if(!eventHandlerAdded) { 
      console.log("eventListener added"); 
      document.addEventListener("click", keyHandler, false); 
      document.addEventListener("keypress", keyHandler, false); 
      eventHandlerAdded = true; 
     } 
    }; 

    var unSelectRow = function() { 
     if (selectedRow !== undefined) { 
      selectedRow.removeAttribute("style"); 
      selectedRow = undefined; 
      selectedId = undefined; 
     } 
    }; 

    var keyHandler = function(e) { 
     var num = parseInt(utils.getKeyChar(e), 10); 
     if(constants.RATING_NUMS.indexOf(num) != -1) { 
      console.log("number typed: " + num); 

      // TODO replace with the real table name and the real column name 
      // $___{<request>res:/tables/catalogItem</request>} 
      model.newValue("item_update_values", selectedId, {"Rating": num}); 
      m.redraw.strategy("diff"); 
      m.redraw(); 
     } else if((e.keyCode && (e.keyCode === constants.ESCAPE_KEY)) 
       || e.type === "click") { 
      console.log("eventListener removed"); 
      document.removeEventListener("click", keyHandler, false); 
      document.removeEventListener("keypress", keyHandler, false); 
      eventHandlerAdded = false; 
      unSelectRow(); 
     } 
    }; 

    // window seizes for adjusting lists, tables etc 
    vm.state = { 
     pageY : 0, 
     pageHeight : 400 
    }; 
    vm.scrollWatchUpdateStateId = null; 

    obj.onScroll = function() { 
     return function(e) { 
      console.log("scroll event found"); 
      vm.state.pageY = e.target.scrollTop; 
      m.redraw.strategy("none"); 
      if (!vm.scrollWatchUpdateStateId) { 
       vm.scrollWatchUpdateStateId = setTimeout(function() { 
       // update pages 
       m.redraw(); 
       vm.scrollWatchUpdateStateId = null; 
       }, 50); 
      } 
     } 
    }; 

    // clean up on unload 
    obj.onunload = function() { 
     delete vm.state; 
     delete vm.scrollWatchUpdateStateId; 
    }; 

    return obj; 
}; 

list.view = function(ctrl) { 

    var pageY = ctrl.vm.state.pageY; 
    var pageHeight = ctrl.vm.state.pageHeight; 
    var begin = pageY/41 | 0 
    // Add 2 so that the top and bottom of the page are filled with 
    // next/prev item, not just whitespace if item not in full view 
    var end = begin + (pageHeight/41 | 0 + 2) 
    var offset = pageY % 41 
    var heightCalc = ctrl.items().length * 41; 

    var contextMenuSelection = m("div", { 
     id : ctrl.contextMenuId, 
     class : "hide" 
    }, [ 
    m(".menu-item.allow-hover", { 
     onclick : ctrl.selectAll 
    }, "Select all"), 
    m(".menu-item.allow-hover", { 
     onclick : ctrl.deselectAll 
    }, "Deselect all"), 
    m(".menu-item.allow-hover", { 
     onclick : ctrl.invertSelection 
    }, "Invert selection") ]); 

    var header = m("table.listHeader", ctrl.sorts(ctrl.items()), m("tr", [ 
    m("th.select_col[data-sort-by=selection]", { 
     oncontextmenu : ctrl.onContextMenu(ctrl.contextMenuId, "context-menu context-menu-bkg", "hide") 
    }, "S"), 
    m("th.name_col[data-sort-by=FileName]", "Name"), 
    ${ <request> 
      # add other column headers as configured 
      <identifier>active:jsPreprocess</identifier> 
      <argument name="id">list:table01:header</argument> 
     </request> 
    } ]), contextMenuSelection); 

    var table = m("table", ctrl.items().slice(begin, end).map(function(item, idx) { 
     return m("tr", ctrl.initRow(item, idx), { 
      key : item.GUID 
     }, 
     [ m("td.select_col", [m("input[type=checkbox]", { 
      id : item.GUID, 
      checked : ctrl.isSelected(item.GUID), 
      onclick : function(e) {ctrl.toggleSelected(this.id);} 
     }) ]), 
     m("td.nameT_col", { 
      onmouseup: function(e) {ctrl.getSelected(e);} 
      }, item.FileName), 
     ${ <request> 
       # add other columns as configured 
       <identifier>active:jsPreprocess</identifier> 
       <argument name="id">list:table01:row</argument> 
      </request> 
     } ]) 
    })); 

    var table_container = m("div[id=l04]", 
      {style: {position: "relative", top: pageY + "px"}}, table); 

    var scrollable = m("div[id=l03]", 
      {style: {height: heightCalc + "px", position: "relative", 
       top: -offset + "px"}}, table_container); 

    var scrollable_container = m("div.scrollableContainer[id=l02]", 
      {onscroll: ctrl.onScroll()}, scrollable); 

    var list = m("div[id=l01]", [header, scrollable_container]); 

    return list; 
} 

Спасибо за комментарии!

0

Есть несколько хороших примеров, когда изменить перерисовывать стратегию в документации: http://mithril.js.org/mithril.redraw.html#changing-redraw-strategy

Но в целом, изменение перерисовывать стратегии редко используются, если состояние приложения хранится где-то Митрил может получить доступ и рассчитать diff, не касаясь DOM. Похоже, что ваши данные находятся в другом месте, так может ли быть так, что ваш метод sorts становится дорогим для запуска после определенного размера?

Вы можете отсортировать список только после событий, которые его модифицируют. В противном случае он будет отсортирован на каждом перерисовке Мифрила, что может быть довольно часто.

m.start/endComputation полезен для стороннего кода, особенно если он работает с DOM. Если в библиотеке хранится некоторое состояние, вы также должны использовать это для состояния приложения, поэтому нет избыточных и, возможно, несовпадающих данных.

+0

Я посмотрел на функцию сортировки, но она вызывается только в том случае, если в заголовке таблицы происходит событие click. Двойной проверил его, посмотрев в отладчик и прокомментировав функцию сортировки, которая не имеет никакого значения. Данные/список для таблицы хранятся в модели вида и сортируются там. Поэтому я предполагаю, что отставание в производительности является результатом большого количества элементов. –

+0

Вещь с большим количеством предметов заключается в том, что пользователю редко удается получить все из них на экране сразу. Существует техника, получившая название «окклюзионная отбраковка» (не отображающая то, что не видно), которая была [разработана в блоге Mithril] (http://lhorie.github.io/mithril-blog/an-exercise-in -awesomeness.html). Я написал плагин для использования этого шаблона с компонентами [здесь] (https://gist.github.com/barneycarroll/f4b4b37e5151a76debdb) - вы можете адаптировать его для своих целей. – Barney

Смежные вопросы