backbone初探(二)-辅助函数

写在前面

这篇文章开始动笔之前抽了一周的晚上看源码,对Backbone的源码结构也算是有了整体认识。在宏观已经有了大体了解和有过大体猜测基础上,有了这篇文章,依赖是梳理学习脉络,二来强化记忆便于进一步整理代码。

Backbone的基础公用函数

Backbone代码经过好多年的整理和重构代码变得非常复用,以至于很多基础函数代码很难一下子整理完毕。这里先看看它的基础函数。

extend

backbone很多代码都从underscore里面继承过来,并且对underscrore函数进行了高度的复用。但是它本身的extend函数确实根据自身的需要量身定制的。我们先看看TODO实例里面的几段代码:

1
2
3
4
5
6
7
8
9
10
11
 var Todo = Backbone.Model.extend({
...
});
var TodoList = Backbone.Collection.extend({
...
});
var Todos = new TodoList;
var TodoView = Backbone.View.extend({
...
});
var App = new AppView;

为了精简和明晰,主干代码已经被精简掉了。具体可以查看这里查看更多。
这三段代码就是Todo的DEMO的主题逻辑代码,可以看到backbone并没有像以往很多库和框架一样使用new的方式来实例化构造器。
然后把源码拖到最后几行,可以看到这一段:

1
Model.extend = Collection.extend = Router.extend = View.extend = History.extend = extend;

很显然,不管是M、V还是C,他们的extend都指向了extend函数,我们来看细节:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// Helper function to correctly set up the prototype chain for subclasses.
// 辅助函数->用于正确设置子类的原型链。
// Similar to `goog.inherits`, but uses a hash of prototype properties and
// 类似于`goog.inherits`,但是使用一个原型的hash属性并继承原型。
// class properties to be extended.

// 参数protoProps对象中的属性是所有child实例的公用方法
// 参数staticProps对象中的属性是child类的静态属性
var extend = function(protoProps, staticProps) {
//=>this指向谁?当作为方法调用 指向调用对象本身,也就是说可能会指向Model,View,Events&Collection之一
var parent = this;
var child;

// The constructor function for the new subclass is either defined by you
// (the "constructor" property in your `extend` definition), or defaulted
// by us to simply call the parent constructor.
// 如果定义了protoProps,且protoProps有constructor属性
// 那么protoProps.constructor将作为子类的构造器
// 否则,会定义一个构造器,且构造器里调用了父类的构造函数
if (protoProps && _.has(protoProps, 'constructor')) {
child = protoProps.constructor;
} else {
// 借用parent的构造函数初始化自身
child = function(){ return parent.apply(this, arguments); };
}
// Add static properties to the constructor function, if supplied.
// 将静态属性staticProps以及parent上的类属性添加到child上作为类属性
_.extend(child, parent, staticProps);

// Set the prototype chain to inherit from `parent`, without calling
// 设置继承自parent的原型链 而不调用parent的构造器函数(实际上在上面已经调用过了)
// `parent`'s constructor function and add the prototype properties.
// 并添加原型属性
child.prototype = _.create(parent.prototype, protoProps);
child.prototype.constructor = child;

// Set a convenience property in case the parent's prototype is needed
// later.
child.__super__ = parent.prototype;

return child;
};

前面几段还算好理解,主要麻烦的是原型prototype和constructor还有__super__这三个玩意儿之间的关系了。这里尽量用几句话说完:
众所周知,在面向对象继承这个概念有两个非常基础的概念:类&实例。
但是在Javascript这门语言中,还存在一个叫做原型(prototype)的东西,js依赖它实现继承。
这里做个场景模拟:

1
2
3
4
5
6
var A = function(){};//构造函数
A.prototype.a=function(){};//构造函数的原型
var b = A.prototype.constructor;
var B = new A();
var c = B.__proto__;
var d = B.constructor;

这里首先说明一下,构造器(A)通常会分为两个部分组成:构造函数(line1)+构造函数的原型(line2);
然后指出这里存在的三个对应关系:

  • b === A =>构造器的原型上的constructor会指向构造器的构造函数
  • c === A.prototype => 实例的__proto__属性会指向构造器的原型链
  • d === A =>实例的constructor会指向构造器的原型链

