我的三次代码重构之路

  最近这段时间相对比较清闲,所以决定将最近学习积累的东西以及对于代码重构的经历做一下总结。

  事情是这样的。。。14年1月份我参加了我人生的第一份正式工作,当时领导为了检验我的能力,让我着手开发一个小项目,项目第一个页面的基本需求是这样的:

  1. 开发一个可以呈现具体年月日信息的时间轴,理论上该时间轴是可以无限拖动的;
  2. 支持两种时间粒度的呈现,按年呈现或按月呈现,两种呈现方式的切换动画要平滑自然;
  3. 给定一个具有起止时间的事件,可以精确定位到时间轴对应的位置进行绘制事件bar条,假如事件的起止时间相交,要能够按照时间顺序进行换行;
  4. 要使用到缓存机制;

  也许这样的一个组件在各位看来很简单,但是对于当时的我来说,简直是一头雾水。当时的我还没正式离开学校,所谓的编程经验仅仅体现在自己在学校看过AS3.0这门编程语言,连一个基本的类都没有写过,各位一定很好奇我是怎么通过面试的,其实我也很好奇。所以当我拿到这个需求的时候,我直接就懵了,无限拖动?动画?还要平滑自然?绘制?用什么绘制?怎么绘制?缓存机制又是个什么东西?

  好了,不扯那些没用的了,总之在领导不厌其烦的讲解、一次又一次的悉心指导下,组件总算坑坑洼洼的搞出来了。说了这么多,仅仅是想讲一下我这三次重构,重构的到底是个什么东东。

  下面就来说说当初是怎么实现的。

  1. 既然要呈现年月日,那我就搞三个控件YearComponent、MonthComponent、DayComponent,控件本身只做绘制刻度、显示日期的事情。但是对于MonthComponent来说,按年呈现时,每一月的宽度固定;按月呈现时,每一月的宽度为每一天的宽度*该月的总天数;
  2. 有了三个基本控件,还需要给他们搞一个容器TimeAxisComponent。既然要能实现精准定位,那该容器就必须得有一个基准时间,所有事件的位置都是依据事件本身的起止时间和基准时间计算而来,所以容器必须提供一个方法用于给定任意的时间,就可精准定位该时间的位置。
     1 /**根据randomTime与benchmarkTime相差多少个月,计算randomTime相对于benchmarkTime的位置*/
     2 public function locationX( randomTime:Date ):Number{
     3     if( monthOrYear == "year" ){
     4         var randomTimeXPosition:Number = locationXByYear( randomTime );
     5     }else{
     6         randomTimeXPosition = locationXByMonth( randomTime );
     7     }
     8     return randomTimeXPosition;
     9 }
    10 
    11 private function locationXByYear( randomTime:Date ):Number{
    12     var monthWidth:Number = yearWidth/12;
    13     var numMonthsApart:uint;
    14     var randomYear:Number = randomTime.fullYear;
    15     var randomMonth:Number = randomTime.month;
    16     var benchmarkYear:Number = benchmarkTime.fullYear;
    17     var benchmarkMonth:Number = benchmarkTime.month;
    18     if( randomYear > benchmarkYear ){
    19         numMonthsApart = 11 - benchmarkMonth + ( randomYear - benchmarkYear - 1 ) * 12 + randomMonth + 1;
    20         var randomTimeXPosition:Number = benchmarkTimePosition + numMonthsApart * monthWidth;
    21     }else if( randomYear < benchmarkYear ){
    22         numMonthsApart = benchmarkMonth + ( benchmarkYear - randomYear - 1 ) * 12 + 12 - randomMonth;
    23         randomTimeXPosition = benchmarkTimePosition - numMonthsApart * monthWidth;
    24     }else{
    25         if( randomMonth >= benchmarkMonth ){
    26             numMonthsApart = randomMonth - benchmarkMonth;
    27             randomTimeXPosition = benchmarkTimePosition + numMonthsApart * monthWidth;
    28         }else{
    29             numMonthsApart = benchmarkMonth - randomMonth;
    30             randomTimeXPosition = benchmarkTimePosition - numMonthsApart * monthWidth;
    31         }
    32     }
    33     
    34     /**计算randomTime.month有多少天*/
    35     var forNumDaysMonthCom:MonthComponent = new MonthComponent();
    36     forNumDaysMonthCom.year = randomYear;
    37     forNumDaysMonthCom.month = randomMonth;
    38     var numDays:uint = forNumDaysMonthCom.numDays();
    39     
    40     randomTimeXPosition = randomTimeXPosition + randomTime.date * monthWidth/numDays;
    41     
    42     trace( randomTimeXPosition, randomTime.fullYear, randomTime.month, randomTime.date );
    43     return randomTimeXPosition;
    44 }
    45 
    46 private function locationXByMonth( randomTime:Date ):Number{
    47     var randomMillisecond:Number = randomTime.time;
    48     var benchmarkMillisecond:Number = benchmarkTime.time;
    49     if( randomMillisecond >= benchmarkMillisecond ){
    50         var numDays:Number = ( randomMillisecond - benchmarkMillisecond )/86400000;
    51         var randomTimeXPosition:Number = benchmarkTimePosition + numDays * 35;
    52     }else{
    53         numDays = ( benchmarkMillisecond - randomMillisecond )/86400000;
    54         randomTimeXPosition = benchmarkTimePosition - numDays * 35;
    55     }
    56     return randomTimeXPosition;
    57 }
    View Code
  3. 既然要使用缓存机制,那就搞三个缓存数组用于分别暂存控件YearComponent、MonthComponent和DayComponent。
  4. 在利用缓存的基础上实现拖动,那就必须判断如果控件超出可视区域,就将其从舞台卸载并添加到缓存中;如果可视区域需要补充控件,则要么从缓存中取,要么创建一个新的控件。并且补充的这个控件的日期需要依据前一个控件的日期进行计算:假设现在朝右拖动,处在屏幕最左侧的MonthComponent的日期是2014.3,那么新补充的MonthComponent的日期则应为2014.2,以此类推,这样即可实现无限拖动。还有一点,这里既然有三种控件,那就必须对三种控件分别处理。
      1 private function timeAxisMoveRemoveCom():void{
      2     //处理 “年”组件
      3     for( var i:uint=0;i<yearsGroup.numElements;i++ ){
      4         var yearCom:YearComponent = yearsGroup.getElementAt( i ) as YearComponent;
      5         //只需判断第一个和最后一个是否超出边界即可,其他的不用判断
      6         if( i == 0 || i == yearsGroup.numElements - 1 ){
      7             if( yearCom.x + yearCom.width < 0 || yearCom.x > this.width ){
      8                 yearsRemoveArray.push( yearCom );
      9                 yearsBufferArray.push( yearCom );
     10             }else{
     11                 yearCom.x += moveX;
     12             }
     13         }else{
     14             yearCom.x += moveX;
     15         }
     16     }
     17     if( yearsRemoveArray && yearsRemoveArray.length > 0 ){
     18         for( i=0;i<yearsRemoveArray.length;i++ ){
     19             yearsGroup.removeElement( yearsRemoveArray[i] );
     20         }
     21         yearsRemoveArray.splice( 0 );
     22     }
     23     //处理 “月”组件
     24     for( i=0;i<monthsGroup.numElements;i++ ){
     25         var monthCom:MonthComponent = monthsGroup.getElementAt( i ) as MonthComponent;
     26         if( i == 0 || i == monthsGroup.numElements - 1 ){
     27             if( monthCom.x + monthCom.width < 0 || monthCom.x > this.width ){
     28                 monthsRemoveArray.push( monthCom );
     29                 monthsBufferArray.push( monthCom );
     30             }else{
     31                 monthCom.x += moveX;
     32             }
     33         }else{
     34             monthCom.x += moveX;
     35         }
     36     }
     37     if( monthsRemoveArray && monthsRemoveArray.length > 0 ){
     38         for( i=0;i<monthsRemoveArray.length;i++ ){
     39             monthsGroup.removeElement( monthsRemoveArray[i] );
     40         }
     41         monthsRemoveArray.splice( 0 );
     42     }
     43     //处理 “日”组件
     44     for( i=0;i<daysGroup.numElements;i++ ){
     45         var dayCom:DayComponent = daysGroup.getElementAt( i ) as DayComponent;
     46         if( i == 0 || i == daysGroup.numElements - 1 ){
     47             if( dayCom.x + dayCom.width < 0 || dayCom.x > this.width ){
     48                 daysRemoveArray.push( dayCom );
     49                 daysBufferArray.push( dayCom );
     50             }else{
     51                 dayCom.x += moveX;
     52             }
     53         }else{
     54             dayCom.x += moveX;
     55         }
     56     }
     57     if( daysRemoveArray && daysRemoveArray.length > 0 ){
     58         for( i=0;i<daysRemoveArray.length;i++ ){
     59             daysGroup.removeElement( daysRemoveArray[i] );
     60         }
     61         daysRemoveArray.splice( 0 );
     62     }
     63 }
     64 
     65 private function timeAxisMoveAddCom():void{
     66     if( monthOrYear == "year" ){
     67         moveAddComByYear();
     68     }else{
     69         moveAddComByMonth();
     70     }
     71 }
     72 
     73 private function moveAddComByYear():void{
     74     //处理第一个 “年”,如果第一个 “年”的x坐标大于0,则需要添加一个新的 “年”,并且始终添加到索引为0的位置
     75     var yearComFirst:YearComponent = yearsGroup.getElementAt( 0 ) as YearComponent;
     76     while( yearComFirst.x > 0 ){  // 处理完了第一个,新增的将变成第一个,然后再去判断新增的这个第一个,然后循环...
     77         if( yearsBufferArray && yearsBufferArray.length > 0 ){
     78             var yearAddCom:YearComponent = yearsBufferArray.pop() as YearComponent;
     79         }else{
     80             yearAddCom = new YearComponent();
     81         }
     82         yearAddCom.x = yearComFirst.x - yearComFirst.width;
     83         yearAddCom.width = yearWidth;
     84         yearAddCom.labelText = ( Number( yearComFirst.labelText ) - 1 ).toString();
     85         yearsGroup.addElementAt( yearAddCom, 0 );
     86         
     87         yearComFirst = yearAddCom;
     88     }
     89     //处理最后一个 “年”,如果最后一个 “年”的x坐标加宽度小于this.width,则需要添加一个新的 “年”,并且始终添加到最后位置
     90     var yearComLast:YearComponent = yearsGroup.getElementAt( yearsGroup.numElements - 1 ) as YearComponent;
     91     while( yearComLast.x + yearComLast.width < this.width ){
     92         if( yearsBufferArray && yearsBufferArray.length > 0 ){
     93             yearAddCom = yearsBufferArray.pop() as YearComponent;
     94         }else{
     95             yearAddCom = new YearComponent();
     96         }
     97         yearAddCom.x = yearComLast.x + yearComLast.width;
     98         yearAddCom.width = yearWidth;
     99         yearAddCom.labelText = ( Number( yearComLast.labelText ) + 1 ).toString();
    100         yearsGroup.addElement( yearAddCom );
    101         
    102         yearComLast = yearAddCom;
    103     }
    104     
    105     //处理第一个 “月”
    106     var monthComFirst:MonthComponent = monthsGroup.getElementAt( 0 ) as MonthComponent;
    107     while( monthComFirst.x > 0 ){
    108         if( monthsBufferArray && monthsBufferArray.length > 0 ){
    109             var monthAddCom:MonthComponent = monthsBufferArray.pop() as MonthComponent;
    110         }else{
    111             monthAddCom = new MonthComponent();
    112         }
    113         monthAddCom.x = monthComFirst.x - monthComFirst.width;
    114         monthAddCom.width = monthWidth;
    115         if( monthComFirst.month == 0 )
    116             monthAddCom.month = 11;
    117         else
    118             monthAddCom.month = monthComFirst.month - 1;
    119         monthAddCom.monthOrYear = monthOrYear;
    120         monthsGroup.addElementAt( monthAddCom, 0 );
    121         
    122         monthComFirst = monthAddCom;
    123     }
    124     //处理最后一个 “月”
    125     var monthComLast:MonthComponent = monthsGroup.getElementAt( monthsGroup.numElements -1 ) as MonthComponent;
    126     while( monthComLast.x + monthComLast.width < this.width ){
    127         if( monthsBufferArray && monthsBufferArray.length > 0 ){
    128             monthAddCom = monthsBufferArray.pop() as MonthComponent;
    129         }else{
    130             monthAddCom = new MonthComponent();
    131         }
    132         monthAddCom.x = monthComLast.x + monthComLast.width;
    133         monthAddCom.width = monthWidth;
    134         if( monthComLast.month == 11 )
    135             monthAddCom.month = 0;
    136         else
    137             monthAddCom.month = monthComLast.month + 1;
    138         monthAddCom.monthOrYear = monthOrYear;
    139         monthsGroup.addElement( monthAddCom );
    140         
    141         monthComLast = monthAddCom;
    142     }
    143 }
    144 
    145 private function moveAddComByMonth():void{
    146     //处理第一个 “月”
    147     var monthComFirst:MonthComponent = monthsGroup.getElementAt( 0 ) as MonthComponent;
    148     while( monthComFirst.x > 0 ){
    149         if( monthsBufferArray && monthsBufferArray.length > 0 ){
    150             var monthAddCom:MonthComponent = monthsBufferArray.pop() as MonthComponent;
    151         }else{
    152             monthAddCom = new MonthComponent();
    153         }
    154         if( monthComFirst.month == 0 ){
    155             monthAddCom.year = monthComFirst.year - 1;
    156             monthAddCom.month = 11;
    157         }else{
    158             monthAddCom.year = monthComFirst.year;
    159             monthAddCom.month = monthComFirst.month - 1;
    160         }
    161         monthAddCom.width = 35 * monthAddCom.numDays();
    162         monthAddCom.x = monthComFirst.x - monthAddCom.width;
    163         monthAddCom.monthOrYear = monthOrYear;
    164         monthsGroup.addElementAt( monthAddCom, 0 );
    165         
    166         monthComFirst = monthAddCom;
    167     }
    168     //处理最后一个 “月”
    169     var monthComLast:MonthComponent = monthsGroup.getElementAt( monthsGroup.numElements -1 ) as MonthComponent;
    170     while( monthComLast.x + monthComLast.width < this.width ){
    171         if( monthsBufferArray && monthsBufferArray.length > 0 ){
    172             monthAddCom = monthsBufferArray.pop() as MonthComponent;
    173         }else{
    174             monthAddCom = new MonthComponent();
    175         }
    176         if( monthComLast.month == 11 ){
    177             monthAddCom.year = monthComLast.year + 1;
    178             monthAddCom.month = 0;
    179         }else{
    180             monthAddCom.year = monthComLast.year;
    181             monthAddCom.month = monthComLast.month + 1;
    182         }
    183         monthAddCom.width = 35 * monthAddCom.numDays();
    184         monthAddCom.x = monthComLast.x + monthComLast.width;
    185         monthAddCom.monthOrYear = monthOrYear;
    186         monthsGroup.addElement( monthAddCom );
    187         
    188         monthComLast = monthAddCom;
    189     }
    190     //处理第一个 “日”
    191     var dayComFirst:DayComponent = daysGroup.getElementAt( 0 ) as DayComponent;
    192     while( dayComFirst.x > 0 ){
    193         if( daysBufferArray && daysBufferArray.length > 0 ){
    194             var dayAddCom:DayComponent = daysBufferArray.pop() as DayComponent;
    195         }else{
    196             dayAddCom = new DayComponent();
    197         }
    198         var isPreviousMonth:Boolean = false;  //是否到了前一个月,如果是,则dayAddCom.date = 前一个月的最后一天
    199         for( i=0;i<monthsGroup.numElements;i++ ){
    200             var monthComForDayComFirst:MonthComponent = monthsGroup.getElementAt( i ) as MonthComponent;
    201             if( dayComFirst.x == monthComForDayComFirst.x ){
    202                 isPreviousMonth = true;
    203                 break;
    204             }
    205         }
    206         if( isPreviousMonth ){
    207             var previousMonthNumDays:uint = numDays( monthComForDayComFirst.year, monthComForDayComFirst.month - 1 );
    208             dayAddCom.day = previousMonthNumDays;
    209         }else{
    210             dayAddCom.day = dayComFirst.day - 1;
    211         }
    212         
    213         dayAddCom.x = dayComFirst.x - dayComFirst.width;
    214         daysGroup.addElementAt( dayAddCom, 0 );
    215         
    216         dayComFirst = dayAddCom;
    217     }
    218     //处理最后一个 “日”
    219     var dayComLast:DayComponent = daysGroup.getElementAt( daysGroup.numElements - 1 ) as DayComponent;
    220     while( dayComLast.x + dayComLast.width < this.width ){
    221         if( daysBufferArray && daysBufferArray.length > 0 ){
    222             dayAddCom = daysBufferArray.pop() as DayComponent;
    223         }else{
    224             dayAddCom = new DayComponent();
    225         }
    226         var isNextMonth:Boolean = false;  //是否到了后一个月,如果是,则dayAddCom.date = 1
    227         for( var i:uint=0;i<monthsGroup.numElements;i++ ){
    228             var monthComForDayComLast:MonthComponent = monthsGroup.getElementAt( i ) as MonthComponent;
    229             if( dayComLast.x + dayComLast.width == monthComForDayComLast.x + monthComForDayComLast.width ){
    230                 isNextMonth = true;
    231             }
    232         }
    233         if( isNextMonth ){
    234             dayAddCom.day = 1;
    235         }else{
    236             dayAddCom.day = dayComLast.day + 1;
    237         }
    238         dayAddCom.x = dayComLast.x + dayComLast.width;
    239         daysGroup.addElement( dayAddCom );
    240         
    241         dayComLast = dayAddCom;
    242     }
    243 }
    View Code
  5. 处理完了时间,还需要处理事件。事件同样需要一个控件EventBarComponent来表示它,控件用于绘制事件bar条和显示事件标题。
  6. 事件同样需要利用缓存,那也需要搞一个缓存数组用于暂存EventBarComponent。
  7. 时间轴和事件都有了,还需要为它们俩再搞一个容器用来管理它们的同步,也即拖动了时间轴,事件也要跟随一起移动。同样的逻辑,超出可视区域的,将其卸载并添加到缓存中;需要补充的,则要么从缓存中取,要么创建一个新的控件。
     1 private function moveRemoveCom():void{
     2     for( var i:uint=0;i<eventsGroup.numElements;i++ ){
     3         var eventCom:EventBarComponent = eventsGroup.getElementAt( i ) as EventBarComponent;
     4         if( eventCom.x + eventCom.width < 0 || eventCom.x > this.width ){
     5             eventsRemoveArray.push( eventCom );
     6             eventsBufferArray.push( eventCom );
     7             if( eventCom.hasEventListener( MouseEvent.DOUBLE_CLICK ) ){
     8                 eventCom.removeEventListener( MouseEvent.DOUBLE_CLICK, eventDoubleClickHandler );
     9             }
    10         }else{
    11             eventCom.x += moveX;
    12         }
    13     }
    14     if( eventsRemoveArray && eventsRemoveArray.length > 0 ){
    15         for( i=0;i<eventsRemoveArray.length;i++ ){
    16             eventsGroup.removeElement( eventsRemoveArray[i] );
    17         }
    18         eventsRemoveArray.splice( 0 );
    19     }
    20 }
    21 
    22 private function moveAddCom():void{
    23     parent:
    24     for each( var info:Object in this.dataProvider ){
    25         //首先判断新读取的事件是否在舞台上已经绘制出来,如果已经存在,则跳过;如果不存在,则需要绘制
    26         for( var i:uint=0;i<eventsGroup.numElements;i++ ){
    27             var eventCom:EventBarComponent = eventsGroup.getElementAt( i ) as EventBarComponent;
    28             if( info.id == eventCom.info.id ){
    29                 continue parent;
    30             }
    31         }
    32         var startPosition:Number = timeAxis.locationX( info.startDate );
    33         var endPosition:Number = timeAxis.locationX( info.endDate );
    34         if( ( startPosition < 0 && endPosition < 0 )||( startPosition > this.width && endPosition > this.width ) ){
    35             continue;
    36         }
    37         
    38         if( eventsBufferArray && eventsBufferArray.length > 0 ){
    39             eventCom = eventsBufferArray.pop() as EventBarComponent;
    40         }else{
    41             eventCom = new EventBarComponent();
    42         }
    43         eventCom.x = startPosition;
    44         eventCom.y = 70;
    45         eventCom.width = endPosition - startPosition;
    46         eventCom.info = info;
    47         eventCom.addEventListener( MouseEvent.DOUBLE_CLICK, eventDoubleClickHandler );
    48         eventsGroup.addElement( eventCom );
    49         if( eventCom.width < eventCom.height ){
    50             eventCom.shapeFlag = "circle";
    51         }else{
    52             eventCom.shapeFlag = "roundRect";
    53         }
    54     }
    55 }
    View Code
  8. 但是事件还有一点不同的就是,如果事件起止时间相交的话,需要换行显示避免重叠。所以每次移动完以后,需要对事件进行时间排序,然后再去判断事件是否重叠,是否该换行显示。
     1 private var sortArray:Array = new Array();
     2 private function sortByStartTime():void{
     3     if( sortArray )
     4         sortArray.splice( 0 );
     5     for( var i:uint=0;i<eventsGroup.numElements;i++ ){
     6         var eventCom:EventBarComponent = eventsGroup.getElementAt( i ) as EventBarComponent;
     7         sortArray.push( eventCom );
     8     }
     9     sortArray.sort( sortForEventsArray );
    10     reLayout( sortArray );
    11 }
    12 
    13 private function sortForEventsArray( aEventCom:EventBarComponent, bEventCom:EventBarComponent ):Number{
    14     var aStartPosition:Number = aEventCom.x;
    15     var bStartPosition:Number = bEventCom.x;
    16     if( aStartPosition > bStartPosition )
    17         return 1;
    18     else if( aStartPosition < bStartPosition )
    19         return -1;
    20     else
    21         return 0;
    22 }
    23 
    24 private var sortEventsArray:Array = new Array();
    25 private function reLayout( sortArray:Array ):void{
    26     if( sortEventsArray )
    27         sortEventsArray.splice( 0 );
    28     for( var i:uint=0;i<sortArray.length;i++ ){
    29         var isWrap:Boolean = false;
    30         var eventCom:EventBarComponent = sortArray[i] as EventBarComponent;
    31         if( sortEventsArray.length > 0 ){
    32             for( var j:uint=0;j<sortEventsArray.length;j++ ){
    33                 var eventComSort:EventBarComponent = sortEventsArray[j] as EventBarComponent;
    34                 if( eventComSort.shapeFlag == "circle" ){
    35                     if( eventCom.shapeFlag == "circle" ){
    36                         if( eventComSort.x + eventComSort.width/2 + eventComSort.height/2 >
    37                         eventCom.x + eventCom.width/2 - eventCom.height/2 ){
    38                             eventCom.y = eventComSort.y + 22;
    39                         }else{
    40                             isWrap = true;
    41                             break;
    42                         }
    43                     }else if( eventCom.shapeFlag == "roundRect" ){
    44                         if( eventComSort.x + eventComSort.width/2 + eventComSort.height/2 > eventCom.x ){
    45                             eventCom.y = eventComSort.y + 22;
    46                         }else{
    47                             isWrap = true;
    48                             break;
    49                         }
    50                     }
    51                 }else if( eventComSort.shapeFlag == "roundRect" ){
    52                     if( eventCom.shapeFlag == "circle" ){
    53                         if( eventComSort.x + eventComSort.width >
    54                             eventCom.x + eventCom.width/2 - eventCom.height/2 ){
    55                             eventCom.y = eventComSort.y + 22;
    56                         }else{
    57                             isWrap = true;
    58                             break;
    59                         }
    60                     }else if( eventCom.shapeFlag == "roundRect" ){
    61                         if( eventComSort.x + eventComSort.width > eventCom.x ){
    62                             eventCom.y = eventComSort.y + 22;
    63                         }else{
    64                             isWrap = true;
    65                             break;
    66                         }
    67                     }
    68                 }
    69             }
    70             if( isWrap ){
    71                 eventCom.y = eventComSort.y;
    72                 sortEventsArray.splice( j, 1, eventCom );
    73             }else{
    74                 sortEventsArray.push( eventCom );
    75             }
    76         }else{
    77             eventCom.y = 70;
    78             sortEventsArray.push( eventCom );
    79         }
    80     }
    81     dynamicHeight = sortEventsArray.length * 22 + 10;
    82 }
    View Code

  好了,截止到这,除了两种呈现场景的切换动画之后,基本功能已经完成了(当时这个切换动画并没有实现,只是很呆板的瞬间切换。一直到现在,我尝试过通过我能想到的各种方法控制这个动画,但是效果始终不理想,看来能力还是差的远哪~~~)。

  //-------------------------------------------------------------------------------------

  说完了当初的实现思路,那下面就来剖析一下它的问题,要不然干嘛还要跑来重构它呢?

  首先,时间控件只不过是时间刻度的事,还要搞三个控件,在进行缓存利用时,还要判断是年、是月又或者是日,你不觉得这样很累么?每次进行控件的卸载与添加时,都要“处理第一个年”、“处理最后一个年”、“处理第一个月”...你不觉得这样很啰嗦么?

  其次,每次进行缓存利用时,代码都是这么写的:

