视图的作用是根据数据和业务组织DOM,最终渲染到页面中。结合Backbone的Model,可以做到模型数据后,界面自动更新,不过这“界面自动更新”不是天上掉下来的,需要业务代码维护Model,监听Model,并在监听事件中更新DOM。所以很多时候,Model使用得并不多,更多人还是习惯用jQuery去操作DOM。
Backbone提供了一个非常基础的视图,定义了一些接口,以及统一事件注册的方式。仅仅靠这个基础视图,在实际的项目中基本是无法使用的,真正的使用方式都是基于Backbone.View进行扩展。我们先看一下Backbone.View定义的接口。
Backbone.View API
extend
用法:Backbone.View.extend(properties, [classProperties])
extend方法用于基于Backbone.view派生用户自定义视图。一般来说render方法是肯定要重写的,因为Backbone.view的render方法基本上是一个空函数,其它属性全部可以重写,只是一般不需要。扩展的时候大部分都是新增接口。
extend方法并不是Backbone.View专有的,而是Backbone内部实现的一个函数,对于改函数的解析,请参照我的这篇文章:Backbone之extend函数解析
照抄官网扩展的一个例子:
- var DocumentRow = Backbone.View.extend({
- tagName: "li", // 覆盖默认的tagName
- template: _.template('<p>Hello</p>'), // 扩展的属性
- className: "document-row", // 覆盖默认的className
- events: { // 覆盖默认的events
- "click .icon": "open",
- "click .button.edit": "openEditDialog",
- "click .button.delete": "destroy"
- },
- initialize: function() { // 覆盖默认的initialize
- this.listenTo(this.model, "change", this.render);
- },
- render: function() { // 覆盖默认的render
- this.$el.html(this.template());
- return this;
- }
- });
constructor
构造函数,也就是子类本身。如果扩展的时候不设置这个属性,则Backbone.View会创建一个默认的构造函数,默认构造函数内部只调用父视图的构造函数。例如以下扩展后,DocumentRow === ChildClass
- function ChildClass() {
- return Backbone.View.apply(this, arguments);
- }
- var DocumentRow = Backbone.View.extend({
- constructor: ChildClass
- });
initialize
初始化函数,在视图实例化的时候(例如 new DocumentRow(options)),initialize函数会被调用,默认的initialize是个空函数。定义这个接口可以做一些视图的初始化工作。
el
视图依附的文档节点,其值有以下几种情况:
- DOM对象
- jQuery对象
- 各种jQuery选择器
- 函数(返回值必须是前三种之一)
- 不设置(或者false,null),这种情况下,则根据
id, tagName, className, attritutes
这几个属性生成一个el
el不一定已经存在于文档流中,特别是使用了layoutmanager后,大部分子视图的el都是依附于父视图的DocumentFragment中,先在DocumentFragment中操作,然后随着根视图的render,一次性将节点插入文档流。不过根视图的el一般情况下都已存在于文档流。
$el
el属性对于的jQuery对象,起到一个缓存作用,即 $el === $(el)
,扩展视图时,该属性不用设置,只要设置el
属性就可以了。
setElement
将视图转移到别的DOM上,除了$el,el属性被更新外(用新的DOM代替了),委托在老el上的事件也被转移到新el上。扩展视图时,该方法一般不需要重写,Backbone.View默认的setElement已经实现主要功能了。
attributes
这个属性只有在不设置el的时候才会生效,在Backbone.View自动生成el的时候,会把attributes当成el节点的文档属性写进去,其实就是调用的jQuery attr方法:$el.attr(attributes)
,其实可以是函数(返回属性对象)
className
和attributes类似,只有在不设置el的时候生效,最终会被写到Backbone.View自动生成el的class属性中,其值可以是函数(返回样式字符串)。
id
和attributes类似,只有在不设置el的时候生效,最终会被写到Backbone.View自动生成el的id属性中,其值可以是函数(返回字符串)。
tagName
和attributes类似,不过有个默认值:"div",在不设置el的时候生效,Backbone.View默认自动生成的el是div节点。也可以设置成其它的html标签,例如"ul",则生成的el就是一个ul元素。
events
事件对象,视图内部事件的绑定,提倡统一写到events对象里面,易于事件统一管理,例如:
- {
- "dblclick":"open",
- "click .icon.doc":"select",
- "contextmenu .icon.doc":"showMenu",
- "click .show_notes":"toggleNotes",
- "click .title .lock":"editAccessLevel",
- "mouseover .title .date":"showTooltip"
- }
$
上下文被限制在el内部的jQuery选择器,这样可以较大程度的限制查询出来的元素是本视图的内部元素(即el的子节点),防止跟其它视图内部的节点互相影响。view.$('.list')
等同于$('.list', view.el)
template
template并不是视图自有的属性,不过它可以作为一个好的编码习惯,将视图对应的模板赋值给这个变量,可以视为一个约定,例如:
- var LibraryView = Backbone.View.extend({
- template: _.template(...)
- });
render
render属于最关键的一个方法了,Backbone.View自带的render方法基本没有做任何操作。render方法的使命就是生成DOM,并把生成的DOM插入到el中。
对于DOM的生成,大多是推荐使用一些模板引擎,handlerbar之类的。尽量避免直接用JavaScript拼写html字符串的方式生成DOM。
另外render方法最后一般都把视图实例返回(renturn this
),这样就可以链式调用了,官网的样例代码:
- var Bookmark = Backbone.View.extend({
- template: _.template(...),
- render: function() {
- this.$el.html(this.template(this.model.attributes));
- return this;
- }
- });
remove
删除视图,注意的是el自身也会被删除,内部调用的jQuery的remove方法,所以委托在el上的事件也被移除了,不用单独处理。另外通过listenTo绑定在视图上的所有事件(如果使用model的话,一般会用视图监听model)也会被移除。
delegateEvents
将事件委托在el上,Backbone.View已经完整的实现了这个功能,扩展的时候,一般不需要再定制delegateEvents方法。需要注意的是,事件是委托在视图根节点的,在事件冒泡到el之前,冒泡是无法被阻止的。
undelegateEvents
取消委托在el上的事件,Backbone.View已经完整的实现了这个功能,扩展的时候,一般不需要再定制undelegateEvents方法。
源码解析
Backbone.View的源码很简单,可以说只是一共了一个简单的模板,以供派生:
- // 视图的作用是根据数据和业务组织DOM,最终渲染到页面中。结合Backbone的Model,可以做到模型数据后,界面自动更新
- // 不过这“界面自动更新”不是天上掉下来的,需要业务代码维护Model,监听Model,并在监听事件中更新DOM
- // 所以很多时候,Model使用得并不多,更多人还是习惯用jQuery去操作DOM
- // 项目到优化提升阶段的话,还是提倡使用Model来维护数据和DOM的
- // Backbone.View的构造函数
- // 完成以下事情
- // 为每一个视图实例生成一个唯一标志
- // 把实例化时传入的model,collection,el,id,attributes,className,tagName,events几个属性写到视图实例上
- // 确保视图实例有一个el作为容器
- // 调用initialize方法
- var View = Backbone.View = function(options) {
- // 给视图实例生成一个唯一标志,存储在cid属性中
- this.cid = _.uniqueId('view');
- // 分拣model,collection,el,id,attributes,className,tagName,events参数,合并到实例属性上
- _.extend(this, _.pick(options, viewOptions));
- // 确保视图实例有一个el作为容器(没有设置el的话,自动创建)
- this._ensureElement();
- // 调用初始化方法
- this.initialize.apply(this, arguments);
- };
- // events属性的分析表达式,第一个子表达式匹配事件类型,第二个子表达式匹配jQuery选择器
- // 例如 "click button.ok"最终会这样绑定事件: $el.on('click', 'button.ok', function(){...});
- var delegateEventSplitter = /^(\S+)\s*(.*)$/;
- // 构造函数只接受这些参数,其它参数被忽悠
- var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName', 'events'];
- // 安装事件模型到View的原型上,同时设置一些基础的属性或者接口到View原型上
- _.extend(View.prototype, Events, {
- // 设置默认的tagName为div
- tagName: 'div',
- // 视图的选择器方法,只在el内部查询指定元素,等于是上下文被限制在el上的全局$。
- $: function(selector) {
- return this.$el.find(selector);
- },
- // 定义默认的初始化方法,默认没有任何实现
- // 如果要在视图实例化的时运行一些代码,则在扩展视图的时候要重写该方法
- initialize: function() {},
- // 视图的核心方法,默认没有实现,由扩展视图实现
- // 主要要根据业务生成DOM,并且将DOM插入到el中
- // 返回视图实例,有助于链式调用
- render: function() {
- return this;
- },
- // 移除视图(整个el节点被移除)
- // 同时也移除视图实例对Model的监听
- remove: function() {
- this._removeElement();
- this.stopListening();
- return this;
- },
- // 内部方法,移动视图所在的el,因为调用的jQuery remove方法,所以委托在el上的事件也全部被删除。
- _removeElement: function() {
- this.$el.remove();
- },
- // 设置视图的$el和el属性,并且重新委托事件到新el上,删除老el上的事件委托
- setElement: function(element) {
- this.undelegateEvents();
- this._setElement(element);
- this.delegateEvents();
- return this;
- },
- // 内部方法,仅仅用于设置视图实例的el和$el属性
- _setElement: function(el) {
- this.$el = el instanceof Backbone.$ ? el: Backbone.$(el);
- this.el = this.$el[0];
- },
- // 委托所有事件,不传events参数的话,将把this.events上的所有事件委托在el上
- delegateEvents: function(events) {
- events || (events = _.result(this, 'events'));
- if (!events) return this;
- this.undelegateEvents();
- for (var key in events) {
- var method = events[key];
- if (!_.isFunction(method)) method = this[method];
- if (!method) continue;
- var match = key.match(delegateEventSplitter);
- this.delegate(match[1], match[2], _.bind(method, this));
- }
- return this;
- },
- // 单个事件的实际委托函数
- delegate: function(eventName, selector, listener) {
- this.$el.on(eventName + '.delegateEvents' + this.cid, selector, listener);
- return this;
- },
- // 删除委托在el上的全部事件,注意使用了.delegateEvents命名空间
- undelegateEvents: function() {
- if (this.$el) this.$el.off('.delegateEvents' + this.cid);
- return this;
- },
- // 删除委托在el上的部分事件,根据选择器和事件处理函数过滤
- undelegate: function(eventName, selector, listener) {
- this.$el.off(eventName + '.delegateEvents' + this.cid, selector, listener);
- return this;
- },
- // 内部方法,创建一个DOM节点
- _createElement: function(tagName) {
- return document.createElement(tagName);
- },
- // 内部方法,确保总存在一个el作为视图的容器
- // 即,不设置el的情况下,视图自动生成一个el作为容器
- _ensureElement: function() {
- if (!this.el) {
- var attrs = _.extend({},
- _.result(this, 'attributes'));
- if (this.id) attrs.id = _.result(this, 'id');
- if (this.className) attrs['class'] = _.result(this, 'className');
- this.setElement(this._createElement(_.result(this, 'tagName')));
- this._setAttributes(attrs);
- } else {
- this.setElement(_.result(this, 'el'));
- }
- },
- // 内部方法,设置el的属性
- _setAttributes: function(attributes) {
- this.$el.attr(attributes);
- }
- });