Mithril.js 中大量元素的性能问题

Perfomance issues with large number of elements in Mithril.js

到目前为止,我的应用程序有一个排序和可过滤列表以及一些输入和复选框。 如果列表中的项目超过 500 个,那么问题就会出现,然后每个带有用户输入的元素(复选框、输入字段、菜单)开始有大约半秒的滞后,随着列表中项目的数量增加。列表的排序和过滤速度足够快,但输入元素的滞后时间太长。

问题是:列表和输入元素如何解耦?

列表代码如下:

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.startComputation/endComputation 来解决问题,这解决了问题,但这是正确的解决方案吗?例如,如果我将来自第 3 方的 Mithril 组件与我的列表组件一起使用,我应该如何在不更改其代码的情况下对外部组件执行此操作?

另一方面,我的列表组件可以使用类似 'retain' 标志的东西吗?所以默认情况下列表不会重绘,除非它被告知这样做?但是第 3 方组件的问题也会持续存在。

我知道还有其他策略可以解决这个问题,例如列表分页,但我想知道 Mithril 方面的最佳实践是什么。

提前致谢, 斯特凡

文档中有一些关于何时更改重绘策略的好例子:http://mithril.js.org/mithril.redraw.html#changing-redraw-strategy

但一般来说,如果应用程序状态存储在某处以便 Mithril 可以在不触及 DOM 的情况下访问和计算差异,则很少使用更改重绘策略。看起来你的数据在别处,所以你的 sorts 方法在一定大小后会变得昂贵到 运行 吗?

您只能在修改列表的事件发生后对列表进行排序。否则它会在 Mithril 每次重绘时进行排序,这可能很常见。

m.start/endComputation 对于第 3 方代码很有用,尤其是在 DOM 上运行时。如果库存储了一些状态,您也应该将其用于应用程序状态,这样就不会有任何冗余和可能不匹配的数据。

感谢 Barney 的评论,我找到了一个解决方案:遮挡剔除。可以在此处找到原始示例 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;
}

感谢评论!