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

IT技术 javascript backbone.js
2021-03-08 15:32:05

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


场景一:

在父级的 initialize 函数中初始化子级。这样,并非所有内容都卡在渲染中,从而减少渲染阻塞。

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

问题:

  • 这意味着渲染函数现在也必须与所有初始化逻辑相关联。

  • 如果我编辑其中一个子视图的状态,然后在父视图上调用渲染,则会创建一个全新的子视图,并且其所有当前状态都将丢失。这似乎也可能导致内存泄漏。


真的很好奇让你的家伙接受这个。你会使用哪种场景?或者是否有第四种神奇的方法可以解决所有这些问题?

您是否曾经跟踪过视图的渲染状态?renderedBefore旗?看起来真的很笨拙。

6个回答

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

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

分页

假设 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并且PhoneListViewParent观点,而InfoViewPhoneView条目Child意见。

就像我之前提到的,这两个类别之间最大的区别在于它们何时被允许渲染。在一个完美的世界中,我希望Parent视图只渲染一次。当模型更改时,由他们的子视图来处理任何重新渲染。Child另一方面,我允许在他们需要的任何时候重新渲染,因为他们没有任何其他视图依赖它们。

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

  1. 初始化我自己的视图
  2. 呈现我自己的观点
  3. 创建并初始化任何子视图。
  4. 在我的视图中为每个子视图分配一个元素(例如,InfoView将被分配#info)。

第 1 步是不言自明的。

第 2 步,渲染已经完成,因此在我尝试分配它们之前,子视图所依赖的任何元素都已经存在。通过这样做,我知道所有孩子events都将被正确设置,并且我可以根据需要多次重新渲染他们的块,而不必担心必须重新委派任何东西。我实际上render在这里没有任何孩子的观点,我允许他们在自己的initialization.

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

对于渲染,我尽量让Parent视图非常简单我希望该render函数只渲染父视图。没有事件委托,没有子视图的呈现,什么都没有。只是一个简单的渲染。

有时这并不总是有效。例如,在我上面的示例中,#name只要模型中的名称发生变化元素就需要更新。但是,这个块是ParentView模板的一部分,不是由专用Child视图处理的,所以我解决了这个问题。我将创建某种替换元素内容subRender函数,而不必删除整个元素。这可能看起来像一个 hack,但我真的发现它比不必担心重新渲染整个 DOM 和重新附加元素等要好得多。如果我真的想让它干净,我会创建一个新的视图(类似于)来处理块。#name#parentChildInfoView#name

现在对于Child视图,initialization它与Parent视图非常相似,只是没有创建任何进一步的Child视图。所以:

  1. 初始化我的视图
  2. 设置绑定监听我关心的模型的任何更改
  3. 呈现我的观点

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,只是听着它关心模型的变化。

希望这对您有所帮助,如果有任何令人困惑或不够详细的内容,请告诉我。

当然。嗯...我只尝试初始化应该立即显示的视图(或者可以在不久的将来显示,例如现在隐藏但在单击编辑链接时显示的编辑表单),因此它们应该立即呈现。如果说,我有一个视图需要一段时间来渲染的情况,我个人不会在需要显示之前创建视图本身,因此在必要之前不会进行初始化和渲染。如果你有一个例子,我可以尝试更详细地说明我如何处理它......
2021-04-24 15:32:05
无法重新渲染 ParentView 是一个很大的限制。我有一种情况,我基本上有一个选项卡控件,其中每个选项卡显示不同的 ParentView。我想在第二次单击选项卡时重新渲染现有的 ParentView,但这样做会导致 ChildViews 中的事件丢失(我在 init 中创建子项,并在渲染中渲染它们)。我想我要么 1)隐藏/显示而不是渲染,2)在 ChildViews 的渲染中使用 delegateEvents,或者 3)在渲染中创建孩子,或者 4)不要担心性能。在出现问题之前,每次都重新创建 ParentView。嗯……
2021-04-25 15:32:05
我应该说这对我来说是一个限制。如果您不尝试重新渲染父级,此解决方案将正常工作:) 我没有很好的答案,但我和 OP 有相同的问题。仍然希望找到合适的“主干”方式。在这一点上,我在想隐藏/显示并且不重新渲染是我要走的路。
2021-04-27 15:32:05
你如何将集合传递给孩子PhoneListView
2021-05-11 15:32:05
非常详尽的解释谢谢。不过,我听说并倾向于同意renderinitialize方法内部调用是一种不好的做法,因为在您不想立即渲染的情况下,它会阻止您提高性能。你怎么看待这件事?
2021-05-15 15:32:05

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

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

当然,我撰写本文的上下文是不同的,但我认为我提供的两种解决方案以及每种解决方案的优缺点应该会让您朝着正确的方向前进。

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

与德里克一样,我不完全确定这是否直接回答了您的问题,但我认为在这种情况下至少值得一提。

另请参阅:在 Backbone 中使用 Eventbus

凯文皮尔给出了一个很好的答案 - 这是我的 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,您可以随意订购它们。