最后,需要补充的是:

  1. constructor属性在实例化时候动态生成,覆盖原来prototype的值
  2. __proto__和__super__其实是不同的引用,__proto__是个别浏览器对原型的内部实现,而__super__仅仅是backbone的内部属性名称而已。

最后来说下代码的意图:
child.prototype = _.create(parent.prototype, protoProps):这里设置了child.prototype为parent和protoProps的混合体;
child.prototype.constructor = child:这里将prototype.constructor指向了本身
child.__super__ = parent.prototype这里将__super__指向了parent.prototype;

——打住!看到这里是不是有似曾相识的感觉?没错,上面代码干了这两件事:

  • 设置原型然后将原型的constructor指向自身构造函数,这里它可以当做一个构造器使用了!
  • 然后它把自己的__super__又给指向了parent.prototype……

有点眼熟了吧,我们离题一下,假设__super__就是__proto__,那么会发生什么奇妙的事情呢?

废话不说了,眼见为实:
JS Bin on jsbin.com

很奇妙的,如果这样写,那么此时这个child会既可以做构造器也可以作为实例来使用原型上的值。
然而__proto__毕竟是个属于浏览器内部实现的属性,所以不管基于什么原因也不该如此写。所以很遗憾的说,以上行为其实在backbone里面并不存在,__super__,其实也就是一个__proto__的替代性的引用。

扯淡了那么多,言归正传,这里说说extend干了什么:

三句话说:

  1. 设置自身的构造函数
  2. 设置自身的原型,并把原型的constructor指向自身以作为构造器使用
  3. 添加一个__super__,用来保持对Model、Collection等这些超类原型的引用。以此为基础可以访问到这些超类的构造器和原型。防止设置了自身的属性后覆盖超类的方法要用时候获取不到.类似es6的super,仅此而已…

一句话说:

  1. 返回一个继承了指定类函数原型和构造器属性并带有一个指向指定类的原型引用的构造函数。

这样,当需要时候,设置好构造器的构造函数,直接像上面代码一样,new一下就ok了。

addUnderscoreMethods

addUnderscoreMethods也是一个可以和extend具有同等地位的函数:
不管是Model还是Collection还是View,通通都用到了这个方法。不同的是extend主要用于暴露给用户使用,而addUnderscoreMethods是用于在内部构建原型使用。
相对于extend来说,addUnderscoreMethods要复杂许多。这里看源码:

1
2
3
4
5
6
// 使用 addMethod方法 添加方法到Class原型上 attribute可能是"attributes"也可能是"models"
var addUnderscoreMethods = function(Class, methods, attribute) {
_.each(methods, function(length, method) {
if (_[method]) Class.prototype[method] = addMethod(length, method, attribute);
});
};

这里来简单解析一下:_.each方法用来遍历方法数组执行回调。在回调内部,执行这样的逻辑:

如果method这个方法在Underscore下有同名的方法,那么为第一个参数的的原型下添加一个同名方法。这个方法的定义调用addMethod来生成。
追溯一下addMethod:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// Proxy Backbone class methods to Underscore functions, wrapping the model's
// 将Backbone类方法代理到Underscore的函数上,包装model的attributes或者
// `attributes` object or collection's `models` array behind the scenes.
// attributes对象或者collection的models数组到该场景后
//
// collection.filter(function(model) { return model.get('age') > 10 });
// collection.each(this.addView);
//
// `Function#apply` can be slow so we use the method's arg count, if we know it.
// `Function#apply`会变慢,所以我们使用方法的参数数量来判定执行方案
var addMethod = function(length, method, attribute) {
switch (length) {
// 1个参数:返回函数->使用method方法处理window的attribute
case 1: return function() {
return _[method](this[attribute]);
};
// 2个参数:返回函数,同上,只是可以多接受一个参数
case 2: return function(value) {
return _[method](this[attribute], value);
};
// 3个参数:返回函数,同上,多接受两个参数:过滤条件和上下文
case 3: return function(iteratee, context) {
return _[method](this[attribute], cb(iteratee, this), context);
};
// 4个参数:返回函数,同上,多接受三个参数,过滤条件、默认值和上下文
case 4: return function(iteratee, defaultVal, context) {
return _[method](this[attribute], cb(iteratee, this), defaultVal, context);
};
// 默认不为以上值时候(0个),返回一个函数:将this的attribute数组中顶部元素压入,返回 _[method].apply(_, args)
default: return function() {
var args = slice.call(arguments);
args.unshift(this[attribute]);
return _[method].apply(_, args);
};
}
};