1 if( monthsBufferArray && monthsBufferArray.length > 0 ){
2     var monthAddCom:MonthComponent = monthsBufferArray.pop() as MonthComponent;
3 }else{
4     monthAddCom = new MonthComponent();
5 }
View Code

  你不觉得这样的代码看上去很不爽么?而且是每一个控件都是这么判断的,当真是一点代码的抽取工作都没做。现在假设即便是需要三个不同的控件要进行缓存利用,如果是按现在的思想来实现的话,我会定义一个接口IComponent,接口方法主要包括绘制刻度、显示日期等,这三个控件都实现这个接口,而且只需一个缓存数组即可,也即我的应用程序中有且仅有一个缓存池,那么上面重复的代码可以优化为以下一个方法:

1 private function getComponent( identifier:String ):IComponent
2 {
3     var component:IComponent = getReusableComponentWidthIdentifier( identifier );
4     if( component == null )
5     {
6         component = creationComponentWithIdentifier( identifier );
7     }
8     return component;
9 }
View Code

  这样就不用再去分别判断三种控件了,在使用时,只需给三种控件设置一个唯一标识就好了嘛,这样不管是从编程思想的角度看还是从代码简洁性的角度看,都要好很多嘛。

  再者,当初实现的方案中,有很大的性能问题。时间轴的拖动功能是基于mouseMove实现的,也就意味着在拖动过程中,mouseMove方法会被频繁调用,但是在mouseMove中我却做了频繁、重复的计算逻辑。在可视区域中的时间控件的处理如移动、卸载、添加等,这些操作是必要的,暂时还没有想到有什么更好的优化方案。但是对于事件控件来说,每次拖动,我都会依据基准时间的最新位置计算所有事件的最新位置,然后再去判断哪些事件处于可视区域哪些事件被移除了可视区域,而且每次都对事件依据起止时间进行排序,进而判断是否该换行显示等,这些重复的逻辑被频繁的调用,确实影响到了性能。能不能有这样一种方案,一次性计算出已经加载的所有事件相对于基准时间的位置并且进行起止时间的排序操作,在拖动时,只需将所有事件的位置在原来的基础上加上基准时间的偏移量?答案当然是肯定的。这样的话,只是简单的移动事件的位置,而不是每次都依据事件的起止时间和基准时间重新计算事件的位置,性能上肯定会提升不少。其实这也就用到了MVC模型,一次性处理了所有事件对应的模型,在拖动时,只是移动模型的位置,模型变了,视图也就变了。不过这也是在第三次实现时才用到的方案,等会再说为啥第二次没有用到。

  其实当初实现的方案中问题还有很多,对于对象的设计与抽象,完全没有考虑低耦合高内聚的原则,只是一味的需要什么,我就写什么,以至于现在回过头来看这部分代码,我自己都觉得头疼。我猜想如果当初有人和我一起做这个组件的话,那人一定会被我逼疯的。

  //-------------------------------------------------------------------------------------

  下面就开始讲讲第二次实现方案吧。当时进行代码重构时,想的远没有现在想的多,还是没有考虑过要使用MVC。当时只是在做了一个图片轮播器之后想到用同一种方案实现一下这个时间轴组件。因为图片轮播器在实现时用到了链表,所以我就在想,这时间控件和事件控件也都是一个接一个的,在进行mouseMove时只需一个节点接一个节点的处理就行了。

 1 /**
 2  * TimeComponent跟随。
 3  * 
 4  */
 5 private function flowTimeComponent( timeComponent:TimeComponent ):void
 6 {
 7     var walkLeft:LinkNode, walkRight:LinkNode;
 8     
 9     var linkNode:LinkNode = _linkList.searchNode( timeComponent );
10     // 链表的头节点对应的timeComponent的x>0,此时需新增一个节点,作为新的头节点。
11     if( linkNode.preNode == null && timeComponent.getStartX() > 0 )
12     {
13         timeComponent = creationTimeComponent( timeComponent, 'left' );
14         walkLeft = _linkList.searchNode( timeComponent ).preNode;
15     }
16     else
17     {
18         walkLeft = linkNode.preNode;
19     }
20     while( walkLeft )
21     {
22         timeComponent = walkLeft.data;
23         // 新的timeComponent的x等于其next(因为这是向左遍历)的开始位置减去本身的宽度。
24         timeComponent.x = walkLeft.nextNode.data.getStartX() - timeComponent.width;
25         // 不断更新横跨x = 0的对象。
26         if( timeComponent.getStartX() < 0 && timeComponent.getEndX() > 0 )
27         {
28             _currentTimeComponent = timeComponent;
29         }
30         if( timeComponent.getEndX() < 0 )
31         {
32             if( timeMcBase.contains( timeComponent ) )
33             {
34                 timeMcBase.removeElement( timeComponent );
35             }
36         }
37         else
38         {
39             timeMcBase.addElement( timeComponent );
40         }
41         
42         // 链表的头节点对应的timeComponent的x>0,此时需新增一个节点,作为新的头节点。
43         if( walkLeft.preNode == null && timeComponent.getStartX() > 0 )
44         {
45             timeComponent = creationTimeComponent( timeComponent, 'left' );
46             walkLeft = _linkList.searchNode( timeComponent ).preNode;
47         }
48         else
49         {
50             walkLeft = walkLeft.preNode;
51         }
52     }
53     
54     // 链表的尾节点对应的timeComponent的endX<width,此时需新增一个节点,作为新的尾节点。
55     timeComponent = linkNode.data;
56     if( linkNode.nextNode == null && timeComponent.getEndX() < width )
57     {
58         timeComponent = creationTimeComponent( timeComponent );
59         walkRight = _linkList.searchNode( timeComponent ).nextNode;
60     }
61     else
62     {
63         walkRight = linkNode.nextNode;
64     }
65     while( walkRight )
66     {
67         timeComponent = walkRight.data;
68         // 新的timeComponent的x等于其previous(因为这是向右遍历)的结束位置。
69         timeComponent.x = walkRight.preNode.data.getEndX();
70         if( timeComponent.getStartX() < 0 && timeComponent.getEndX() > 0 )
71         {
72             _currentTimeComponent = timeComponent;
73         }
74         if( timeComponent.getStartX() > width )
75         {
76             if( timeMcBase.contains( timeComponent ) )
77             {
78                 timeMcBase.removeElement( timeComponent );
79             }
80         }
81         else
82         {
83             timeMcBase.addElement( timeComponent );
84         }
85         
86         // 链表的尾节点对应的timeComponent的endX<width,此时需新增一个节点,作为新的尾节点。
87         if( walkRight.nextNode == null && timeComponent.getEndX() < width )
88         {
89             timeComponent = creationTimeComponent( timeComponent );
90             walkRight = _linkList.searchNode( timeComponent ).nextNode;
91         }
92         else
93         {
94             walkRight = walkRight.nextNode;
95         }
96     }
97     
98     trace( 'The number of timeComponent in stage is ', timeMcBase.numElements );
99 }
View Code

  但是事实看来,并没什么卵用,丝毫没有解决上面提及的那些问题,反而还把缓存给抛弃了。其实在时间控件、事件控件的问题上,用链表根本就不合适。假设可视区域最多可容纳4个时间控件,那么在整个应用中最多就只有4到5个时间控件就足够了。就像是这样:

  不过有一点值得一提的就是,在这次实现的方案中,实现了时间轴的惯性滑动,这也是当初做这个组件时一直想实现而没有实现的功能。其实道理很简单,就是在mouseMove过程中不断记录时间轴上鼠标点的位置,在mouseUp那一刻,取得最后两个点的位置做差即可得到惯性滑动的初始速度,之后再利用enterFrame不断改变惯性滑动的速度与位移即可。

 1 private var _mouseDownTarget:TimeComponent;
 2 private var _mouseDownPoint:Point = new Point();
 3 
 4 private var pointX:Array = [];         // 运动坐标。
 5 private var speedX:Number = 0;         // 滑动的速度。
 6 private var mouseUpX:Number = 0;       // mouseUp时的位置。
 7 private var friction:Number = 0.95;    // 滑动的摩擦力。
 8 private var isMoving:Boolean = false;  // 是否正在滑动。
 9 
