BackboneJS 渲染问题 [英] BackboneJS Rendering Problems

查看:23
本文介绍了BackboneJS 渲染问题的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

在过去的六个月里,我一直在与 Backbone 合作.前两个月很混乱,学习并弄清楚我想如何围绕它构建我的代码.在接下来的 4 个月里,我们正在开发一个适合生产的应用程序.不要误会我的意思,Backbone 使我摆脱了之前标准的数千行客户端代码的混乱,但它使我能够在更短的时间内完成更多宏伟的事情,从而开辟了一系列全新的问题.对于我在这里提出的所有问题,都有一些简单的解决方案,这些解决方案感觉像是黑客攻击或者只是感觉错误.我承诺奖励 300 分,以获得出色的解决方案.这是:

  1. 加载 - 对于我们的用例(管理面板)悲观同步很糟糕.对于某些事情,我需要在接受它们之前在服务器上验证它们.我们在 'sync' 事件被合并到 Backbone 之前就开始了,

我们使用这个小代码来模拟加载事件:

window.old_sync = Backbone.sync# 给backbone.sync 添加一个加载事件Backbone.sync =(方法,模型,选项)->old_sync(方法,模型,选项)模型.触发器(加载")

太好了.它按预期工作,但感觉不正确.我们将此事件绑定到所有相关视图并显示加载图标,直到我们收到来自该模型的成功或错误事件.有没有更好、更理智的方法来做到这一点?

现在是困难的:

  1. 太多的东西让自己变得太多 - 假设我们的应用程序有标签.每个选项卡控制一个集合.在左侧,您可以获得收藏.单击模型以开始在中心对其进行编辑.您更改其名称并按 Tab 键进入下一个表单项.现在,您的应用程序是实时的东西",可以注意到差异、运行验证并自动将更改同步到服务器,无需保存按钮!很好,但表单开头的 H2 与输入中的名称相同 - 您需要更新它.哦,你需要把名单上的名字更新到一边.哦,该列表按名称自行排序!

这是另一个示例:您想在集合中创建一个新项目.您按下新建"按钮并开始填写表格.您是否立即将项目添加到集合中?但是如果你决定丢弃它会发生什么?或者,如果您将整个集合保存在另一个选项卡上?并且,有一个文件上传 - 您需要先保存并同步模型,然后才能开始上传文件(以便您可以将文件附加到模型).所以一切都开始在颤抖中呈现:你保存模型和列表,表单再次呈现自己 - 现在已经同步,所以你得到一个新的删除按钮,它显示在列表中 - 但现在文件上传完成上传,所以一切再次开始渲染.

在混合中添加子视图,一切都开始看起来像费里尼的电影.

  1. 一直是子视图 - 这是一篇关于这个东西的好文章.出于对神圣事物的热爱,我无法找到将 jQuery 插件或 DOM 事件附加到任何具有子视图的视图的正确方法.地狱迅速发生.工具提示听到渲染时间很长并开始发疯,子视图变得像僵尸一样或不响应.这是实际存在的错误的主要痛点,但我仍然没有一个包罗万象的解决方案.

  2. 闪烁 - 渲染速度很快.事实上,它是如此之快,以至于我的屏幕看起来像是癫痫发作了.有时必须再次加载图像(通过另一个服务器调用!),因此 html 最小化然后再次突然最大化 - 该元素的 css 宽度+高度将解决这个问题.有时我们可以用淡入和淡出来解决这个问题——这写起来很麻烦,因为有时我们要重用一个视图,有时又要重新创建它.

TL;DR - 我在 Backbone 中的视图和子视图有问题 - 它渲染太多次,渲染时闪烁,子视图分离我的 DOM 事件并吃掉我的大脑.>

谢谢!

更多细节:带有 Ruby on Rails Gem 的 BackboneJS.使用 UnderscoreJS 模板的模板.

解决方案

部分渲染视图

为了最大限度地减少 DOM 层次结构的完整呈现,您可以在 DOM 中设置特殊节点,以反映给定属性的更新.