addMethod的是个非常公用的方法,基本上是为添加underscore到MVC上而存在。这个函数存在的意义主要是为了优化apply性能而生,它转为参数数量进行了分支处理。
当参数数量不同时候,进行了类似重载的功能。需要注意的是:这里this,刚开始初始化Backbone时候this指向了Window,再之后this指向Model、View和Collection。这个如果太抽象的话,那么来个有点实际的情况:当length=1,而attribute为”models”,这时候addMethod实际上就是使用undercore的method方法来处理models指向的数组。
当然,这是最简单的情况,实际上,当lenght为3和4时候,这时候就有个cb函数要处理,我们继续追溯:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Support `collection.sortBy('attr')` and `collection.findWhere({id: 1})`.
// 支持`collection.sortBy('attr')` 和 `collection.findWhere({id: 1})`
// 函数作用:过滤多余对象属性&根据属性查找数组中对象
var cb = function(iteratee, instance) {
//如果iteratee是函数,直接把函数原样返回
if (_.isFunction(iteratee)) return iteratee;
//如果iteratee是Oject同时也是Model,返回modelMatcher过滤的对象
if (_.isObject(iteratee) && !instance._isModel(iteratee)) return modelMatcher(iteratee);
//如果iteratee是字符串,返回model.get(iteratee)
if (_.isString(iteratee)) return function(model) { return model.get(iteratee); };
return iteratee;
};
// 模型匹配函数,使用_的matches来生成闭包,过滤model中指定的属性
var modelMatcher = function(attrs) {
var matcher = _.matches(attrs);
return function(model) {
return matcher(model.attributes);
};
};

cb本身还依赖了modelMatcher,这里顺便一起贴出来,具体实现已经有了,简单说下它的作用:用来过滤多余属性,比如有个对象A:{a:1,b:1},使用modelMatcher(“a”)后会返回{a:1},b被过滤掉了。
简单整理下cb逻辑:

  1. 如果是函数,直接返回函数
  2. 如果是对象且是Model封装。那么返回过滤属性后的Model
  3. 如果是字符串,那么使用model的get方法获取指定属性然后返回。

简而言之,这个cb,就是个属性过滤函数。用来过滤多余的属性。

来个小总结

addUnderscoreMethods方法的总体作用其实如他的名字一样好理解。这里顺了一遍逻辑发现确实如此,如果非要说这个代码细节阅读一次之后的收获,那么应该是从这个对参数处理分支的角度可以发现,不同的underscore方法传参时候参数是存在规律的。这里顺便归纳一下:

  1. 1个参数的:一个参数的接受的数据类型不确定,但是共同点是都是逻辑运行需要的数据
  2. 2个参数的:如_.pluck(list, propertyName)
  3. 3个参数的:参考_.sortBy(list, iteratee, [context])
  4. 4个参数的:参考:_.reduce(list, iteratee, [memo], [context])

这里需要注意的是!
backbone根据自身逻辑来进行了部分设计,所以backbone在对underscore分配length长度时候有时候并没有完全根据underscore实际接受数据来,比如reduce实际上接受4个参数,但是分派的lenght却是0,这样可以直接apply来引用。除了如此,实际上方法参数长度===2的,也并不存在。

实际设计过程中存在的有3种:0,1和3.

eventsApi

如果说上面2个方法是各个大模块都会用到,那么eventsApi这个应用范围就会变少很多,它只是在Event这个上面用到,应用范围变小很多,但是它本身是设计精巧,高度复用,应该是AOP的完美实例。

