本文已同步在 我的博客
在这个 react 和 vue 如日中天、 jquery 逐渐被大家抛弃的年代,我还是想要来说一说 backbone 。
16年6月初,在没有任何前端框架使用经验、js水平也较一般的情况下,被告知需要在几个工作日内搭建完成一个后台管理系统,没有页面设计稿、没有组件库,一切都是从零开始。当时面临两个选择,backbone和react。虽然我很希望能够拿react来练手,但是考虑到学习成本和项目时间问题,leader还是建议我使用backbone来完成,就这样,一直用到差不多现在。虽然到项目后期业务场景越来越复杂,backbone的这套技术栈体现出越来越多的问题,但是对于小型项目来说,我还是认为backbone是个不错的选择,而且学习成本低,上手极快~
backbone是个非常轻量的 mvc 库,本文将基于 backbone 的源码谈一谈其实现的核心部分,以及其中一些巧妙的思想和代码实现技巧。
事件机制(Events)
事件部分的核心逻辑其实比较简单,简化一下可以用如下的伪代码来表示:
var events = { evt1: handlers1, evt2: handlers2, ... } //注册事件 function on(name, callback) { const handlers = events[name] || (events[name] = []); handles.push(callback); } //触发事件 function trigger(name) { if (!events[name]) { return; } const handlers = events[name]; for (let i = 0, len = handlers.length; i < len; i++) { handlers[i](); } } //解除绑定 function off(name) { delete events[name]; }
当然了,以上写法有很多细节的地方没有加入进来,比如 上下文绑定 、 对多种传参方式的支持 、 触发事件时对事件处理器传参的处理 等等。
我们知道,对于 MVC 来说, M(模型) 的变化会反映在 V(视图) 上,实际上是视图监听了模型的变化,再根据模型去更新自身的状态,这当中最重要的一个功能就是监听 (listen) 。该功能也是由 Events 部分实现的,包括: listenTo 、 stopListening 等
listenTo和 on 类似,都是监听一个事件,只不过 listenTo 是监听其他对象的对应事件,而 on 是监听自身的对应事件。 stopListening同 理。比如:
a.on('testevent', function(){ alert('1'); }); a.trigger('testevent');
如果其他对象希望监听 a 的 testevent 事件呢?则可以通过 listenTo 来实现:
b.listenTo(a, 'testevent', function() { alert('catch a\'s testevent'); })
其中第一个参数为要监听的对象,第二个参数为事件名称
当调用 on 方法的时候,会为对象自身创建一个 _event 属性;而调用 listenTo 方法时,会为监听对象创建 _event 属性,同时为了记录监听者,被监听对象还会创建一个 _listeners 属性:
a.on('testevent', handlers1);
a会变成:
{ _events: { testevent: [handlers1] }, on: function() { //... } ... }
当有其他对象监听 a 时,如:
b.listenTo(a, 'testevent', handlers2);
a会变成:
{ _events: { testevent: [handlers1, handlers2] }, _listeners: b, on: function() { //... } ... }
在事件机制的实现部分,除了核心逻辑之外,在对一些方法的使用上,也很考究。为了绑定函数执行的上下文,我们经常会使用 apply , call 这些方法,而源码中多次提到 apply 的执行效率要低一些,因此,有这样的实现:
// A difficult-to-believe, but optimized internal dispatch function for // triggering events. Tries to keep the usual cases speedy (most internal // Backbone events have 3 arguments). var triggerEvents = function(events, args) { var ev, i = -1, l = events.length, a1 = args[0], a2 = args[1], a3 = args[2]; switch (args.length) { case 0: while (++i < l) (ev = events[i]).callback.call(ev.ctx); return; case 1: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1); return; case 2: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2); return; case 3: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2, a3); return; default: while (++i < l) (ev = events[i]).callback.apply(ev.ctx, args); return; } };
有关为什么 call 比 apply 的效率更高的解释可以参考 这篇文章
模型(Model)
model用于维护数据,其中最关键的是对数据的更新部分,即 set
// Trigger all relevant attribute changes. if (!silent) { if (changes.length) this._pending = options; for (var i = 0; i < changes.length; i++) { this.trigger('change:' + changes[i], this, current[changes[i]], options); } } // You might be wondering why there's a `while` loop here. Changes can // be recursively nested within `"change"` events. if (changing) return this; if (!silent) { while (this._pending) { options = this._pending; this._pending = false; this.trigger('change', this, options); } } this._pending = false; this._changing = false; return this;
每次 set 数据的时候,根据数据变化的部分,使用 trigger 方法触发相应的事件;在 set 数据时,如果不希望触发 change 事件,可以设置 silent 为 true 。
这部分比较容易让人产生疑惑的是 while 循环部分,这个 while 循环有什么用呢?举个例子:
new Model.on("change", function() { console.log('model change'); }).set({ a: 1 });
以上代码是最简单的情况,监听 change 事件,当 model 变化时,打印出 model change
在源码中,当第一次进入 while 后,紧接着 this._pending 被置为 false ,而事件触发回调函数也不会更改 this._pending 的值,因此再次判断时条件不成立, while 内的代码段只会执行一次。
但是实际情况往往不是这么简单,如代码注释中所说,有可能会有嵌套的情况,比如:
new Model.on("change", function() { this.set({ b: 1 }) }).set({ a: 1 });
在这种情况下,第一次 trigger 触发 change 的回调函数中,又再次对 model 进行了更新操作,
this.set({ b: 1 })
每次 set 时,会更新 this._pending 为 true ,这样当 set b 后,就会再次进入 while 内,触发 change 事件。而如果没有使用 while 循环的话,对 b 属性更新的操作就无法触发 change 事件,导致其监听者到无法根据最新的数据更新自身状态。
视图(View)
View部分的实现比较简单,其中最主要的是 events 部分,通常在一个 View 中,都会绑定一些 dom 事件,比如:
{ 'click .preview-btn': 'preview', 'click .save-btn': 'save' }
主要有两点需要说明:
- backbone 中是采用的事件委托的方式绑定事件,因此,一些不冒泡的事件,比如 scroll ,是无法通过这样的方式绑定的
- 回调函数会保持正确的 this 指向。 backbone 内部进行了处理
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; }
结语
以上部分介绍了 backbone 中最核心部分的实现机制。可以看到其实现非常的简单,但是对于小型项目来说,确实可以帮我们做一些对数据的维护和管理工作,提高开发效率。但是随着业务逐渐复杂,会越来越发现, backbone 所能做的实现有限,而对于数据维护部分也非常不方便,尤其是需要是对多个模块间的通信和数据维护问题。后续我会结合在复杂业务中的使用谈一谈 backbone 的缺点,以及更优的框架能带来的便利。
说句题外话,虽然去年由于时间原因选择了 backbone ,这一年基本没有在复杂业务场景中使用 react 技术栈,都是自己做个小 demo 练手。但是也正是因为有了使用 backbone 去写复杂业务的经历,在数据维护上和模块间通信上非常麻烦,以及 backbone 渲染 dom 时直接全部更新的会导致的页面渲染性能问题,才更让我感觉 react + redux 的美好。知其然,还需知其所以然啊~ ~
不然我觉得我可能会一直疑惑为什么要用一套这么复杂的技术栈,异步请求这块写起来还那么麻烦。这么看,坏事也算是好事了吧~~
注:本文内容来自互联网,旨在为开发者提供分享、交流的平台。如有涉及文章版权等事宜,请你联系站长进行处理。