现在的位置: 首页 > Frameworks > Backbone > 正文
Backbone视图概述及源码分析
2017年06月23日 Backbone ⁄ 共 7607字 暂无评论 ⁄ 被围观 271 views+
文章目录
[隐藏]

视图的作用是根据数据和业务组织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函数解析

照抄官网扩展的一个例子:

  1. var DocumentRow = Backbone.View.extend({
  2.     tagName: "li"// 覆盖默认的tagName
  3.     template: _.template('<p>Hello</p>'), // 扩展的属性
  4.     className: "document-row"// 覆盖默认的className
  5.     events: { // 覆盖默认的events
  6.         "click .icon""open",
  7.         "click .button.edit""openEditDialog",
  8.         "click .button.delete""destroy"
  9.     },
  10.     initialize: function() { // 覆盖默认的initialize
  11.         this.listenTo(this.model, "change"this.render);
  12.     },
  13.     render: function() { // 覆盖默认的render
  14.         this.$el.html(this.template());
  15.         return this;
  16.     }
  17. });
constructor

构造函数,也就是子类本身。如果扩展的时候不设置这个属性,则Backbone.View会创建一个默认的构造函数,默认构造函数内部只调用父视图的构造函数。例如以下扩展后,DocumentRow === ChildClass

  1. function ChildClass() {
  2.     return Backbone.View.apply(this, arguments);
  3. }
  4. var DocumentRow = Backbone.View.extend({
  5.     constructor: ChildClass
  6. });
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对象里面,易于事件统一管理,例如:

  1. {
  2.     "dblclick":"open",
  3.     "click .icon.doc":"select",
  4.     "contextmenu .icon.doc":"showMenu",
  5.     "click .show_notes":"toggleNotes",
  6.     "click .title .lock":"editAccessLevel",
  7.     "mouseover .title .date":"showTooltip"
  8. }
$

上下文被限制在el内部的jQuery选择器,这样可以较大程度的限制查询出来的元素是本视图的内部元素(即el的子节点),防止跟其它视图内部的节点互相影响。view.$('.list')等同于$('.list', view.el)

template

template并不是视图自有的属性,不过它可以作为一个好的编码习惯,将视图对应的模板赋值给这个变量,可以视为一个约定,例如:

  1. var LibraryView = Backbone.View.extend({
  2.   template: _.template(...)
  3. });
render

render属于最关键的一个方法了,Backbone.View自带的render方法基本没有做任何操作。render方法的使命就是生成DOM,并把生成的DOM插入到el中。

对于DOM的生成,大多是推荐使用一些模板引擎,handlerbar之类的。尽量避免直接用JavaScript拼写html字符串的方式生成DOM。

另外render方法最后一般都把视图实例返回(renturn this),这样就可以链式调用了,官网的样例代码:

  1. var Bookmark = Backbone.View.extend({
  2.   template: _.template(...),
  3.   render: function() {
  4.     this.$el.html(this.template(this.model.attributes));
  5.     return this;
  6.   }
  7. });
remove

删除视图,注意的是el自身也会被删除,内部调用的jQuery的remove方法,所以委托在el上的事件也被移除了,不用单独处理。另外通过listenTo绑定在视图上的所有事件(如果使用model的话,一般会用视图监听model)也会被移除。

delegateEvents

将事件委托在el上,Backbone.View已经完整的实现了这个功能,扩展的时候,一般不需要再定制delegateEvents方法。需要注意的是,事件是委托在视图根节点的,在事件冒泡到el之前,冒泡是无法被阻止的。

undelegateEvents

取消委托在el上的事件,Backbone.View已经完整的实现了这个功能,扩展的时候,一般不需要再定制undelegateEvents方法。

源码解析