PS:AOP:AOP(面向切面编程)的主要作用是把一些跟核心业务逻辑模块无关的功能抽离出来,这些 跟业务逻辑无关的功能通常包括日志统计、安全控制、异常处理等。把这些功能抽离出来之后, 再通过“动态织入”的方式掺入业务逻辑模块中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// Iterates over the standard `event, callback` (as well as the fancy multiple
// 迭代 兼容标准的`event, callback` 同时支持空格分隔多事件`"change blur", callback`
// space-separated events `"change blur", callback` and jQuery-style event
// 以及jquery的对象式的{event: callback}映射
// maps `{event: callback}`).
// eventsApi实质上是利用iteratee来实现操作events对象并返回,eventsApi
// 实际上只是处理了不同参数类型的问题,这里iteratee是onApi或者offApi
var eventsApi = function(iteratee, events, name, callback, opts) {
var i = 0, names;
if (name && typeof name === 'object') {//jQuery-style event map
// Handle event maps.
if (callback !== void 0 && 'context' in opts && opts.context === void 0) opts.context = callback;
//_.keys获取对象的键名,返回一个包含键名的数组
for (names = _.keys(name); i < names.length ; i++) {
//将对象转换为单个字符串式,然后循环调用自身
events = eventsApi(iteratee, events, names[i], name[names[i]], opts);
}
} else if (name && eventSplitter.test(name)) {
// Handle space-separated event names by delegating them individually.
//判断name存在并由空格分隔
//切割成数组循环传入函数自身进入下一分支环节
for (names = name.split(eventSplitter); i < names.length; i++) {
events = iteratee(events, names[i], callback, opts);
}
} else {
// Finally, standard events.
events = iteratee(events, name, callback, opts);
}
return events;
};

这段代码有三个分支,一共接受3种类型的值已完善使用体验,它接受 对象空格分隔的字符串如”click change”字符串三种。实际上真正处理业务逻辑的只有第三种逻辑,逻辑是这样的:

  1. 如果是对象,将对象的键名全部取出来组成数组作为参数传入自身(步骤1),将数组遍历作为参数传入自身进入分支3(步骤2)
  2. 如果是空格分隔的字符串,那么split变成数组,执行步骤2
  3. 如果是字符串,直接进入分支3

eventsApi作为AOP编程方法的实践,它处理的是公共的逻辑。

而eventsApi除了使用了AOP的的思想很值得称赞外,它还在自身内部实现了递归,这种递归在AOP的情景下发挥了很重要的作用:

几乎所有最重要的逻辑都交由分支3处理,这样保持了传入参数的一致性,只需要维护分支3的参数传递即可,其他的分支只需要对参数类型进行转换然后递归调用自身进入分支3即可,防止了3个分支各自维护自己的参数顺序造成可能的混乱。

而且相对来说,降低了源码阅读的难度,这里我们看看eventsApi有哪些iteratee传入:onApi、offApi、onceMap、triggerApi。函数很明晰了,这些iteratee涵盖了绑定、解绑、一次性绑定和事件触发4个方案的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// The reducing API that adds a callback to the `events` object.
// 增加了一个回调到`events`对象。
// 这个函数用来处理events对象,为events内部指定监听的事件对应的数组添加一个回调函数,同时将options的linstening数量+1
// 返回一个events对象(实际上已经实质性操作了,返回是为了方便操作)
var onApi = function(events, name, callback, options) {
if (callback) {
var handlers = events[name] || (events[name] = []);
var context = options.context, ctx = options.ctx, listening = options.listening;
if (listening) listening.count++;

handlers.push({callback: callback, context: context, ctx: context || ctx, listening: listening});
}
return events;
};

贴一下onApi的源码,这个事件绑定的内部细节可以实证一下第一篇的想法,使用Pub/Sub即订阅发布模式来处理事件。同时也排除了可能使用Dom方式如AddEventListener来绑定事件的猜测。
backbone Event内部完全使用了Pub/Sub模式,这样的操作的最大好处是:DOM无关,那么与DOM相关的兼容性在backbone Event这个模块内部也就不再存在。当然还有一个好处是容易维护和扩展。

总结

这篇暂时就写这三个辅助函数或者也能叫做公用方法了。Backbone内部大面积使用了这些,如果不对其做深入的研究,那么下一步也相当困难也无法继续深入。本文对其进行了相对深入的了解和探索。到此也就告落一段。从下一篇就要开始整理Model、Event、View和Collection了。

先收工!