内存泄漏
事件机制可以很好的带来代码维护的便利,但是由于事件绑定会使对象之间的引用变得复杂和错乱,容易造成内存泄漏。下面的写法就会造成内存泄漏:
var Task = Backbone.Model.extend({}) var TaskView = Backbone.View.extend({ tagName: 'tr', template: _.template('<td><%= id %></td><td><%= summary %></td><td><%= description %></td>'), initialize: function(){ this.listenTo(this.model,'change',this.render); }, render: function(){ this.$el.html( this.template( this.model.toJSON() ) ); return this; } }) var TaskCollection = Backbone.Collection.extend({ url: 'http://api.test.clippererm.com/api/testtasks', model: Task, comparator: 'summary' }) var TaskCollectionView = Backbone.View.extend({ initialize: function(){ this.listenTo(this.collection, 'add',this.addOne); this.listenTo(this.collection, 'reset',this.render); }, addOne: function(task){ var view = new TaskView({ model : task }); this.$el.append(view.render().$el); }, render: function(){ var _this = this; //简单粗暴的将DOM清空 //在sort事件触发的render调用时,之前实例化的TaskView对象会泄漏 this.$el.empty(); this.collection.each(function(model){ _this.addOne(model); }) return this; } })
使用下面的测试代码,并结合Chrome的堆内存快照来证明:
var tasks = null; var tasklist = null; $(function () { // body... $('#start').click(function(){ tasks = new TaskCollection(); tasklist = new TaskCollectionView({ collection : tasks, el: '#tasklist' }) tasklist.render(); tasks.fetch(); }) $('#refresh').click(function(){ tasks.fetch({ reset : true }); }) $('#sort').click(function(){ //将侦听sort放在这里,避免第一次加载数据后的自动排序,触发的sort事件,以至于混淆 tasklist.listenToOnce(tasks,'sort',tasklist.render); tasks.sort(); }) })
点击开始,使用Chrome的'Profile'下的'Take Heap Snapshot'功能,查看当前堆内存情况,使用child类型过滤,可以看到Backbone对象实例一共有10个(1+1+4+4):
之所以用child过滤,因为我们的类继承自Backbone的类型,而继承使用了重写原型的方法,Backbone在继承时,使用的变量名为child,最后,child被返回出来了
点击排序后,再次抓取快照,可以看到实例个数变成了14个,这是因为,在render过程中,又创建了4个新的TaskView,而之前的4个TaskView并没有释放(之所以是4个是因为记录的条数是4)
再次点击排序,再次抓取快照,实例数又增加了4个,变成了18个!
那么,为什么每次排序后,之前的TaskView无法释放呢。因为TaskView的实例都会侦听model,导致model对新创建的TaskView的实例存在引用,所以旧的TaskView无法删除,又创建了新的,导致内存不断上涨。而且由于引用存在于change事件的回调队列里,model每次触发change都会通知旧的TaskView实例,导致执行很多无用的代码。那么如何改进呢?
修改TaskCollectionView:
var TaskCollectionView = Backbone.View.extend({ initialize: function(){ this.listenTo(this.collection, 'add',this.addOne); this.listenTo(this.collection, 'reset',this.render); //初始化一个view数组以跟踪创建的view this.views =[] }, addOne: function(task){ var view = new TaskView({ model : task }); this.$el.append(view.render().$el); //将新创建的view保存起来 this.views.push(view); }, render: function(){ var _this = this; //遍历views数组,并对每个view调用Backbone的remove _.each(this.views,function(view){ view.remove().off(); }) //清空views数组,此时旧的view就变成没有任何被引用的不可达对象了 //垃圾回收器会回收它们 this.views =[]; this.$el.empty(); this.collection.each(function(model){ _this.addOne(model); }) return this; } })