我有三种不同的方法来初始化和渲染视图及其子视图,每种方法都有不同的问题。我很想知道是否有更好的方法可以解决所有问题:
场景一:
在父级的初始化函数中初始化子级。这样,并非所有内容都卡在渲染中,从而减少了渲染阻塞。
initialize : function () {
//parent init stuff
this.child = new Child();
},
render : function () {
this.$el.html(this.template());
this.child.render().appendTo(this.$('.container-placeholder');
}
问题:
最大的问题是第二次在父节点上调用渲染将删除所有子事件绑定。 (这是因为 jQuery 的 $.html() 是如何工作的。)这可以通过调用 this.child.delegateEvents().render().appendTo(this.$el); 来缓解。相反,但是第一个也是最常见的情况,你做了更多不必要的工作。
通过添加子元素,您可以强制渲染函数了解父 DOM 结构,以便获得所需的排序。这意味着更改模板可能需要更新视图的渲染功能。
场景二:
仍然在父级的 initialize()
中初始化子级,但不是追加,而是使用 setElement().delegateEvents()
将子级设置为父级模板中的一个元素。
initialize : function () {
//parent init stuff
this.child = new Child();
},
render : function () {
this.$el.html(this.template());
this.child.setElement(this.$('.placeholder-element')).delegateEvents().render();
}
问题:
这使得 delegateEvents() 现在是必需的,这与它仅在第一种情况下的后续调用中是必需的相比略有负面。
场景三:
改为在父级的 render()
方法中初始化子级。
initialize : function () {
//parent init stuff
},
render : function () {
this.$el.html(this.template());
this.child = new Child();
this.child.appendTo($.('.container-placeholder').render();
}
问题:
这意味着渲染函数现在也必须与所有初始化逻辑绑定在一起。
如果我编辑其中一个子视图的状态,然后在父视图上调用渲染,则会生成一个全新的子视图,并且其所有当前状态都将丢失。这似乎也可能因内存泄漏而变得冒险。
真的很想得到你的家伙对此的看法。你会使用哪种场景?还是有第四种神奇的方法可以解决所有这些问题?
您是否曾经跟踪过视图的渲染状态?说一个 renderedBefore
标志?看起来真的很鸡肋。
setElement()
之后调用 delegateEvents()
?根据文档:“......并将视图的委托事件从旧元素移动到新元素”,setElement
方法本身应该处理事件重新委托。
这是一个很好的问题。 Backbone 很棒,因为它没有做任何假设,但这确实意味着您必须(决定如何)自己实现这样的事情。在浏览了我自己的东西之后,我发现我(有点)混合使用了场景 1 和场景 2。我认为不存在第四个神奇场景,因为简单地说,你在场景 1 和 2 中所做的一切都必须是完毕。
我认为用一个例子来解释我喜欢如何处理它是最容易的。假设我将这个简单的页面分解为指定的视图:
https://i.stack.imgur.com/k2QMj.jpg
假设 HTML 在渲染后是这样的:
<div id="parent">
<div id="name">Person: Kevin Peel</div>
<div id="info">
First name: <span class="first_name">Kevin</span><br />
Last name: <span class="last_name">Peel</span><br />
</div>
<div>Phone Numbers:</div>
<div id="phone_numbers">
<div>#1: 123-456-7890</div>
<div>#2: 456-789-0123</div>
</div>
</div>
希望 HTML 如何与图表匹配非常明显。
ParentView
包含 2 个子视图 InfoView
和 PhoneListView
以及一些额外的 div,其中一个 #name
需要在某个时候设置。 PhoneListView
拥有自己的子视图,即 PhoneView
条目的数组。
那么关于你的实际问题。我根据视图类型以不同方式处理初始化和渲染。我将我的视图分为两种类型,Parent
视图和 Child
视图。
它们之间的区别很简单,Parent
视图包含子视图,而 Child
视图不包含。因此,在我的示例中,ParentView
和 PhoneListView
是 Parent
视图,而 InfoView
和 PhoneView
条目是 Child
视图。
就像我之前提到的,这两个类别之间的最大区别在于它们何时被允许渲染。在一个完美的世界中,我希望 Parent
视图只呈现一次。当模型更改时,由他们的子视图处理任何重新渲染。 Child
视图,另一方面,我允许在需要时重新渲染,因为它们没有任何其他依赖于它们的视图。
更详细地说,对于 Parent
视图,我喜欢我的 initialize
函数来做一些事情:
初始化我自己的视图 渲染我自己的视图 创建并初始化任何子视图。在我的视图中为每个子视图分配一个元素(例如,InfoView 将被分配#info)。
第 1 步很容易解释。
第 2 步(渲染)已完成,以便子视图所依赖的任何元素在我尝试分配它们之前已经存在。通过这样做,我知道所有子 events
都将被正确设置,并且我可以根据需要多次重新渲染它们的块,而不必担心必须重新委托任何东西。我在这里实际上没有render
任何子视图,我允许他们在自己的 initialization
中这样做。
第 3 步和第 4 步实际上是在我在创建子视图时传入 el
的同时处理的。我喜欢在这里传递一个元素,因为我觉得父母应该确定允许孩子在自己的视图中放置其内容的位置。
对于渲染,我尽量让 Parent
视图保持简单。我希望 render
函数只渲染父视图。没有事件委托,没有子视图的渲染,什么都没有。只是一个简单的渲染。
有时这并不总是有效。例如,在我上面的示例中,只要模型中的名称发生更改,就需要更新 #name
元素。但是,此块是 ParentView
模板的一部分,不由专用 Child
视图处理,因此我解决了这个问题。我将创建某种 subRender
函数,它仅 替换 #name
元素的内容,而不必丢弃整个 #parent
元素。这可能看起来像一个 hack,但我真的发现它比不必担心重新渲染整个 DOM 和重新附加元素等效果更好。如果我真的想让它干净,我会创建一个新的 Child
视图(类似于 InfoView
)来处理 #name
块。
现在对于 Child
视图,initialization
与 Parent
视图非常相似,只是没有创建任何进一步的 Child
视图。所以:
初始化我的视图 设置绑定监听我关心的模型的任何更改 渲染我的视图
Child
视图渲染也很简单,只需渲染并设置我的el
的内容即可。同样,不要搞乱委派或类似的事情。
以下是我的 ParentView
的一些示例代码:
var ParentView = Backbone.View.extend({
el: "#parent",
initialize: function() {
// Step 1, (init) I want to know anytime the name changes
this.model.bind("change:first_name", this.subRender, this);
this.model.bind("change:last_name", this.subRender, this);
// Step 2, render my own view
this.render();
// Step 3/4, create the children and assign elements
this.infoView = new InfoView({el: "#info", model: this.model});
this.phoneListView = new PhoneListView({el: "#phone_numbers", model: this.model});
},
render: function() {
// Render my template
this.$el.html(this.template());
// Render the name
this.subRender();
},
subRender: function() {
// Set our name block and only our name block
$("#name").html("Person: " + this.model.first_name + " " + this.model.last_name);
}
});
您可以在此处查看我的 subRender
实现。通过将更改绑定到 subRender
而不是 render
,我不必担心炸毁和重建整个块。
以下是 InfoView
块的示例代码:
var InfoView = Backbone.View.extend({
initialize: function() {
// I want to re-render on changes
this.model.bind("change", this.render, this);
// Render
this.render();
},
render: function() {
// Just render my template
this.$el.html(this.template());
}
});
绑定是这里的重要部分。通过绑定到我的模型,我不必担心自己手动调用 render
。如果模型发生变化,此块将重新渲染自身,而不会影响任何其他视图。
PhoneListView
类似于 ParentView
,您只需要在 initialization
和 render
函数中增加一点逻辑来处理集合。如何处理集合完全取决于您,但您至少需要监听集合事件并决定如何渲染(追加/删除,或者只是重新渲染整个块)。我个人喜欢添加新视图并删除旧视图,而不是重新渲染整个视图。
PhoneView
与 InfoView
几乎相同,只是监听它关心的模型更改。
希望这对您有所帮助,如果有任何令人困惑或不够详细的地方,请告诉我。
我不确定这是否直接回答了您的问题,但我认为这是相关的:
http://lostechies.com/derickbailey/2011/10/11/backbone-js-getting-the-model-for-a-clicked-element/
当然,我设置这篇文章的背景是不同的,但我认为我提供的两种解决方案,以及每种解决方案的优缺点,应该会让你朝着正确的方向前进。
对我来说,通过某种标志来区分视图的初始设置和后续设置似乎并不是世界上最糟糕的主意。为了使这个干净和简单,应该将标志添加到您自己的视图中,该视图应该扩展主干(基础)视图。
与 Derick 一样,我不完全确定这是否直接回答了您的问题,但我认为在这种情况下至少值得一提。
另请参阅:在 Backbone 中使用 Eventbus
Kevin Peel 给出了一个很好的答案——这是我的 tl;dr 版本:
initialize : function () {
//parent init stuff
this.render(); //ANSWER: RENDER THE PARENT BEFORE INITIALIZING THE CHILD!!
this.child = new Child();
},
我试图避免这样的视图之间的耦合。我通常有两种方法:
使用路由器
基本上,您让您的路由器功能初始化父视图和子视图。所以视图彼此不知道,但路由器处理了这一切。
将相同的 el 传递给两个视图
this.parent = new Parent({el: $('.container-placeholder')});
this.child = new Child({el: $('.container-placeholder')});
两者都具有相同的 DOM 知识,您可以随意订购它们。
我所做的是给每个孩子一个身份(Backbone 已经为你做到了:cid)
当 Container 进行渲染时,使用 'cid' 和 'tagName' 为每个孩子生成一个占位符,因此在模板中,孩子不知道容器将把它放在哪里。
<tagName id='cid'></tagName>
比你可以使用
Container.render()
Child.render();
this.$('#'+cid).replaceWith(child.$el);
// the rapalceWith in jquery will detach the element
// from the dom first, so we need re-delegateEvents here
child.delegateEvents();
不需要指定占位符,Container 只生成占位符而不是子节点的 DOM 结构。 Cotainer 和 Children 仍在生成自己的 DOM 元素,而且只生成一次。
这是一个用于创建和渲染子视图的轻量级 mixin,我认为它解决了这个线程中的所有问题:
https://github.com/rotundasoftware/backbone.subviews
这个插件采用的方法是在第一次渲染父视图之后创建和渲染子视图。然后,在父视图的后续渲染中,$.detach 子视图元素,重新渲染父视图,然后将子视图元素插入适当的位置并重新渲染它们。这样子视图对象在后续渲染中被重用,并且不需要重新委托事件。
请注意,集合视图的情况(集合中的每个模型都用一个子视图表示)是完全不同的,我认为值得自己讨论/解决方案。对于这种情况,我知道的最佳通用解决方案是 CollectionView in Marionette。
编辑:对于集合视图案例,如果您需要基于点击和/或拖放来重新排序来选择模型,您可能还需要查看 this more UI focused implementation。
initialize
方法中调用render
是一种不好的做法,因为它会阻止您在不想立即渲染的情况下提高性能。你怎么看待这件事?PhoneListView
?