前言
你没有看错,的确是backbone。
首先谈一谈为什么要在9102年做这个事情?
Backbone作为几年前热门的前端MV*框架的老前辈,现在早已被淘汰。但是不妨站在未来回到过去那个时候看一看,以前的编码思路,找一找哪些依然是值得借鉴学习的点,哪些在后来有了新的代替方案,或者说以前没有解决的一些问题现在的流行框架是否已经有了完善解决方案,以一个发展的角度去看事物更替是一件很有意思的事。
官网介绍
Backbone.js为复杂WEB应用程序提供模型(models)、集合(collections)、视图(views)的结构。其中模型用于绑定键值数据和自定义事件;集合附有可枚举函数的丰富API; 视图可以声明事件处理函数,并通过RESRful JSON接口连接到应用程序。
通过Backbone,你可以将数据呈现为 Models,你可以对模型进行创建,验证和销毁,以及将它保存到服务器。 任何时候只要UI事件引起模型内的属性变化,模型会触发"change"事件; 所有显示模型数据的 Views 会接收到该事件的通知,继而视图重新渲染。你无需查找DOM来搜索指定id的元素去手动更新HTML。 — 当模型改变了,视图便会自动变化。现代流行框架依然是保持这个思路去运作的。
P.S.曾经自称“MVC”框架,其实C是collections(collections其实不应该在这里提及容易混淆)
入口函数
Backbone唯一重度依赖的是Underscore。基于RESTful(一个架构样式的网络系统)的约束,histroy的支持依赖于Backbone.Router,DOM处理依赖于 Backbone.View,包括jQuery, 和 json2.js 对旧的IE浏览器的支持。(模仿Underscore 和 jQuery 的APIs,例如 Lo-Dash 和 Zepto,在不同的兼容性下也一样能运行)
值得一提的是,lodash提供的方法到如今仍然有许多适用的场景。而Zepto、Underscore和jQuery其实已经逐渐出现被淘汰的趋势,
(function(factory) {
// Establish the root object, `window` (`self`) in the browser, or `global` on the server.
// We use `self` instead of `window` for `WebWorker` support.
var root = typeof self == 'object' && self.self === self && self ||
typeof global == 'object' && global.global === global && global;
// Set up Backbone appropriately for the environment. Start with AMD.
if (typeof define === 'function' && define.amd) {
define(['underscore', 'jquery', 'exports'], function(_, $, exports) {
// Export global even in AMD case in case this script is loaded with
// others that may still expect a global Backbone.
root.Backbone = factory(root, exports, _, $);
});
// Next for Node.js or CommonJS. jQuery may not be needed as a module.
} else if (typeof exports !== 'undefined') {
var _ = require('underscore'), $;
try { $ = require('jquery'); } catch (e) {}
factory(root, exports, _, $);
// Finally, as a browser global.
} else {
root.Backbone = factory(root, {}, root._, root.jQuery || root.Zepto || root.ender || root.$);
}
})(function(root, Backbone, _, $) {
// 核心代码...
})
官方英文注释很贴心,几乎每一步做什么已经阐述明白。写法也很严谨,先判断了环境再选择引入依赖的方式,之后变开始Backbone的核心内容了。
Event
on、off、trigger、once等常见的事件方法
// Inversion-of-control versions of `on`. Tell *this* object to listen to
// an event in another object... keeping track of what it's listening to
// for easier unbinding later.
Events.listenTo = function(obj, name, callback) {
if (!obj) return this;
var id = obj._listenId || (obj._listenId = _.uniqueId('l'));
var listeningTo = this._listeningTo || (this._listeningTo = {});
var listening = _listening = listeningTo[id];
// This object is not listening to any other events on `obj` yet.
// Setup the necessary references to track the listening callbacks.
if (!listening) {
this._listenId || (this._listenId = _.uniqueId('l'));
listening = _listening = listeningTo[id] = new Listening(this, obj);
}
// Bind callbacks on obj.
var error = tryCatchOn(obj, name, callback, this);
_listening = void 0;
if (error) throw error;
// If the target obj is not Backbone.Events, track events manually.
if (listening.interop) listening.on(name, callback);
return this;
};
// Iterates over the standard `event, callback` (as well as the fancy multiple
// space-separated events `"change blur", callback` and jQuery-style event
// maps `{event: callback}`).
var eventsApi = function(iteratee, events, name, callback, opts) {
var i = 0,
names;
if (name && typeof name === 'object') {
// Handle event maps.
if (callback !== void 0 && 'context' in opts && opts.context === void 0) opts.context = callback;
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.
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;
};
// The reducing API that adds a callback to the `events` object.
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;
};
eventsApi
的作用是对多事件进行解析拆分,遍历执行单个eventname, callback,该方法在on、off、once等中也会调用。
一个主要的点是_events
对象,它以事件名为key,value则是一个包含对象的Array,其中的元素包含了事件触发执行所需的信息(例如cb、上下文对象、被监听对象等相关信息)
如何去实现on、off等我觉得不是该关注的重点,毕竟他的目的也是在基础的事件绑定上去做扩展,这一步jquery也有类似的方案。但是其中的两个方法listenTo
和stopListening
在很多的类库中使用的事件机制都是没有这两个方法的功能。(listenToOnce
字面意思不讨论)
作者备注第一句就是Inversion-of-control versions of on,这是种主从关系的转变,一种是A直接控制B,另一种用控制器(listenTo方法)间接的让A控制B。
这两个方法更像是专为view,model而生的。通过这两个方法可以方便的对view相关的对象监听事件进行跟踪,解绑。绑定关系都存放在this._listeningTo
中,例如view.listenTo(model,'change',changeHandler)
,就完成了当model发生改变去触发view的改变。(当然以前常用的做法也有modal上去on change事件改变view都是可行的)
Modal
Models(模型)是任何Javascript应用的核心,包括数据交互及与其相关的大量逻辑: 转换、验证、计算属性和访问控制。你可以用特定的方法扩展 Backbone.Model,Model 也提供了一组基本的管理变化的功能。
Backbone的modal提供了一系列的方法。例如
- extend供你创建子类
- constructor / initialize初始化方法,modal创建后执行方法
- get/set设置值的方法
……
这一块到没有太多值得深入研究的地方,大体上是一些modal的基本特征。
Collection、Router、History、View
Collection 其实就是Modal+s
Router 与现在的思路基本一致,也是基于hash或者是history去控制view
P.S.不得不说作者也考虑了多重模式 - support the hashchange event, HTML5 history
History 继承了History对象扩展Backbone额外的方法。
View View的配置上除了template选择一个模板(以前通常选择handlebars或者mustache)大体上都是基于view的操作修改。
心得总结
Backbone的几个优点
- 充分发挥函数式编程的精神,符合函数式编程。在当时那个年代的前端领域确实少见。
- 高内聚低耦合可扩展,之前做过的项目也是这样,在基础上拓展。
- 听说现在还支持ES6了,的确很不容易
除去不具备现在热门的vnode,Backbone给人感觉更像是一堆造好的轮子,在以前用模板引擎写页面的时候提供了很大的便利性,即使在现在也有一定的参考价值。
但也因为新的框架逐渐开始自带例如解析模板、双向绑定、vnode、生命周期等等功能,而Backbone需要自己额外去实现,同时也因为重度依赖jquery、underscore,这几者最后都免不了渐渐被淘汰了。
参考
Backbone.js API中文文档
Backbone系列篇之Backbone.Events源码解析
backbone源码解读(一篇全)