Matando zumbis espinha dorsais em aplicativos de página única

Pensou que você matou todas as suas visualizações de zumbis em seu aplicativo de backbone? Eu também, até perceber que um evento de keyup estava disparando vários renderizadores e que o número de sorteios aumentava gradativamente de acordo com o número de vezes que naveguei de volta para a página de pesquisa. Eu já tinha feito o protótipo de um método de fechamento no Backbone.View (de acordo com o excelente trabalho feito aqui http://lostechies.com/derickbailey/2011/09/15/zombies-run-managing-page-transitions-in-backbone-apps/ ) e desvinculou e removeu as visualizações em vão! Acontece que o encapsulamento de visão pode levar a zumbis em todos os lugares.

Após uma investigação completa, encontrei 2 problemas sérios:

  1. Percebi que minha visão de coleção nunca estava sendo fechada! Embora eu estivesse apenas instanciando a página de pesquisa uma vez e fazendo referência a ela do roteador, a exibição da página de pesquisa encapsulou outra exibição que gerou a coleção. Parece tão simples agora, mas este é um exemplo perfeito de como vazamentos de memória podem pegar você.

  2. Você chamou um unbind em sua coleção, não apenas em sua visualização. Este me fez tropeçar por um minuto. Suponho que isso torne o fechamento de um processo de 3 etapas: desvincular eventos de visão, remover a visão do DOM e desvincular a coleção (ou modelo) … Teste .. simplesmente chamar desvincular na visão e remover não resolverá o problema. Seu aplicativo continuará a usar mais e mais eventos de disparo de memória na coleção várias vezes!

Exemplo antes do código:

// add default close method for views
Backbone.View.prototype.close = function () {
console
.log('Unbinding events for ' + this.cid);
this.remove();
this.unbind();

if (this.onClose) {
this.onClose();
}
};

// page view for search page
window
.SearchPage = Backbone.View.extend({
render
: function() {
// render search template
this.$el.html(this.template());

// create list view to render collection
this.listview = new MessageListView({
el
: $('ul', this.el),
collection
: this.collection
});

this.listview.render();

return this;
},

events
: {
'keyup .search-query': 'search',
},

search
: function (e) {
var queryVal = $('.search-query').val();

// actual filter logic is in the collection
this.collection.find(queryVal);
},
});

// ul view - wrapper for list items view
window
.MessageListView = Backbone.View.extend({

initialize
: function () {
this.collection.bind('reset', this.render, this);
this.collection.bind('change', this.render, this)
this.collection.bind('add', this.render, this);
this.collection.bind('remove', this.render, this);
},

render
: function () {
var self = this;
this.$el.empty();

// instantiate & pass model to list item view
this.collection.each(function (message) {
self.$el.append(new MessageListItemView({
model
: message }).render().el);
}, this);

return this;
}
});

O problema acima é que cada vez que renderizamos a página de pesquisa – a partir do método de lista padrão do roteador, neste caso – ela cria um novo objeto listview … Então feche-o, você diz ?? Simples o suficiente…

// keep a reference to it and close the old
if (!this.listview) {
this.listview = new MessageListView({ el: $('ul', this.el), model: this.model, collection: this.collection });
}
else {
this.listview.close();
this.listview = new MessageListView({ el: $('ul', this.el), model: this.model, collection: this.collection });
}

this.listview.render();

À primeira vista, pensei que o código acima resolveria meu problema. No entanto, se você seguir a trilha, notará que ainda temos eventos remanescentes vinculados à nossa coleção, embora pareça ter sido removido do DOM … A correção ?

// inside your sub-view or any view that is binding
// to a model or collection
onClose
: function () {
this.collection.unbind();
}

Você poderia simplesmente colocar uma linha como this.collection.unbind () no fechamento do protótipo, no entanto, como nem todas as visualizações têm referência a uma coleção, você ainda precisa verificar se ela existe ou pode fazer como acima e talvez fazer uma limpeza adicional ou desvincular de eventos específicos como:

onClose: function() {
this.collection.unbind('add', this.render);
}

Portanto, se você estiver registrando seus objetos e notar que uma coleção está sendo renderizada mais de uma vez, lembre-se de chamar o unbind na coleção também!

Além disso, decidi reestruturar as visualizações do aplicativo de maneira totalmente diferente e mesclar as visualizações de lista de pesquisa e coleção em uma Visualização … Parece mais limpo, minimizando a quantidade de visualizações instanciando subvisões e mantendo referências a elas. Uma advertência para mover a lógica para uma visão é que você ainda deve ter uma função separada para renderizar a coleção e o modelo de pesquisa ou você acabará redesenhando a página toda vez que digitar uma chave em sua caixa de pesquisa … Filtragem no a coleção aciona uma nova renderização, separando a lógica de algo assim:

// Search Page View
initialize
: function() {
this.template = _.template($('#search').html());

this.collection.bind('reset', this.update, this);
this.collection.bind('change', this.update, this);
this.collection.bind('add', this.update, this);
this.collection.bind('remove', this.update, this);
},

render
: function () {
this.$el.empty();

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

this.update();

return this;
},

update
: function() {
$
('#myList', this.el).empty();

this.collection.each(function (message) {
$
('#myList', this.el).append(new ListItem({ model: message }).render().el);
}, this);

return this;
},

events
: {
'keyup .search-query': 'search',
},

search
: function (e) {
var queryVal = $('.search-query').val();

// filter in collection
this.collection.find(queryVal);
}

Fazer desta maneira significa que só temos a subvisualização de listItem para manter dentro da Visão de Pesquisa, em vez de Visão de Pesquisa -> Visão de Lista -> Visão de Item de Lista .. Além disso, usando um mecanismo de modelagem diferente, como bigode, você pode passe a coleção inteira para o modelo e itere sobre ela. Não tenho certeza de que passar a coleção para um mecanismo de modelo é mais rápido do que iterar em javascript, mas pode ajudá-lo a manter sua sanidade.