首页 文章

如何在Backbone.js中处理初始化和渲染子视图?

提问于
浏览
198

我有三种不同的方法来初始化和呈现视图及其子视图,并且每个方法都有不同的问题 . 我很想知道是否有更好的方法可以解决所有问题:


场景一:

在父级的初始化函数中初始化子级 . 这样,并不是所有东西都会陷入渲染状态,因此渲染时阻塞较少 .

initialize : function () {

    //parent init stuff

    this.child = new Child();
},

render : function () {

    this.$el.html(this.template());

    this.child.render().appendTo(this.$('.container-placeholder');
}

问题:

  • 最大的问题是第二次在父级上调用render将删除所有的子事件绑定 . (这是因为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();
}

问题:

  • 这意味着现在必须将渲染函数与所有初始化逻辑联系起来 .

  • 如果我编辑其中一个子视图的状态,然后在父视图上调用render,将创建一个全新的子节点,并且它的所有当前状态都将丢失 . 对于内存泄漏而言,它似乎也会变得冒险 .


非常好奇让你的家伙接受这个 . 你会使用哪种场景?还是有第四个神奇的解决所有这些问题?

你有没有跟踪视图的渲染状态?说 renderedBefore 旗?看起来真的很笨拙 .

7 回答

  • 6

    这是一个很好的问题 . Backbone很棒,因为它缺乏假设,但它确实意味着你必须(决定如何)自己实现这样的事情 . 在查看了我自己的东西之后,我发现我(有点)使用了场景1和场景2的混合 . 我不认为存在第四个神奇场景,因为,简单地说,你在场景1和场景2中所做的一切都必须是完成 .

    我认为用一个例子解释我喜欢用它来解释它是最容易的 . 假设我将这个简单的页面分解为指定的视图:

    Page Breakdown

    假设HTML在呈现后是这样的:

    <div id="parent">
        <div id="name">Person: Kevin Peel</div>
        <div id="info">
            First name: <span class="first_name">Kevin</span>
    Last name: <span class="last_name">Peel</span>
    </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个子视图, InfoViewPhoneListView 以及一些额外的div,其中一个, #name ,需要在某个时候设置 . PhoneListView 拥有自己的子视图,一个 PhoneView 条目数组 .

    那么关于你的实际问题 . 我根据视图类型处理初始化和渲染 . 我将我的观点分为两类: Parent views和 Child views .

    它们之间的区别很简单, Parent 视图包含子视图,而 Child 视图则不包含 . 所以在我的例子中, ParentViewPhoneListViewParent 视图,而 InfoViewPhoneView 条目是 Child 视图 .

    就像我之前提到的,这两个类别之间的最大区别在于它们被允许渲染 . 在一个完美的世界里,我希望 Parent 只能渲染一次 . 当模型发生变化时,由子视图决定是否处理任何重新渲染 . 另一方面,我允许在他们需要的任何时候重新渲染,因为他们没有依赖它们的任何其他视图 .

    更详细一点,对于 Parent 个视图,我喜欢我的 initialize 函数做一些事情:

    • 初始化我自己的视图

    • 渲染我自己的视图

    • 创建并初始化任何子视图 .

    • 在视图中为每个子视图分配一个元素(例如, InfoView 将被分配 #info ) .

    第1步非常自我解释 .

    第2步,完成渲染,以便在我尝试分配子视图之前,子视图所依赖的任何元素都已存在 . 通过这样做,我知道所有的孩子都将被正确设置,我可以根据需要重新渲染他们的块,而不必担心必须重新委托任何事情 . 我实际上并没有 render 这里有任何子视图,我允许他们在自己的 initialization 中做到这一点 .

    实际上,当我在创建子视图时传递 el 时,处理步骤3和4 . 我喜欢在这里传递一个元素,因为我觉得父母应该在自己的视图中确定允许孩子放置其内容的位置 .

    对于渲染,我试图让它对于 Parent 视图非常简单 . 我希望 render 函数除了呈现父视图之外什么都不做 . 没有事件委托,没有渲染子视图,没有 . 只是一个简单的渲染 .

    有时这并不总是有效 . 例如,在上面的示例中,只要模型中的名称发生更改,就需要更新 #name 元素 . 但是,此块是 ParentView 模板的一部分,而不是由专用的 Child 视图处理,所以我解决了这个问题 . 我将创建某种 subRender 函数,它只替换 #name 元素的内容,而不必删除整个 #parent 元素 . 这可能看起来像是一个黑客攻击,但是我创建了一个新的 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 ,您只需要在 initializationrender 函数中使用更多逻辑来处理集合 . 你如何处理集合真的取决于你,但你至少需要听取集合事件并决定你想要渲染的方式(追加/删除,或者只是重新渲染整个块) . 我个人喜欢添加新视图并删除旧视图,而不是重新渲染整个视图 .

    PhoneView 几乎与 InfoView 完全相同,只是听取它关心的模型更改 .

    希望这有点帮助,如果有什么令人困惑或不够详细,请告诉我 .

  • 258

    我不确定这是否直接回答了你的问题,但我认为这是相关的:

    http://lostechies.com/derickbailey/2011/10/11/backbone-js-getting-the-model-for-a-clicked-element/

    当然,我设置这篇文章的背景是不同的,但我认为我提供的两个解决方案,以及每个解决方案的优缺点,都应该让你朝着正确的方向前进 .

  • 1

    对我而言,通过某种标志来区分视图的初始设置和后续设置似乎不是世界上最糟糕的想法 . 为了使这个干净简单,应该将标志添加到您自己的View中,该View应该扩展Backbone(Base)View .

    与Derick相同我不完全确定这是否直接回答了您的问题,但我认为在此背景下至少可能值得一提 .

    另请参阅:在Backbone中使用Eventbus

  • 2

    Kevin Peel给出了一个很好的答案 - 这是我的tl; dr版本:

    initialize : function () {
    
        //parent init stuff
    
        this.render(); //ANSWER: RENDER THE PARENT BEFORE INITIALIZING THE CHILD!!
    
        this.child = new Child();
    },
    
  • 2

    我试图避免这些视图之间的耦合 . 我通常有两种方式:

    使用路由器

    基本上,您让路由器功能初始化父视图和子视图 . 因此视图彼此不了解,但路由器处理所有这些 .

    将相同的el传递给两个视图

    this.parent = new Parent({el: $('.container-placeholder')});
    this.child = new Child({el: $('.container-placeholder')});
    

    两者都知道相同的DOM,您可以随意订购它们 .

  • 6

    我所做的是给每个孩子一个身份(Backbone已经为你做了这个:cid)

    当Container执行渲染时,使用'cid'和'tagName'为每个子节点生成占位符,因此在模板中,子节点不知道Container将放置它的位置 .

    <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();
    

    不需要指定的占位符,并且容器只生成占位符而不是子节点的DOM结构 . Cotainer和Children仍然只生成一次DOM元素 .

  • 3

    这是一个轻量级混合,用于创建和渲染子视图,我认为这解决了这个线程中的所有问题:

    https://github.com/rotundasoftware/backbone.subviews

    此插件采用的方法是在第一次呈现父视图后创建和渲染子视图 . 然后,在后续渲染父视图时,$ .detach子视图元素,重新渲染父元素,然后将子视图元素插入适当的位置并重新渲染它们 . 这样子视图对象可以在后续渲染中重用,并且不需要重新委派事件 .

    请注意,集合视图的情况(集合中的每个模型都用一个子视图表示)是完全不同的,我认为它值得讨论/解决 . 我知道的最佳通用解决方案是CollectionView in Marionette .

    编辑:对于集合视图案例,您可能还需要查看this more UI focused implementation,如果您需要根据点击选择模型和/或拖放进行重新排序 .

相关问题