3

私のアプリには、これまでのところ、並べ替えおよびフィルター可能なリストと、いくつかの入力とチェックボックスがあります。この問題は、リストに 500 を超えるアイテムがある場合に発生し、ユーザー入力 (チェックボックス、入力フィールド、メニュー) を持つすべての要素で、リスト内のアイテムの数が増加するにつれて約 0.5 秒の遅延が発生し始めます。リストのソートとフィルタリングは十分に高速ですが、入力要素の遅延が長すぎます。

問題は、リストと入力要素をどのように分離できるかということです。

リストコードは次のとおりです。

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 をクリック イベントに追加して問題の回避策を開始しましたが、これで問題は解決しましたが、これは正しい解決策ですか? 例として、サード パーティの Mithril コンポーネントをリスト コンポーネントと一緒に使用する場合、外部コンポーネントのコードを変更せずにこれを行うにはどうすればよいですか?

反対に、私のリスト コンポーネントは「retain」フラグのようなものを使用できますか? リストは、指示されない限り、デフォルトでは再描画されませんか? しかし、サードパーティ コンポーネントの問題も解決しません。

リストのページネーションなど、この問題を解決するための他の戦略があることは知っていますが、Mithril 側からのベスト プラクティスを知りたいです。

前もってありがとう、ステファン

4

2 に答える 2

1

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;
}

コメントありがとうございます!

于 2015-09-17T16:26:45.593 に答える
0

ドキュメントには、いつ再描画戦略を変更するかの良い例がいくつかあります

しかし、一般に、アプリケーションの状態がどこかに保存されている場合、再描画戦略の変更はめったに使用されないため、Mithril は DOM に触れることなく差分にアクセスして計算できます。データが別の場所にあるようsortsです。特定のサイズになると、メソッドの実行コストが高くなる可能性がありますか?

リストを変更するイベントの後でのみ、リストをソートできます。そうしないと、Mithril が行う再描画のたびにソートされます。

m.start/endComputation特にDOMで動作する場合、サードパーティのコードに役立ちます。ライブラリに何らかの状態が保存されている場合は、それをアプリケーションの状態にも使用する必要があります。これにより、冗長で不一致の可能性のあるデータがなくなります。

于 2015-09-02T13:31:49.107 に答える