Backbone.View的源码很简单,可以说只是一共了一个简单的模板,以供派生:

  1. // 视图的作用是根据数据和业务组织DOM,最终渲染到页面中。结合Backbone的Model,可以做到模型数据后,界面自动更新
  2. // 不过这“界面自动更新”不是天上掉下来的,需要业务代码维护Model,监听Model,并在监听事件中更新DOM
  3. // 所以很多时候,Model使用得并不多,更多人还是习惯用jQuery去操作DOM
  4. // 项目到优化提升阶段的话,还是提倡使用Model来维护数据和DOM的
  5. // Backbone.View的构造函数
  6. // 完成以下事情
  7. // 为每一个视图实例生成一个唯一标志
  8. // 把实例化时传入的model,collection,el,id,attributes,className,tagName,events几个属性写到视图实例上
  9. // 确保视图实例有一个el作为容器
  10. // 调用initialize方法
  11. var View = Backbone.View = function(options) {
  12.     // 给视图实例生成一个唯一标志,存储在cid属性中
  13.     this.cid = _.uniqueId('view');
  14.     // 分拣model,collection,el,id,attributes,className,tagName,events参数,合并到实例属性上
  15.     _.extend(this, _.pick(options, viewOptions));
  16.     // 确保视图实例有一个el作为容器(没有设置el的话,自动创建)
  17.     this._ensureElement();
  18.     // 调用初始化方法
  19.     this.initialize.apply(this, arguments);
  20. };
  21. // events属性的分析表达式,第一个子表达式匹配事件类型,第二个子表达式匹配jQuery选择器
  22. // 例如 "click button.ok"最终会这样绑定事件: $el.on('click', 'button.ok', function(){...});
  23. var delegateEventSplitter = /^(\S+)\s*(.*)$/;
  24. // 构造函数只接受这些参数,其它参数被忽悠
  25. var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName', 'events'];
  26. // 安装事件模型到View的原型上,同时设置一些基础的属性或者接口到View原型上
  27. _.extend(View.prototype, Events, {
  28.     // 设置默认的tagName为div
  29.     tagName: 'div',
  30.     // 视图的选择器方法,只在el内部查询指定元素,等于是上下文被限制在el上的全局$。
  31.     $: function(selector) {
  32.         return this.$el.find(selector);
  33.     },
  34.     // 定义默认的初始化方法,默认没有任何实现
  35.     // 如果要在视图实例化的时运行一些代码,则在扩展视图的时候要重写该方法
  36.     initialize: function() {},
  37.     // 视图的核心方法,默认没有实现,由扩展视图实现
  38.     // 主要要根据业务生成DOM,并且将DOM插入到el中
  39.     // 返回视图实例,有助于链式调用
  40.     render: function() {
  41.         return this;
  42.     },
  43.     // 移除视图(整个el节点被移除)
  44.     // 同时也移除视图实例对Model的监听
  45.     remove: function() {
  46.         this._removeElement();
  47.         this.stopListening();
  48.         return this;
  49.     },
  50.     // 内部方法,移动视图所在的el,因为调用的jQuery remove方法,所以委托在el上的事件也全部被删除。
  51.     _removeElement: function() {
  52.         this.$el.remove();
  53.     },
  54.     // 设置视图的$el和el属性,并且重新委托事件到新el上,删除老el上的事件委托
  55.     setElement: function(element) {
  56.         this.undelegateEvents();
  57.         this._setElement(element);
  58.         this.delegateEvents();
  59.         return this;
  60.     },
  61.     // 内部方法,仅仅用于设置视图实例的el和$el属性
  62.     _setElement: function(el) {
  63.         this.$el = el instanceof Backbone.$ ? el: Backbone.$(el);
  64.         this.el = this.$el[0];
  65.     },
  66.     // 委托所有事件,不传events参数的话,将把this.events上的所有事件委托在el上
  67.     delegateEvents: function(events) {
  68.         events || (events = _.result(this, 'events'));
  69.         if (!events) return this;
  70.         this.undelegateEvents();
  71.         for (var key in events) {
  72.             var method = events[key];
  73.             if (!_.isFunction(method)) method = this[method];
  74.             if (!method) continue;
  75.             var match = key.match(delegateEventSplitter);
  76.             this.delegate(match[1], match[2], _.bind(method, this));
  77.         }
  78.         return this;
  79.     },
  80.     // 单个事件的实际委托函数
  81.     delegate: function(eventName, selector, listener) {
  82.         this.$el.on(eventName + '.delegateEvents' + this.cid, selector, listener);
  83.         return this;
  84.     },
  85.     // 删除委托在el上的全部事件,注意使用了.delegateEvents命名空间
  86.     undelegateEvents: function() {
  87.         if (this.$el) this.$el.off('.delegateEvents' + this.cid);
  88.         return this;
  89.     },
  90.     // 删除委托在el上的部分事件,根据选择器和事件处理函数过滤
  91.     undelegate: function(eventName, selector, listener) {
  92.         this.$el.off(eventName + '.delegateEvents' + this.cid, selector, listener);
  93.         return this;
  94.     },
  95.     // 内部方法,创建一个DOM节点
  96.     _createElement: function(tagName) {
  97.         return document.createElement(tagName);
  98.     },
  99.     // 内部方法,确保总存在一个el作为视图的容器
  100.     // 即,不设置el的情况下,视图自动生成一个el作为容器
  101.     _ensureElement: function() {
  102.         if (!this.el) {
  103.             var attrs = _.extend({},
  104.             _.result(this, 'attributes'));
  105.             if (this.id) attrs.id = _.result(this, 'id');
  106.             if (this.className) attrs['class'] = _.result(this, 'className');
  107.             this.setElement(this._createElement(_.result(this, 'tagName')));
  108.             this._setAttributes(attrs);
  109.         } else {
  110.             this.setElement(_.result(this, 'el'));
  111.         }
  112.     },
  113.     // 内部方法,设置el的属性
  114.     _setAttributes: function(attributes) {
  115.         this.$el.attr(attributes);
  116.     }
  117. });

给我留言

留言无头像?


×