前端Backbone源码解读(二)

开场

强烈建议一边看着源码一边读本文章,本文不贴大段代码。源码地址

在写backbone应用的时候,说实话,大部分的时间都是在写这三个模块的内容。关于这三个模块的分析网上随随便便就能找到一堆还不错的文章。但我希望能够找到一条线索,能把各自模块的内部机理整理清楚。就像前一篇文章中介绍的Events那样。Events整个模块其实就是通过一些外部的方法来修改内部对象的属性,从而达到事件管理的目的。以一条线索来看待整个模块,一切都清晰了然了。下面就开始了~

(最近要开学,要准备回学校上课,乱七八糟的东西很多,所以文章可能也会拖一阵子啦...但是我还是非常希望能够写下来,半途而废的感觉真心不好...

这一篇文章主要讲backbone的Model, Collection和View。这三个模块有很多相似的地方。这篇文章不会把模块的每一个方法都介绍一遍,因为只要看源码就知道,其实主要的方法只有几个,而很多其他的模块实际上只是在调用这几个核心的方法而已。

Model & Collection & View

首先讲一下三者的相似之处。这一节让我们来看看这三个模块一个总体结构。

这三个模块在结构上和Events不同。他们先通过以下方式来定义构造函数。(以View为例)


  1. var View = Backbone.View = function(options) { 
  2.     // 构造函数的内容 
  3. };  

构造函数的内部一般会做以下几个操作:

  • 各种给内部对象设置属性。(各种this.a = b)
  • 调用preinitialize

  1. this.preinitialize.apply(this, arguments); 

调用initialize


  1. this.initialize.apply(this, arguments); 

各个模块的方法和属性是通过underscore的extend来获得的。注意在extend新加入的方法和属性中,以下划线开头的变量是内部函数名。(其实理论上用户也可以调用这些方法,谁叫Javascript没有内部变量呢...)这些内部方法是供自己模块内部调用的


  1. _.extend(View.prototype, Events, { 
  2.     // 这里是各种对View.prototype的拓展,定义各种方法 
  3. });  

还有一个比较大的共同点,就是slient参数。这个参数决定了是否要trigger一个事件,在源码用占了很大的篇幅对其进行分类讨论。

Model

关键方法

有一些关键的方法一进入函数就会根据传入的参数的形态进行变化。因为backbone一些方法支持两个参数传入或者一个数组传入,这时候需要有个判断。

set

set方法在model里面是个很不好理解的东西,看了网上大多数解析感觉都很模糊(而且遇到难理解的就用一些借口蒙混过去)。不得不说set里面复杂精妙程度是每读一遍惊叹一遍。

我想以变量的角度来讲解可能是一个比较好的角度。

  • changing和this._changing
    如果这个函数只是从头执行到尾,那说实话,这两个变量没有任何意义。因为他们的值是确定的。看函数开头:
    
    
    1. var changing   = this._changing; 
    2. this._changing = true; 

    在函数结尾:


  1. this._changing = false;  

这个changing将永远永远是false。我上网看到有人说可能是webWorker,多线程相关的东西,但我直接在源码console的时候却发现,这个changing是会变的,而且我用得是todo范例。todo范例没有任何类似webWorker的东西。这个假设猜测应该来说是不正确的。(不过这篇文章讲得也很不错啊)

所以这个changing到底有什么用呢?答案就是递归函数。set里明明没有递归啊?其实递归藏在了所有trigger的事件的回调函数里面。源代码下面的这一段:


  1. // You might be wondering why there's a `while` loop here. Changes can 
  2. // be recursively nested within `"change"` events. 
  3. while (this._pending) { 
  4.     options = this._pending; 
  5.     this._pending = false; 
  6.     this.trigger('change', this, options); 

这一个while里的trigger使得函数发生递归,然后重新调用set。这样的话,下一次changing就等于true了,这个变量的作用才能发挥。可以看一下这个链接里面的讲解。

current变量是用来作为引用改变attributes的,其实是set能设置attributes的本质。
changes数组是用来存放改变了的key的,用于后期的事件触发。
changed & _previousAttributes
把这两个放到一起是因为他们的一个特殊的地方。我在todo的主函数的render里面console,发现不论我做什么操作,changed
=== {},_previousAttributes没有发生改变。后来在查看官方文档的时候,才了解previous的用法:


  1. var bill = new Backbone.Model({ 
  2.   name: "Bill Smith" 
  3. }); 
  4.  
  5. bill.on("change:name", function(model, name) { 
  6.   alert("Changed name from " + bill.previous("name") + " to " + name); 
  7. }); 
  8.  
  9. bill.set({name : "Bill Jones"}); 

set方法在被调用的时候,previous只有在回调函数里才能有用,也就是说,在回调函数外面想要用这个previous获取前一个值是不可能的。它只能获取到当前值。为什么呢?源码做出了解释。当用户做出操作需要用到set方法的时候,其实set方法并不是直接执行完就结束了。在这个方法里面触发了很多的事件,而previous只有在函数里触发了的事件的回调函数“里面”才能返回正确的“前一个值”。changed也同理,因为不论中间如何变化,递归,到最后它会被设置为{}。

save

save方法的作用是把当前model的状态保存到数据库中,因此不可避免地要用到ajax。由于backbone已经有了一个封装好的方法sync用于触发ajax,因此在save当中重点是设置参数。需要设置的有success,error,method。

  • 在success里面会调用用户传入的回调函数并触发sync事件表示已经同步了。
  • error用封装好的wrapError函数,这个函数用得很多,用于处理错误。
  • method根据实际要用那种方法设置
    其中比较值得注意的是wait参数。这个参数会影响页面更新的时机。如果wait是true的话,就会需要等到服务器端相应才更新页面,否则就会立即更新。

destory

destory方法也是与ajax有密切联系的。主要也是设置ajax参数。它分了几种不同的情况并作出了相应的处理:

  • wait是false,不用等待。发起delete请求,触发内部函数destory。
  • wait是true,发起ajax,等待服务器响应才触发destory更新页面。
  • 这是一个新的model,那就不需要发起请求了。

isValid

验证函数,通过调用内部函数_validate,在通过这个函数调用validate函数。然后返回一个错误,如果没有错误就返回true,否则触发invalid,返回false。

Collection

Collection类似一个数组,里面存放着各种以model为结构的对象。在Collection中也有这形式的判断,如果传入的参数是单个对象就会被转换成数组。

set

这是Collection的一个很常用的方法,源码中这一段很长,也有点繁琐,但是没有特别难以理解的地方。整个set的结构是:

  • 设置几个数组(下面会详细讲)
  • 设置实际的models(修改this.models)
  • trigger事件

主要来说就是有如下几个关键点:

  • 如果不符合model形式,转换之。
  • 设置相应的插入位置at。
  • 设置set数组。set数组在里面作用是为给后面排序做准备。里面存放的是新的Collection的models。
  • 设置toAdd数组。这个数组是用于存储新建的合法的model,然后需要调用内部函数_addReference设置索引于_byId数组,并且添加all事件(后面就可以通过model直接trigger事件)。当slient不是true,后期可以通过遍历它来触发add事件。
  • 设置toMerge数组。当这个model是原本已经存在的model的时候(cid匹配),就会修改,然后被push进这个数组中。
  • 设置toRemove数组。然后通过内部函数_removeModels删除那些已经不在set里面的models。
  • 修改this.models,分两种情况,一种是直接整个替换掉,一种是后面再添加。
  • 如果silent不是true就要触发事件。特别值得注意的一点是:这里面的事件有两种,一种事件是由Model发出的,一种事件是有Collection发出的。从Model发出的事件可以很容易_addReference函数中发现

  1. model.on('all', this._onModelEvent, this); 

在这里注册了,调用的是_onModelEvent函数。而其他没有注册的函数应该是给使用者注册监听用的。

sort

sort所依据的是用户传入的comparator参数,这个参数可以是一个字符串也可以是一个函数,如果是字符串就通过underscore的sortBy方法,如果是个函数就直接传入sort的第二个参数中。

fetch & create

fetch和create是backbone与服务器端交互的一个接口。两个方法内部处理其实都很好理解,就是设置ajax参数。最终本质上都是触发sync。但是唯一不同的是fetch是通过自身的sync函数,但create是通过调用model的save,然后触发sync的。在


  1. model.save(null, options); 

跟着这个save函数里面走,就会发现参数null传入是有意义的。在save里面的参数设置会很好地赋值并最后触发sync,而且有一个很有趣的点,就是这个create把model传上服务器,但是这个model是一个相对独立的状态,仅仅通过它的Collection属性来维系和Collection的关系。那就要求后端需要把这一个model添加到相应的Collection数据里面去。

reference

在Collection有一个值得关注的内部变量,那就是_byId,这个变量用cid和id(所以model是一对一对出现的)来存储Collection里面的model,方便直接性的存取。在源码中有很多操作目的就是删除,增加,获取这个内部变量的值。

CollectionIterator

这东西我觉得很有意思...在官方文档里面没有提到,但是由于涉及到ES6的东西所以觉得有点眼前一亮的感觉(哈哈哈),backbone在这里用了Symbol.iterator,具体用法在这个链接里有介绍,还是挺清晰的。通过设置CollectionIterator的Symbol.iterator和next方法。它通过内部变量_kind来区分种类,_index来确定对应的next的结果,这个对于写迭代器还是有点借鉴意义的~

View

在写backbone应用的时候,View写着写着会越来越大...追根溯源,就是View的代码很少...(大雾)。关于View,在写相关代码的时候有一些值是需要设置的(可选的)。下面的代码就展示了可设置的参数,这些参数在View的方法中会用到(如果有的话)。


  1. var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName', 'events']; 

下面我会从两个大的方面来解读源码,一个是element,一个是Events。整个View的源码事实上就是这两组东西。

Element

View字面意思是视图,而在浏览器中,视图就是html所呈现的页面。每一个View事实上就对应着html的一个元素(当然这个元素里面可以有很多很多元素)。这个元素默认标签是div。与元素相关的代码其实很简单,首先要认清this.el和this.$el。前者是真正的节点,后者则是jquery对象的节点。后者由于是jquery式的,因此就可以做相关的jquery的操作。因此事件发起,删除节点,设置属性的操作都是jquery的api对this.$el或其子节点的操作。在进入构造函数的时候会调用一个叫_ensureElement的内部函数,在这个函数里会根据用户设置的参数去构建节点,最后展现到页面之上。

Events

事件是View中非常重要的组成。这是用户可以操作数据的一个接口。在View里面和数据相关的方法有delegateEvents,delegates,undelegateEvents,undelegate。里面通过使用者设置的events属性来创建各种事件,操作各种事件。


  1.     'mousedown .title':  'edit', 
  2.     'click .button':     'save', 
  3.     'click .open':       function(e) { ... } 
  4. }  

events相关代码很简单,但是有一个非常非常巧妙的地方:就是作者用了jquery事件相关api的命名空间。在delegate被调用的时候就给事件加上了一个特定的命名空间。


  1. delegate: function(eventName, selector, listener) { 
  2.     this.$el.on(eventName + '.delegateEvents' + this.cid, selector, listener); 
  3.     return this; 
  4. }  

因此在后续需要对整体的所有事件进行操作的时候就会方便很多很多。

最后的话

这次源码解析不能百分百保证是正确的,有一些混杂了自己的思考。因为不想像其他大部分的源码解析那样,对于问题模糊处理。但我觉得还是有意义的,因为每个人读的角度不一样。兼听则明,也希望读者能够包容,希望深刻理解backbone的读者也请多读几篇文章,多读几遍源码。下一篇文章要写router
& history,这一个模块可以单独拆出来作为SPA的一个入口,个人认为这部分时backbone的backbone(骨架)。

希望能够坚持更下去吧,开学了,事情也开始多了起来...

本人还是backbone小白,如果哪里说错了或者怎样,请轻喷~相互学习~

作者:harryfyodor

来源:51CTO

时间: 2024-08-22 16:21:33

前端Backbone源码解读(二)的相关文章

前端Backbone源码解读(一)

MVC? MVC是一种GUI软件的一种架构模式.它的目的是将软件的数据层(Model)和视图(view)分开.Model连接数据库,实现数据的交互.用户不能直接和数据打交道,而是需要通过操作视图,然后通过controller对事件作出响应,最后才得以改变数据.最后数据改变,通过观察者模式更新view.(所以在这里需要用到设计模式中的观察者模式) Smalltalk-80 MVC Smalltalk-80是早期的对MVC模式的一种实现.这种模式的目的是分离应用的内部逻辑和用户的交互界面.在书中讲述

SEDA源码解读(二)

接着上一篇的话题,本篇继续探讨SEDA的实践项目--sandstorm. 首先,看看package里面的类文件: ResponseTimeControllerIF:该接口代表一个响应时间的控制器,通常情况下被stage的线程管理器执行,以执行事件准入控制策略来达到特定响应时间的目标. StageStatsIF:该接口允许各种各样的系统组件在执行时记录以及收集关于stage的统计. StageWrapperIF:它是一个应用程序stage的内部表示,一个applicationstage包含一个ev

jQuery选择器源码解读(二):select方法_jquery

/* * select方法是Sizzle选择器包的核心方法之一,其主要完成下列任务: * 1.调用tokenize方法完成对选择器的解析 * 2.对于没有初始集合(即seed没有赋值)且是单一块选择器(即选择器字符串中没有逗号), * 完成下列事项: * 1) 对于首选择器是ID类型且context是document的,则直接获取对象替代传入的context对象 * 2) 若选择器是单一选择器,且是id.class.tag类型的,则直接获取并返回匹配的DOM元素 * 3) 获取最后一个id.cl

Backbone源码分析(三)

Backbone源码分析(一)Backbone源码分析(二) Backbone中主要的业务逻辑位于Model和Collection,上一篇介绍了Backbone中的Model,这篇文章中将主要探讨Collection的源码. 让我们先来看一下Collection的构造函数: // Create a new **Collection**, perhaps to contain a specific type of `model`. // If a `comparator` is specified

Ajax::prototype 源码解读_javascript技巧

AJAX之旅(1):由prototype_1.3.1进入javascript殿堂-类的初探  还是决定冠上ajax的头衔,毕竟很多人会用这个关键词搜索.虽然我认为这只是个炒作的概念,不过不得不承认ajax叫起来要方便多了.所以ajax的意思我就不详细解释了. 写这个教程的起因很简单:经过一段时间的ajax学习,有一些体会,并且越发认识到ajax技术的强大,所以决定记录下来,顺便也是对自己思路的整理.有关这个教程的后续,请关注http://www.x2design.net 前几年,javascri

jQuery 1.5 源码解读 面向中高阶JSER_jquery

几乎很难从jQuery分离其中的一部分功能.所以在这里我分享下应该读 jQuery 源码的一些成果,以及读源码的方法.啃代码是必须的. 1. 代码折叠是必须的. 因此必须在支持语法折叠的编辑器里打开源码. 根据折叠层次,我们可以很快知道: 所有 jQuery 的代码都在一个函数中: (function( window, undefined ) {// jQuery 代码 })(window); 这样可以避免内部对象污染全局.传入的参数1是 window, 参数2是 undefined , 加快j

CI框架源码解读之利用Hook.php文件完成功能扩展的方法_php实例

本文实例讲述了CI框架源码解读之利用Hook.php文件完成功能扩展的方法.分享给大家供大家参考,具体如下: 看了hook.php的源码,就知道CI使用hook来进行扩展的原理了. hook的基本知识 http://codeigniter.org.cn/user_guide/general/hooks.html CI中hook的使用经历了一个:开启hook,定义hook,调用hook,执行hook的过程. 手册中已经告知了开启.定义.调用的方法.那么hook的实现原理是啥呢. <?php if

一个引用java接口但任何没有实现的源码解读

问题描述 一个引用java接口但任何没有实现的源码解读 有个类一直无法理解,情况是这样的,该类有个内部接口,确定没有任何实现方法,怀疑代码不全需要自己补充,请高手支招确定下,第一次发帖,望大家捧捧场,谢谢! PresenceEventDispatcher继承的一个引用自己内部接口的抽象类,有"<"刚被csdn隐藏了 public class PresenceEventDispatcher extends EventDispatcher<PresenceEventDispat

jQuery源码解读之removeAttr()方法分析

 这篇文章主要介绍了jQuery源码解读之removeAttr()方法分析,较为详细的分析了removeAttr方法的实现技巧,非常具有实用价值,需要的朋友可以参考下     本文较为详细的分析了jQuery源码解读之removeAttr()方法.分享给大家供大家参考.具体分析如下: 扩展jQuery原型对象的方法: 代码如下: jQuery.fn.extend({ //name,传入要DOM元素要移除的属性名. removeAttr: function( name ) {   //使用jQue