让我们使用这个简单的 Underscore 模板,一个名字列表:

    <% _(children).each(function(model) { %><li><span class='model-<%= model.cid %>-name'><%= model.name %></span>:<span class='model-<%= model.cid %>-name'><%= model.name %></span><% });%>

注意model-<%= model.cid %>-name类,这将是我们的注入点.

然后我们可以定义一个基本视图(或修改 Backbone.View)以在这些节点更新时用适当的值填充它们:

var V = Backbone.View.extend({初始化:函数(){//将所有更改绑定到集合中的模型this.collection.on('change', this.autoupdate, this);},//获取更改并填充任何区域集以接收值自动更新:功能(模型){var _this = 这个,更改 = model.changedAttributes(),attrs = _.keys(changes);_.each(属性,函数(属性){_this.$('.model-' + model.cid + '-' + attr).html(model.get(attr));});},//渲染完整的模板//应该只在视图确实发生巨大变化时发生渲染:函数(){无功数据,html;//构建数据以呈现模板//this.collection.toJSON() 添加了cid,实际上数据 = this.collection.map(函数(模型){返回 _.extend(model.toJSON(), {cid: model.cid});});html = 模板({孩子:数据});this.$el.html(html);返回这个;}});

代码会有所不同,以适应模型而不是集合.与 http://jsfiddle.net/nikoshr/cfcDX/

一起玩的小提琴

限制 DOM 操作

将渲染委托给子视图可能代价高昂,它们的 HTML 片段必须插入到父视图的 DOM 中.看看这个 jsperf 测试比较不同的渲染方法

它的要点是生成完整的 HTML 结构然后应用视图比构建视图和子视图然后级联渲染要快得多.例如,

var ItemView = Backbone.View.extend({});var ListView = Backbone.View.extend({渲染:函数(){var 数据,html,$table,模板 = this.options.template;数据 = this.collection.map(函数(模型){返回 _.extend(model.toJSON(), {cid:model.cid});});html = this.options.template({儿童:数据});$table = $(html);this.collection.each(函数(模型){var subview = new ItemView({el: $table.find("#" + model.cid),型号:型号});});this.$el.empty();this.$el.append($table);返回这个;}});var view = new ListView({模板:_.template($('#tpl-table').html()),集合:新的 Backbone.Collection(data)});

http://jsfiddle.net/nikoshr/UeefE/

请注意,jsperf 表明可以将模板拆分为子模板而不会造成太多损失,这将允许您为行提供部分渲染.

在相关说明中,不要在附加到 DOM 的节点上工作,这会导致不必要的回流.在操作它之前创建一个新的 DOM 或分离节点.

挤压僵尸

Derick Bailey 写了一篇关于 根除僵尸视图

基本上,您必须记住,当您丢弃视图时,您必须解除所有侦听器的绑定并执行任何额外的清理工作,例如销毁 jQuery 插件实例.我使用的是类似于 Derick 在 Backbone.Marionette 中使用的方法的组合:

var BaseView = Backbone.View.extend({初始化:函数(){//子视图列表this.views = [];},//处理子视图//覆盖以销毁 jQuery 插件实例取消阶段:函数(){如果(!this.views){返回;}var i, l = this.views.length;对于 (i = 0; i 

更新我之前的示例以使行具有可拖动的行为,如下所示:

var ItemView = BaseView.extend({阶段:函数(){this.$el.draggable({回复:无效",助手:克隆"});},取消阶段:函数(){this.$el.draggable('destroy');BaseView.prototype.unstage.call(this);}});var ListView = BaseView.extend({渲染:函数(){//和之前一样this.unstage();this.collection.each(函数(模型){var subview = new ItemView({el: $table.find("#" + model.cid),型号:型号});subview.stage();this.views.push(subview);}, 这);this.stage();this.$el.empty();this.$el.append($table);返回这个;}});

http://jsfiddle.net/nikoshr/yL7g6/

销毁根视图将遍历视图的层次结构并执行必要的清理.

注意:对于 JS 代码很抱歉,我对 Coffeescript 不够熟悉,无法提供准确的片段.

For the last six months I've been working with Backbone. The first two months were messing around, learning and figuring out how I want to structure my code around it. The next 4 months were pounding away a production-fit application. Don't get me wrong, Backbone has saved me from the thousands-lines mess of client side code that were the standard before, but it enabled me to do more grandiose things in less time, opening up a complete new stack of problems. For all the questions I raise here there are simple solutions that feels like hacks or just feel wrong. I promise a 300 points bounty for an awesome solution. Here goes:

  1. Loading - For our use case (an admin panel) pessimistic syncing is bad. For some things I need to validate things on the server before accepting them. We started out before the 'sync' event was merged into Backbone,

and we used this little code for mimicking the loading event:

window.old_sync = Backbone.sync

# Add a loading event to backbone.sync
Backbone.sync = (method, model, options) ->
  old_sync(method, model, options)
  model.trigger("loading")

Great. It works as expected but it doesn't feel correct. We bind this event to all the relevant views and display a loading icon until we receive a success or error event from that model. Is there a better, saner, way to do this?

Now for the hard ones:

  1. Too many things render themselves too much - Let's say our application have tabs. Every tab controls a collection. On the left side you get the collection. You click a model to start editing it at the center. You change its name and press tab to get to the next form item. Now, your app is a "real time something something" that notices the difference, runs validations, and automatically sync the change to the server, no save button required! Great, but the H2 at the start of the form is the same name as in the input - you need to update it. Oh, and you need to update the name on the list to the side. OH, and the list sorts itself by names!

Here's another example: You want to create a new item in the collection. You press the "new" button and you start filling out the form. Do you immediately add the item to the collection? But what happens if you decided to discard it? Or if you save the entire collection on another tab? And, there's a file upload - You need to save and sync the model before you can start uploading the file (so you can attach the file to the model). So everything starts rendering in tremors: You save the model and the list and the form renders themselves again - it's synced now, so you get a new delete button, and it shows in the list - but now the file upload finished uploading, so everything starts rendering again.

Add subviews to the mix and everything starts looking like a Fellini movie.

  1. It's subviews all the way down - Here's a good article about this stuff. I could not, for the love of everything that is holy, find a correct way to attach jQuery plugins or DOM events to any view that has subviews. Hell ensues promptly. Tooltips hear a render coming a long and start freaking around, subviews become zombie-like or do not respond. This is the main pain points as here actual bugs stand, but I still don't have an all encompassing solution.

  2. Flickering - Rendering is fast. In fact, it is so fast that my screen looks like it had a seizure. Sometimes it's images that has to load again (with another server call!), so the html minimizes and then maximizes again abruptly - a css width+height for that element will fix that. sometimes we can solve this with a fadeIn and a fadeOut - which are a pain in the ass to write, since sometimes we're reusing a view and sometimes creating it anew.

TL;DR - I'm having problems with views and subviews in Backbone - It renders too many times, it flickers when it renders, subviews detach my DOM events and eat my brains.

Thank you!

More details: BackboneJS with the Ruby on Rails Gem. Templates using UnderscoreJS templates.

解决方案

Partial rendering of views

In order to minimize the full rendering of your DOM hierarchy, you can set up special nodes in your DOM that will reflect updates on a given property.

Let's use this simple Underscore template, a list of names:

<ul>
  <% _(children).each(function(model) { %>
    <li>
        <span class='model-<%= model.cid %>-name'><%= model.name %></span> :
        <span class='model-<%= model.cid %>-name'><%= model.name %></span>
    </li>
  <% }); %>
</ul>

Notice the class model-<%= model.cid %>-name, this will be our point of injection.

We can then define a base view (or modify Backbone.View) to fill these nodes with the appropriate values when they are updated:

var V = Backbone.View.extend({
    initialize: function () {
        // bind all changes to the models in the collection
        this.collection.on('change', this.autoupdate, this);
    },

    // grab the changes and fill any zone set to receive the values
    autoupdate: function (model) {
        var _this = this,
            changes = model.changedAttributes(),
            attrs = _.keys(changes);

        _.each(attrs, function (attr) {
            _this.$('.model-' + model.cid + '-' + attr).html(model.get(attr));
        });
    },

    // render the complete template
    // should only happen when there really is a dramatic change to the view
    render: function () {
        var data, html;

        // build the data to render the template
        // this.collection.toJSON() with the cid added, in fact
        data = this.collection.map(function (model) {
            return _.extend(model.toJSON(), {cid: model.cid});
        });

        html = template({children: data});
        this.$el.html(html);

        return this;
    }
});

The code would vary a bit to accommodate a model instead of a collection. A Fiddle to play with http://jsfiddle.net/nikoshr/cfcDX/

Limiting the DOM manipulations

Delegating the rendering to the subviews can be costly, their HTML fragments have to be inserted into the DOM of the parent. Have a look at this jsperf test comparing different methods of rendering

The gist of it is that generating the complete HTML structure and then applying views is much faster than building views and subviews and then cascading the rendering. For example,

<script id="tpl-table" type="text/template">
    <table>
        <thead>
            <tr>
                <th>Row</th>
                <th>Name</th>
            </tr>
        </thead>
        <tbody>
        <% _(children).each(function(model) { %>
            <tr id='<%= model.cid %>'>
                <td><%= model.row %></td>
                <td><%= model.name %></td>
            </tr>
        <% }); %>
        </tbody>
     </table>
</script>

var ItemView = Backbone.View.extend({
});

var ListView = Backbone.View.extend({
    render: function () {
        var data, html, $table, template = this.options.template;

        data = this.collection.map(function (model) {
            return _.extend(model.toJSON(), {
                cid: model.cid
            });
        });

        html = this.options.template({
            children: data
        });

        $table = $(html);

        this.collection.each(function (model) {
            var subview = new ItemView({
                el: $table.find("#" + model.cid),
                model: model
            });
        });

        this.$el.empty();
        this.$el.append($table);

        return this;
    }
});


var view = new ListView({
    template: _.template($('#tpl-table').html()),
    collection: new Backbone.Collection(data)
});

http://jsfiddle.net/nikoshr/UeefE/

Note that the jsperf shows that the template can be be split into subtemplates without too much penalty, which would allow you to provide a partial rendering for the rows.

On a related note, don't work on nodes attached to the DOM, this will cause unnecessary reflows. Either create a new DOM or detach the node before manipulating it.

Squashing zombies

Derick Bailey wrote an excellent article on the subject of eradicating zombie views

Basically, you have to remember that when you discard a view, you must unbind all listeners and perform any additional cleanup like destroying the jQuery plugin instances. What I use is a combination of methods similar to what Derick uses in Backbone.Marionette:

var BaseView = Backbone.View.extend({

    initialize: function () {
        // list of subviews
        this.views = [];
    },

    // handle the subviews
    // override to destroy jQuery plugin instances
    unstage: function () {
        if (!this.views) {
            return;
        }

        var i, l = this.views.length;

        for (i = 0; i < l; i = i + 1) {
            this.views[i].destroy();
        }
        this.views = [];
    },

    // override to setup jQuery plugin instances
    stage: function () {
    },

    // destroy the view
    destroy: function () {
        this.unstage();
        this.remove();
        this.off();

        if (this.collection) {
            this.collection.off(null, null, this);
        }
        if (this.model) {
            this.model.off(null, null, this);
        }
    }
});

Updating my previous example to give the rows a draggable behavior would look like this:

var ItemView = BaseView.extend({
    stage: function () {
        this.$el.draggable({
            revert: "invalid",
            helper: "clone"
        });
    },

    unstage: function () {
        this.$el.draggable('destroy');
        BaseView.prototype.unstage.call(this);
    }
});

var ListView = BaseView.extend({

    render: function () {
       //same as before

        this.unstage();
        this.collection.each(function (model) {
            var subview = new ItemView({
                el: $table.find("#" + model.cid),
                model: model
            });
            subview.stage();
            this.views.push(subview);
        }, this);
        this.stage();

        this.$el.empty();
        this.$el.append($table);

        return this;
    }
});

http://jsfiddle.net/nikoshr/yL7g6/

Destroying the root view will traverse the hierarchy of views and perform the necessary cleanups.

NB: sorry about the JS code, I'm not familiar enough with Coffeescript to provide accurate snippets.

这篇关于BackboneJS 渲染问题的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