英文:
Understanding Sort and comparator in Backbone.js
问题
我正在尝试理解Backbone.js中的排序和比较器。我在网上看到了许多答案,但是我无法让它们中的任何一个起作用。在经典的"ToDo"应用程序的jsfiddle中工作:https://jsfiddle.net/quovadimus/9z3wnoh2/2/ 我尝试更改第34行:
comparator: 'order'
为
comparator: function(model) {
    return -model.get('order');
}
或者甚至简单地:
comparator: 'title'
我为我的第一个待办事项的标题输入"zzz",为下一个输入"aaa"。我期望我的任何修改都能反转待办事项的列表顺序。但每次它都显示在原始顺序中。我漏掉了什么?谢谢。
英文:
I'm trying to understand sort and comparator in Backbone.js. I've seen many answers online but I have been unable to get any of them to work. Working with the jsfiddle of the classic "ToDo" application: https://jsfiddle.net/quovadimus/9z3wnoh2/2/ I've tried changing line 34:
comparator: 'order'
with
   comparator:  function(model) {
   	return -model.get('order');
   }  
or even simply:
comparator: 'title
I'm entering "zzz" for the title of my first todo and "aaa" for the next. I expect either of my modifications to reverse the list order of the todos. But every time it is displayed in the original order. What am I missing? thank you
答案1
得分: 1
你正确理解了comparator的工作原理。然而,调整集合的顺序只是故事的一半。视图(在这种情况下是AppView)仍然可以自由地以任何顺序呈现集合中的模型。在你提供的示例代码中,确定呈现顺序的代码行如下:
	var AppView = Backbone.View.extend({
        // ...
		initialize: function() {
            // ...
			this.listenTo(Todos, "add", this.addOne);
			this.listenTo(Todos, "reset", this.addAll);
            // ...
		},
        // ...
		addOne: function(todo) {
			var view = new TodoView({model: todo});
			this.$("#todo-list").append(view.render().el);
		},
		addAll: function() {
			Todos.each(this.addOne, this);
		},
        // ...
	});
基本上,这段代码表示以下内容:
- 在正常情况下(
add事件,addOne方法),将最新的待办事项放在列表底部。 - 在特殊情况下(
reset事件,addAll方法),按照它们在集合中出现的顺序放置待办事项。 
在你的示例中,reset事件的特殊情况从未发生,即使在使用一些已经在你的localStorage中的待办事项刷新应用程序时也没有发生,因为fetch方法调用的是set而不是reset。
要解决这个问题,构建你的视图以始终尊重集合的顺序。这说起来容易做起来难;因此,我建议使用专门用于此目的的库,例如backbone-fractal或Marionette,它们都提供了用于此目的的CollectionView。然而,出于教育的目的,这里是一种基本方法,用于在视图中反映集合的顺序。
在开始之前,停止从外部渲染视图。你会看到像view.render().el这样的代码行在互联网上到处都有,因为它使示例代码更加清晰,但实际上你不应该在生产中这样做。视图的渲染是一个内部事务,是视图自己的责任(除非渲染非常昂贵,你应该尽量避免这种情况,而且只有视图本身无法确定何时进行渲染)。在大多数情况下,视图应该在其initialize方法中渲染一次,然后在其模型触发'change'事件时再次渲染。换句话说,在TodoView.initialize中添加以下行:
this.render();
现在可能看起来不重要,但相信我,如果只有每个单独的视图需要跟踪何时进行渲染,这将极大地简化问题。
接下来,你需要将创建子视图(TodoView的实例)与放置这些子视图分开。创建子视图应在两种类型的情况下发生:
- 当父视图(
AppView)被初始化时,对于那些在那个时候已经在集合中的模型。 - 每次之后当新模型被添加到集合中时,也就是在
add事件上。 
放置子视图也应在两种类型的情况下发生,但它们不完全相同:
- 当父视图被初始化时,对于那些在那个时候已经在集合中的模型。
 - 每次之后当集合获得新的模型或更改顺序时,也就是在