10 private function prepareMouseMove():void
11 {
12     stage.addEventListener( MouseEvent.MOUSE_UP, stage_mouseUpHandler );
13     stage.addEventListener( MouseEvent.MOUSE_MOVE, stage_mouseMoveHandler );
14 }
15 private function mouseDownHandler( event:MouseEvent ):void
16 {
17     event.stopImmediatePropagation();
18     
19     var timeComponent:TimeComponent = event.currentTarget as TimeComponent;
20     _mouseDownPoint.x = timeComponent.mouseX;
21     _mouseDownTarget = timeComponent;
22     
23     prepareMouseMove();
24 }
25 private function stage_mouseMoveHandler( event:MouseEvent ):void
26 {
27     _mouseDownTarget.x = timeMcBase.mouseX - _mouseDownPoint.x;
28     
29     // mouseMove时,不断记录运动坐标。
30     pointX.push( timeMcBase.mouseX );
31     
32     flowTimeComponent( _mouseDownTarget );
33     flowEventComponent();
34 }
35 private function stage_mouseUpHandler( event:MouseEvent ):void
36 {
37     // 记录mouseUp时的位置。
38     mouseUpX = timeMcBase.mouseX;
39     
40     stage.addEventListener( Event.ENTER_FRAME, enterFrameHandler );
41     if( pointX.length > 2 )
42     {
43         // 取最后两个位置作差,即:得到了最后时刻鼠标移动的速度。
44         speedX = ( pointX[ pointX.length - 1 ] - pointX[ pointX.length - 2 ] ) * 2;
45         isMoving = true;
46     }
47     
48     _mouseDownPoint.x = 0;
49     stage.removeEventListener( MouseEvent.MOUSE_UP, stage_mouseUpHandler );
50     stage.removeEventListener( MouseEvent.MOUSE_MOVE, stage_mouseMoveHandler );
51 }
52 
53 private function enterFrameHandler( event:Event ):void
54 {
55     // mouseUp之后的第一帧判断,mouseUp时的位置和第一帧之后的位置是否相同,若相同,则表示无需惯性滑动。
56     if( mouseUpX - timeMcBase.mouseX == 0 )
57     {
58         stage.removeEventListener( Event.ENTER_FRAME, enterFrameHandler );
59         pointX.splice( 0 );
60         isMoving = false;
61         flowEventComponent();
62         return;
63     }
64     
65     // 当速度小于0.1时停止滑动。
66     if( Math.abs( speedX ) < 0.1 )
67     {
68         stage.removeEventListener( Event.ENTER_FRAME, enterFrameHandler );
69         pointX.splice( 0 );
70     }
71     
72     // 惯性滑动并不断更新TimeComponent的位置。
73     speedX *= friction;
74     _mouseDownTarget.x += speedX;
75     flowTimeComponent( _mouseDownTarget );
76     
77     // 如果滑动速度大于3,为了提高性能,不再计算EventComponent的位置。
78     if( Math.abs( speedX ) < 3 )
79     {
80         isMoving = false;
81         flowEventComponent();
82     }
83 }
View Code

  或许还有更好的关于惯性滑动的实现方案,只是现在还没想到。

  项目地址:https://git.oschina.net/You.wanwu/Memorabilia.git(原谅我家里的网连不上Github,欲哭无泪啊。。。)

  //-------------------------------------------------------------------------------------

  在第三次的实现方案中,以上提及的点基本上都做到了。比如缓存的分析以及实现,事件MVC模型的使用,类的封装与组织等,关于他们的原理和实现,上面已经讲完了,(额。。。难道这些不应该是在这里才讲的吗?)这里我就不在累述了,就直接上代码吧。

 1 /**
 2  * 移动事件。
 3  * <p>这里所有的eventModel已经在初始化时计算了相对于基准时间的位置,所以在移动过程中,只需在原来的基础上加上移动的偏移量即可,而不是每移动一次就计算一次,相对提高了性能。
 4  * 同时舞台上的eventView采用了缓存机制,舞台上eventView的数量不会很多,所以也是相对提高了性能。</p>
 5  * @param moveX
 6  * 
 7  */        
 8 public function moveEvent( moveX:Number = 0 ):void
 9 {
10     for each( var eventModel:EventModel in _cacheEventModel )
11     {
12         eventModel.startX += moveX;
13         eventModel.endX += moveX;
14         
15         // 此时eventModel需要一个eventView显示到舞台上
16         if( eventModel.endX >= 0 && eventModel.startX <= this.width )
17         {
18             if( eventModel.hidden )
19             {
20                 eventModel.hidden = false;
21                 var eventView:EventView = creationEventView( eventModel );
22                 moveX > 0 ? this.addElementAt( eventView, 0 ) : this.addElement( eventView );
23             }
24         }
25     }
26     
27     // 更新视图
28     var previousEventView:EventView;
29     for( var i:int = 0; i < this.numElements; i++ )
30     {
31         eventView = this.getElementAt( i ) as EventView;
32         eventView.x = eventView.eventModel.startX;
33         eventView.y = 0;
34         
35         if( eventView.maxX < 0 || eventView.x > this.width )
36         {
37             eventView.eventModel.hidden = true;
38             eventView.eventModel = null;
39             _cacheEventView.push( this.removeElementAt( i ) );
40             i--;
41         }
42         else
43         {
44             // 判断是否该换行
45             if( previousEventView && previousEventView.maxX > eventView.x )
46             {
47                 eventView.y = previousEventView.y + Const.EVENT_HEIGHT + 5;
48             }
49             previousEventView = eventView;
50         }
51     }
52 }
53 
54 /**
55  * 获得一个可用的EventView。
56  * <p>这里只是简单实现只有EventView需要缓存的情况。
57  * 等一下,不是还有TimeView也需要缓存么,怎么这里没有?
58  * TimeView是在时间轴容器中进行处理的,EventView是在事件容器中处理的,这里并没有把他们放到一起。</p>
59  * 
60  */        
61 private function creationEventView( eventModel:EventModel ):EventView
62 {
63     var eventView:EventView = _cacheEventView.pop();
64     if( eventView == null )
65     {
66         eventView = new EventView();
67         
68         trace( 'create a eventView' );
69     }
70     eventView.eventModel = eventModel;
71     
72     return eventView;
73 }
View Code

  项目地址:https://git.oschina.net/You.wanwu/ZTest.git(再说一遍,算了,隔这么近,不说了。。。)

  //-------------------------------------------------------------------------------------

  好了,整理完这篇文章,对于缓存、MVC的理解似乎更透彻了一些。说来挺可笑的,一个两年开发经验的人,却还在这讨论这些东西,我想问问我自己,这两年我到底干了啥?其实最近我也一直在反思这些,两年了,关于编程思想、设计理念、数据结构等我学到了多少?关于编程语言,我又自学了几门?之前我还在为自己找借口,我在公司做的全是一些业务系统,和具体业务贴合的太紧密,框架也是公司自己封装的,好在并不是所有的项目都在用。可是事实却是,这么长时间以来,我是真真的第一次去考虑这些问题,我也是真真的第一次感觉到恐慌。。。。。

  调整一下心情吧,上面就是我对这个小组件的三次实现方案,或许会有很多问题存在,如果有小伙伴愿意一起分享学习经历的话,欢迎评论留言。

posted @ 2016-01-12 13:45  游玩屋  阅读(356)  评论(0编辑  收藏  举报