博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

转: Ext拖拽分析

Posted on 2015-02-09 16:50  sunjie  阅读(2535)  评论(1编辑  收藏  举报

整个Ext架构中组件是其重要的组成部分,除了少部分(如树的结点)的界面表现元素不是在这样的一个体系中,大部分的页面表现元素都被绑定在这个体系之中,下面从这个体系的最底层即在这个继承体系的最高层进行研究:

1.     Ext.util.Observable

所有在这个体系中的元素都需要有事件的支持,在这个系统最顶层是一个事件的发布系统,它为系统中的所有元素提供事件支持。Observable原意是看得见的,可观察的,也就是一个Observable对象是一个可以被观察的对象,它可以被谁观察呢?如果在面向对象的语言应该是可以被对象进行观察,在JS中这里的观察者通常都是由一个方法来充当,但可以为方法指定一个运行空间或者说方法所依附的对象。一个Observable对象可以:

         添加管理的事件;

         可以添加被管理事件的监听;

         可以删除事件的监听;

         通过relayEvents方法可以将另一个Observable对象的事件触发转接来,即当另一个Observable对象发生事件时,当前的Observable也可以感知;

         可以暂停也可以在暂停之后恢复事件的发布;

         可以判定一个事件是否存在监听器;

         可以为一个监听器添加before与after拦截,之后也可以随时对这些拦截取消;

         可以使用静态方法为一个指定一个Observable对象添加一个统一的发布事件前拦截,之后也可以使用相反的静态取消这个统一的拦截

与Observable密切相关的一个类对象是Ext.util.Event对象,它是由Observable对象管理的事件的实体,它只是一个普通的事件实体,它有事件名称,拥有此事件的对象(即Observable对象),以及绑定此事件上的监听的集合,这是事件三大成员,这里的事件与DOM结构中的事件还有一定的距离,这只是一个很简单的属性包装,要将此事件与DOM事件结合,还要在这个类的子类中进行相就的转换处理。

 

Ext3的事件分析:

 

Ext3的事件监听模式,是在一个较高的级别上事件发布系统,它实际与浏览器事件,没有什么关系。整个管理模式就是组件或者说一个Observable管理理着一些Ext.util.Event对象,而每一个Ext.Util.Event对象又管理着与此事件相关的监听方法(是被装饰之后的方法),对于一个Observable来说它可以添加事件,也可以添加监听方法,最后很可能它在适当的时候又触发一些事件(委托对应的事件对象来执行它管理的方法)。整个过程基本是一个封闭的结构.或者说是一个完整的事件发布系统

 

但是有一个问题就是这里的事件体系并没有与浏览器事件进行结合,所以说这只是Ext3整个事件体系的一部分,我将它称为Ext标准事件子体系,或称之为高级事件子体系。Ext3的事件体系的另外一个部分是与浏览器相关的事件,我将它称之为低级事件子体系,或者叫底层事件子体系。事件体系的这个部分主要与Element对象相关,因为一个低级事件总是与Dom对象相关的,而Element是Dom对象的一个包装,在低级事件中相关的对象主要有Element,Ext.EventManager,Ext.lib.Event,Ext.EventObject在低级事件系统中围绕事件发生的对象是dom对象,而在高级事件系统中发生的事件的对象通常情况下是组件或者说是一个Observable的子类。Ext.lib.Event是一个静态类,它在定义的时候是没有构造器的,它只有方法,它操作的对象全部是以dom为中心,它使用私有的局部容器来管理监听方法,它与高级事件系统中的事件对象有相似之处,但也有很大的不同,除了构造器之外,就是低级事件触发是不需要手动触发的,而是由浏览器自动触发的。Ext.lib.Event它只是管理监听方法,它自己并不是一个事件对象,真正的事件对象是Ext.EventObject,这个对象是浏览器事件对象经过标准化之后的一个对象,它的事件属性要比在高级事件体系中的事件对象信息要丰富很多。可以说Ext.lib.Event与Ext.EventObject两个全在一起基本上完成在高级事件系统中Ext.util.Event对象所完成的功能。而Ext.EventManager恰好是将这两个对象进行了组合,这个类就完成整个低级事件的管理

 

在一个组件当中往往需要感知浏览器,而在组件当中,Ext采用的是高级事件体系,因为这样要更加灵活,比如可人为根据定义事件,可以随时发布事件,也可以随时取消事件的发布。因为就涉及到两个个事件系统之间的沟通问题,这件事件一般是在Ext组件的内部根据逻辑的需要,就是转换的,大部分的情况是当一个低级的浏览器事件发生是组件希望触发一个高级,Ext组件内部一般采用的做法是,为组件的相关Element(实际是它包装的dom)绑定一个低级的监听,而在这个低级的监听方法中来发布一个Ext的高级事件。关于这个沟通在Component组件类中有两个支持方法:mon(绑定)与mun (御载),分别是为组件的相关元素绑定与御载低级监听的方法

 

2.     Ext.Component

这个类可以说是Ext所有组件的基类,Observable虽然在继承体系的最上层但是它不具有实体,只是因为组件类需要事件管理所以才将它置于类的最顶层,其实任何需要事件管理的类都可以继承Observable。而Component是一个实际意义上的组件基类,因为它与看的见DOM实体进行联系,整个Component的结构为页面看得见的小窗体的部件元素搭建了一个抽象的框架类,规定界面元素的大体走向,与常见功能的实现,先从此基类的构造方法说起:

         组件基类首先对构造的配置参数进行处理,如果配置参数config中如果存在initialConfig则会忽略其它的配置参数,直接将initialConfig作为config使用;

         如果config中存在tagName或dom属性或者说config是一个字符串,则将config看成一个dom配置对象来看,它将会被看一个页面的DOM结构中已经存在的实体,此组件的applyTo属性将会指向此dom元素;

         在对配置参数config进行处理之后,组件的initialConfig在成员将会保存这个经过处理后的config对象,所以程序处理中如果想获取初始的参数可以从这里获取;

         接着组件会将config中的所有成员拷贝到看的成员中,作为自己的直接配置成员;

         接着组件会添加自己管理的一些最基础的事件;

         生成组件的唯一标识id,如果已经指定了id则使用指定的;

         向组件管理器注册此组件;

         调用超类(Observable)的构造方法以对配置的监听进行初始化;

         调用initComponent方法来进行组件的额外的初始化操作(空方法),通常这个方法是留给子类来根据具体的情况进行初始化,所此方法是留给子类来实现的,或者说是留给子在的一个初始化的一个钩子方法;

         如果组件配置了插件则将当前组件作为参数传递给插件的init方法来进行插件的初始化,这也就是说插件必须实现init方法且接受一个组件作为参数;

         初始化组件相关的一些用cookie记录的状态,例如长度,宽度等;

         如果定义了applyTo或者说renderTo属性则提前进行初始化,applyTo与renderTo区别是applyTo指定一个存在的dom元素的父结点作为此组件的容器,而renderTo直接指定的一个存在的dom元素作为组件的容器;

         至此整个组件的构造初始化操作完成。

构造的完成并不意味着组件的创建完成,构造完成只是说为创建组件准备好的基础数据,实际组件的创建需要一个渲染的过程,当然如果存在一个applyTo或renderTo属性,则此操作已经提前完成了,此基类中的渲染流程,将会对所有子组件的渲染起到一个规范作用,下面是基类组件的渲染流程:

         首先判定组件是否已经被渲染,如果没有则发布beforrender渲染前事件,如果没有受到事件阻止,则开始渲染操作;

         渲染操作接受两个参数,第一个为作为容器CT的dom结点,第二个为渲染到此结点的哪一个位置,可以指定容器子结点的一个索引或直接指定容器的子结点,如果没有指定容器则会使用配置属性el的父结点作为容器(此种情况对于el所指定的元素来说是不能移动的,类似于applyTo属性指定的元素);

         为容器结点添加样式类ctCls;

         标记组件已经被渲染;

         调用onRender方法来进行渲染;

                        i.              onRender方法与render方法接收相同的参数,如果指定了autoEl属性则会根据此属性创建一个元素对象,并将此元素对象赋予this.el成员,因此这里要特别小心,也就是如果你配置了autoEl属性,则对el属性的配置最终都会被覆盖,这会产生很让人迷惑的现象与页面效果,你可以同时指定一个存在的容器与一个存在的el,而你没有指定autoEl属性,这会使得,这会全已经存在的el结构被移入指定的容器中,当然这不会存在问题;有两种情况会导致Dom结构无法进行移动,实际是一种情况:1、如果指定的applyTo属性;2、如果指定的el但是没有指定容器对象CT,如果不能移动DOM结构,对于一个autoEl属性是没有意义的,因为这个凭空创建的元素最终不能进入到页面的Dom结构中。所以要把握两点,如果autoEl属性存在一定要使用allowDomMove属性为true,实际中就是不要设置applyTo属性;如果没有autoEl属性,allowDomMove属性可以为true与可以为false

                      ii.              在有了el元素之后将el的dom结构插入到容器对象中(当然allowDomMove属性为true的时候才会这样);

                    iii.              最后在el元素上添加overCls事件监听

         如果autoShow属性为true则移除el元素上的隐藏类;

         如果配置了cls属性则向el元素添加组件的类挂钩;

         如果配置了style属性则向el元素添加此样式;

         发布“render”渲染事件;

         调用afterRender方法,参数此组件的容器对象(这是留给子类的挂钩方法);

         如果配置隐藏hidden为true则隐藏此组件;

         如果配置禁用disabled为true则禁用此组件;

         至此完成组件的渲染

另外组件基类还实现组件的一些基本功能,比如获取焦点或失败失去焦点;启用与禁用;隐藏与显示;

 

3.     Ext.BoxComponent

此类是所有使用一个盒容器的可视化组件的一基类,此类主要提供了一个自动盒模型的调整,当组件尺寸与位置改变的时候,此类提供的主要配置属性有x,y,pageX,pageY, height,width;提供的主要方法有,设置组件的尺寸与位置;获取组件的尺寸与位置,组件这两属性改变都委托组件的实体el元素来完成的,在改变位置与尺寸的时候都发布相应的事件,另外在改变尺寸的时候会调用onResize方法,这是一个给子类留下的钩子方法,以便子组件在改变尺寸的时候子类能够添加额外的逻辑;在改变位置的时候人调用onPosition方法,这也是留给子类的钩子方法;另外还有两个调整方法adjustSize与adjustPosition方法可以留给子类进行微调尺寸与位置用;还有两个方法getResizeEl与getPositionEl这两个方法用来获取实际尺寸与位置改变的元素,因为有小时候可能el不是需要改变的元素

 

此类还重写了onRender方法,它在超类的渲染方法调用完之后,主要做了一件事,就是看子类有没有额外设置resizeEl与positionEl,如果设置了则将它们作为尺寸与位置改变的实际元素;另外此类还重写了afterRender方法,主要是用来在组件渲染完成之后根据设置的位置与大小来更新组件

 

4.     Ext.Container

此类预期被用来作为管理其它组件的一个容器,可以把看成是一批关联组件的管理者,这里的组件是普遍意义下的组件,并不一定要是此类的子类,虽然通常都是它的子类

 

Ext的一个主要目标就是在WEB页面上实现类似窗体的程序,因此其中的小窗体组件是其中的一个重要组成部分,当然小窗体组件属性是属于高层封装,所以它对一些较底层的操作是小依赖的,下面就从最常见的小窗体部件:按钮开始研究

5.         Ext.Button

Ext.Button类的实例代表了页面上的一个按钮,一般构造的按钮都是按照默认的模板对一个button元素进行包装的,当然如果有现成的按钮已经做好了也可以直接使用,这时可以配置一个属性:dhconfig来替代默认的包装形式,dhconfig可以是一个DOM对象也可以是一段HTML片段,用它来代表一个按钮的主体。当使用自定义的按钮时,大部分的配置属性都不能用,例如text,cls,icon,iconCls,tabIndex,tooltip都将无效。默认的按钮包装使用表格对button元素进行包装,分为左中右三个单元格。默认按钮使用的图片高度固定为21px,也就是说按钮的高度固定为21px高,其中涉及的几最重要的CSS类:x-btn、x-btn-wrap、x-btn-left、x-btn-right、x-btn-center。这里的按钮的主要作用有:一般的事件处理;充当状态切换开关;充当菜单触发按钮

 

这里临时记录一个问题,就是当表单面板的尺寸发生改变时,如果让表单元件的尺寸也随着或者不随着表单的大小而改变,本来这个问题是很简单的,只需要为表单元素指定了auchor属性,这些元件就会随着一起变化,但往往表单元件有时候被嵌套的层次比较深,如果其中任何一个面板不能监听这种变化,那么这些表单元件的大小很可能就不会改变,尤其对于那些auchorLayout布局或者其子类布局的面板,只有对这种容器中的组合配置auchor属性才会监听这种变化;另外还发现一个需要小心的问题,当组件配置了auchor属性,而在它的父容器的defaults属性也配置了字段auchor那么会出现当前第一渲染的时候采用的是defaults中的配置值,而监听这种变化时却是采用组件的实际配置值,这有时候会给人一种迷惑的感觉

 

Ext中的拖动:

Ext将拖动看成是元素与元素之间的作用,从整个拖动操作上看,拖动主要可以分为两类实体,一类实体是被拖动的对象或者说是可以被拖动的对象;一类实体是接收实体的对象或者说可以接收其它实体的对象。Ext中将这两类实体包装成同一类对象,可以被称为DD实体或者说DD对象,每一种实体与三种dom元素相关联:链接元素;控制元素;象征元素。如果不加以细分可以简单的认为这三个元素就是同一个元素,即被拖动的元素。在整个拖动过程中,Ext将每一个发生的细节规范成了DD对象的各种回调方法,对于这些回调用方法主要有:

 

onAvailable() : void

开发者可以在子类中覆盖此方法,来完成子类需要初始化的逻辑

onMouseDown(Event e):void

当用户的鼠标在对象上按下时第一个回调

startDrag(int X,int Y):void

在构建完成DD管理器此次拖动操作的环境后调用

onDrag(Event e):void

当拖动一个DD对象正在移动的时候将调用此方法(所以如果要即时修改参数可以覆盖此方法)

onDragEnter(Event e,String|DragDrop[] id):void

当被拖动的DD对象进入在另一个DD对象上的时时候回调(注意方法参数)

onDragOver(Event e,String|DragDrop[] id):void

当被拖动的DD对象悬停在另一个DD对象上的时时候回调(注意方法参数)

onDragOut(Event e,String|DragDrop[] id):void

当被拖动的DD对象退出在另一个DD对象上的时时候回调(注意方法参数)

onDragDrop(Event e,String|DragDrop[] id):void

当被拖动的元素被放下的时候将调用此方法,注意方法的参数

endDrag(Event e):void

当被拖动的元素完成拖动的时候将调用此方法

onMouseUp(Event e):void

当鼠标键释放的时将调用此方法

onInvalidDrop(Event e):void

这个是一个比较特别的回调方法,什么时候会调用此方法呢?在系统发布dragdrop事件时可能需要回调此方法,

它的回调条件是系统通过计算没有发现适合的接收者,就会回调此方法,它可以看是非法放置修正回调处理器

 

 

那么这些回调方法是如何协作的呢?下面分析整个拖动过程与回调方法的对应关系,以及在Ext中的实现原理。

 

在虽然我们说拖动包括两类实体,但是在Ext中是以被拖动的元素为主体,但事实上这两种实体是相对的,拖动元素随时也是目标元素,而目标元素随时也可能成为拖动元素,所以其中之一进行编程处理也是合理的。要完成一个拖动过程,

         首先需要创建一个DD对象(其中包括向DD管理器注册,以及绑定mousedown事件)在mousedown事件处理方法之前将有一个onAvailable回调方法,以完成子类的初始化

         在一个DD对象被创建时系统只为我们绑定了一个mousedown事件处理方法,因此这个事件处理方法是触发整个拖动过程的入口。在这个事件处理方法中主要会调用一个onMouseDown回调方法,接着当前DD对象会将会将控制权交给DD管理器继续进行处理

         DD管理器会以传入的DD对象为基础来构建此次拖动操作的运行环境,其中最重要的一个环境就是dragCurrent表示当前正在被拖动的DD对象。完成环境构建之后,DD管理器会回调当前DD对象的startDrag回调方法。到这里为止被拖动元素的使命已经完成,那么接下来的操作将如何完成呢?

         从上面的分析看程序似乎已经走到了尽头,但现在拖动才开始,秘密就是对body的监听方法,分析如下:在整个拖动流程中,提供实体的一方(或者说被拖动者)一般是一个DD对象,而接收实体的一方(或者说等待接收)往往是多个DD对象,它们都是接收实体的"候选者",最终有可能被拖动的实体,被其中的一个候选者接收,也有可能没有一个候选者会接收到这个实体,因为有可能用户在拖动的时候没有拖到任何一个候选者的接收区域之内.因为有多个DD实例是候选者,这里采用了对body元素上的mouseup与mousemove事件进行了监听。那么系统究竟是如何对这两个事件进行监听的呢?

         从拖动的先后顺序来看,肯定是先发生mousemove事件,然后才发生mouseup事件,所以这里先看mousemove事件监听情况。这个事件的监听是由方法handleMouseMove方法来完成,此方法有一个重要的生命周期回就是对当前被拖动的DD对象的onDrag回调方法的调用,接着此监听会调用一个重要的方法,但它是不是一个生命周期的回方法,这个方法的名称是fireEvents,这个方法是用来做什么的呢?它主要查用来在DD管理器中发布事件的,我们说发布事件实际是调用回调方法,只不过发布是对多个对象进行回调,前面已经分析接收实体的DD对象也需要知道当前被拖动的DD对象的状态,以便作出响应,这里便用这个fireEvents方法进行了代理完成,可以看到前面的回调都是针对被拖动的DD对象来调用的,而这里的fireEvents方法将着眼于那些准备接收实体的DD对象之上,它可以计算出,哪些对象该触发dragover,哪些对象该触发dragenter,哪些对象该触发dragout事件。在这里便有了被拖动DD对象与其它DD对象的相互作用,但触发这三个事件的方式要特别留意,正常情况情况下应该是回调用其它对象的相应回调方法(依次为:onDragEnter,onDragOveronDragOut)但Ext在这里做了一个巧妙的处理,它将这些回调转嫁给了被拖动的对象的相应方法,所做的调整就是将本该触发这些事件的DD对象(或id)传递给了这些方法,所以这些事件的布实际也变成了对被拖动的DD对象的方法回调,这样可以让开发人员更专注于一个对象的思考

         接下来看对mouseup事件的监听情况。对此事件的监听DD管理器使用方法handleMouseUp方法,那么此方法又做了哪些事情呢?首先调用fireEvents方法来发布dragdrop事件,处理的方式与上一步调用fireEvents方法的处理方式相同,也是将方法回调转嫁给了当前被拖动的对象(onDragDrop),接此方法调用当前被拖动的DD对象的endDrag回调方法;接着再调用当前被拖动DD对象的onMouseUp回调方法,至此就完成了整个拖动的过程

 

以下是对上述回调接口几个系统的实现情况分析:

         Ext.dd.DD

这类主要实现了dragEl,即象征元素一起移动的功能,因为需要时刻修改象征元素的位置,所实现需要回调onDrag方法以达到实时的效果,所以系统采用了如果实现:

b4Drag: function(e) {

        this.setDragElPos(e.getPageX(), e.getPageY());

},

         Ext.dd.DDProxy

这个实现类对Ext.dd.DD类进行扩展,主要添加了一项代理功能,因为Ext.dd.DD实现了象征元素与鼠标一起移动的功能,但默认情况是象征元素与被拖动元素是同一个元素,往往这不是很好,而这个使用了一个创建虚拟的元素来替换象征元素,这样使得象征元素与链接元素进行了分离,不过这个类在完成结束拖动的操作时候又分离出了两个回调,一个在链接元素移动前,另一个是在链接元素移动后具有代码如果下

endDrag: function(e) {

 

        var lel = this.getEl();

        var del = this.getDragEl();

 

        // Show the drag frame briefly so we can get its position

        del.style.visibility = "";

 

        this.beforeMove();

        // Hide the linked element before the move to get around a Safari

        // rendering bug.

        lel.style.visibility = "hidden";

        Ext.dd.DDM.moveToEl(lel, del);

        del.style.visibility = "hidden";

        lel.style.visibility = "";

 

        this.afterDrag();

    },

所以对于endDrag方法覆盖要特别小心,因为这个方法已经被系统占用,要覆盖可以选择beaforeMove或afterDrag方法

         Ext.dd.DDTarget

此类直接继承自Ext.dd.DragDrop类,并且去除对链接元素mousedown事件的监听方法,因此它也失去了触发拖事件功能,它也不可能被拖动,因为它不能作为拖动流程的入口点,但是它可以作为一个接收其它实体的DD对象而存在,只是它自己不能被拖动而已

         Ext.dd.DragSource

这个类扩展自Ext.dd.DDProxy主要在两方面加强了功能:代理的样式与以及代理的复位处理,关于代理的样式,此类采用了一个标准的代理类Ext.dd.StatusProxy。其实这两个方面具有支持都是由这个类来支持的,并对代理拖动的流程进行了进一步规范,抽取出一批before型与after型的回调方法

         Ext.dd.DragZone

这个类做得工作并不多,它直接继承于Ext.dd.DragSource,此类做的一个唯一的变化是对代理对象进行了注册管理可以参考:Ext.dd.Registry

         Ext.dd.DropTarget

这个类继承自Ext.dd.DDTarget类主要对放置对象,对于被拖动的DD对象的一个回应操作的规范,如果进入到当前目标对象时如何响应;如果悬停到当前目标对象时如何响应;当放置到当前目标对象上时如何响应等一系列的notify系列方法。其实在分析之初就已经说了Ext将重点着眼于被拖动的DD对象,对于一个接收其它实体的DD对象基本没有什么关注,因为在整个拖动过程中如果DD对象是一接收者,则在Ext.dd.DragDrop类中定义的方法全部都不会被调用,因为前面已经分析了enter,over,out,drop等事件发生,回调用被转移到被拖动的DD对象上去了。但是这样会有一个问题,拖动如果最终要有效果那么势必要对接收的DD对象进行相应的操作,一个比较可行的办法就是在被拖动的DD对象的onDragEnter,onDragOver, onDragOut,onDragDrop等回调用方法中对目标DD对象进行处理,那么怎么处理呢?方法很自由,也很多,可以发挥你的无限想像力来完成各种效果,但整个代码都有一个基本的套路,那么Ext.dd.DragSource类将这个基本套路进行了规范化,覆盖了onDragEnter,onDragOver, onDragOut,onDragDrop等方法,在这些方法中对相应的目标对象进行了回调处理,或者说此类对象目标DD对象的回调操作进行规范化,而Ext.dd.DropTarget类恰好是对这个规范的一个呼应,Ext.dd.DropTarget对这些规范进行简单的实现,这些回调接口都是以notify开头:notifyEnter,notifyOver,notifyOut,notifyDrop其中notifyDrop是需要一个真正的业务实现的地方,一般需要子类来完成,通常是移动元素等

         Ext.dd.DropZone

这个类表示一个放置区域,它扩展自Ext.dd.DropTarget,这个似乎对超类的几个接口方法进行了改写具体还需要研究一下