update事件上。 
由于需要在时间上分开创建和放置子视图,因此你将不得不在此期间存储子视图。这也意味着当模型被删除时,你需要更新存储。你可以选择不同的存储方法,这反过来会影响如何创建和放置子视图。下面,我演示了一种方法,将子视图保存在一个由相应模型的cid索引的对象中。请记住,这只是示例代码;基本原则是正确的,但在实践中,要做到完美将会很棘手。使用像backbone-fractal这样的库将为你省去这些麻烦。
    var AppView = Backbone.View.extend({
        // ...
        initialize: function() {
            // ...
            this.resetSubviews().placeSubviews().listenTo(Todos, {
                add: this.addSubview,
                remove: this.forgetSubview,
                update: this.placeSubviews,
                reset: this.resetSubviews,
            });
            // ...
        },
        // ...
        addSubview: function(todo) {
            this.subviews[todo.cid] = new TodoView({model: todo});
        },
        forgetSubview: function(todo) {
            delete this.subviews[todo.cid];
        },
        placeSubviews: function() {
            var model2el = _.compose(_.property('el'), _.propertyOf(this.subviews), _.property('cid'));
            var subviewEls = Todos.map(model2el);
            this.$('#todo-list').append(subviewEls);
            return this;
        },
        resetSubviews: function() {
            this.subviews = {};
            Todos.each(this.addSubview, this);
            return this;
        },
        // ...
    });
英文:
You correctly understand how comparator works. However, adjusting the order of the collection is only half the story. The view (AppView in this case) is still free to present the models in the collection in any order. In the fiddle that you linked, the lines of code that determine the order of presentation are the following:
	var AppView = Backbone.View.extend({
        // ...
		initialize: function() {
            // ...
			this.listenTo(Todos, "add", this.addOne);
			this.listenTo(Todos, "reset", this.addAll);
            // ...
		},
        // ...
		addOne: function(todo) {
			var view = new TodoView({model: todo});
			this.$("#todo-list").append(view.render().el);
		},
		addAll: function() {
			Todos.each(this.addOne, this);
		},
        // ...
	});
Basically, this code says the following:
- In the normal case (
addevent,addOnemethod), put the newest todo at the bottom of the list. - In an exceptional case (
resetevent,addAllmethod), place the todos in the order in which they appear in the collection. 
The exceptional case with the reset event never happens in your fiddle, not even when you refresh the app with some todos already in your localStorage, because the fetch method calls set rather than reset.
To solve this, build your view such that it always respects the order of the collection. This is easier said than done; for this reason, I recommend using a dedicated library for this purpose, such as backbone-fractal or Marionette, both of which offer a CollectionView for this purpose. However, for the sake of education, here is a basic approach for mirroring the order of a collection in a view.
Before we start, stop rendering views from the outside. You'll see lines like view.render().el all over the internet, because it makes nice example code, but you shouldn't actually do that in production. The rendering of a view is an internal affair and the view's own concern (unless rendering is really expensive, which you should try to avoid, and the view alone cannot determine whether the time is right to do it). In the majority of cases, a view should render once in its initialize method and then again when its model triggers the 'change' event. In other words, add this line to TodoView.initialize:
this.render();
This may not seem important now, but take my word that it simplifies matters enormously if only each individual view needs to keep track of when to render itself.
Next, you need to separate creating subviews (instances of TodoView) from placing those subviews. Creating subviews should happen at two types of occasions:
- When the parent view (
AppView) is initialized, for each model that happens to already be in the collection at that time. - Each time after that when a new model is added to the collection, i.e., on the 
addevent. 
Placing subviews should also happen at two types of occasions, but they are not exactly the same:
- When the parent view is initialized, for each model that happens to be already in the collection at that time.
 - Each time after that when the collection gains new models or changes order, i.e., on the 
updateevent. 
Since creating and placing the subviews needs to be separated in time, you will have to store the subviews in the meanwhile. This also means that you will need to update the storage when models are removed again.
You can choose different approaches for the storage and this in turn affects how to go about creating and placing the subviews. Below, I demonstrate an approach that keeps the subviews in an object that is indexed by the corresponding model's cid. Keep in mind that this is example code; the basic principles are correct, but in practice, getting all the details right will be tricky. Using a library like backbone-fractal will save you that trouble.
    var AppView = Backbone.View.extend({
        // ...
        initialize: function() {
            // ...
            this.resetSubviews().placeSubviews().listenTo(Todos, {
                add: this.addSubview,
                remove: this.forgetSubview,
                update: this.placeSubviews,
                reset: this.resetSubviews,
            });
            // ...
        },
        // ...
        addSubview: function(todo) {
            this.subviews[todo.cid] = new TodoView({model: todo});
        },
        forgetSubview: function(todo) {
            delete this.subviews[todo.cid];
        },
        placeSubviews: function() {
            var model2el = _.compose(_.property('el'), _.propertyOf(this.subviews), _.property('cid'));
            var subviewEls = Todos.map(model2el);
            this.$('#todo-list').append(subviewEls);
            return this;
        },
        resetSubview: function() {
            this.subviews = {};
            Todos.each(this.addSubview, this);
            return this;
        },
        // ...
    });
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。


评论