jQuery-ui源代码重点难点分析

本文以jquery-ui sortable排序功能插件为例分析jquery-ui源代码重点难点。

jquery-ui-sortable插件非常复杂深奥,本来是针对鼠标操作设计的,在手机运行无效,因为手机没有mouse事件,更没有drag事件。

但增加一个jquery-ui-touch-punch.js文件之后在手机运行也有效,touch有效,原理在于sortable源代码不是针对鼠标drag and drop设计的,而是针对mouse事件中的位置数据设计的,底层有复杂的算法,touch事件数据也包含位置数据,因此sortable能兼容touch,很强大。

sortable插件的调用方式是$().sortable(opts),那么sortable()函数就是入口初始化函数,我们从这个入口初始化函数源代码开始分析。

$.widget.bridge = function( name, object ) {
  var fullName = object.prototype.widgetFullName || name;
  $.fn[ name ] = function( options ) { // 此即为sortable()函数代码

    this.each(function() { // this是$()元素集合,一般只有一个节点,$().each()就是循环一次,元素就是container元素。
      var instance = $.data( this, fullName ); // this是元素集合里面每一个元素对象,这是从元素对象取比如ui-sortable属性值是一个实例
      if ( instance ) {
        instance.option( options || {} );
        if ( instance._init ) {
          instance._init();
        }
      } else {
        $.data( this, fullName, new object( options, this ) ); // 如果没有就建一个实例保存到元素属性中

          constructor = $[ namespace ][ name ] = function( options, element ) { // object代码
            // allow instantiation without "new" keyword
            if ( !this._createWidget ) {
              return new constructor( options, element );
            }

            // allow instantiation without initializing for simple inheritance
            // must use "new" keyword (the code above always passes args)
            if ( arguments.length ) {
              this._createWidget( options, element );                  

                $.Widget.prototype = {
                  _createWidget: function( options, element ) {

                    this._create(); // 调sortable插件的内置方法初始化sortable插件,由于加了一层封装,用apply方式调用

                       

                        var sortable = $.widget("ui.sortable", $.ui.mouse, { // sortable插件的定义
                          _create: function() {

                            this.refresh();//根据已经存在的网页中的列表创建一套items[]
                            this._mouseInit(); //调用mouse插件的通用初始化方法,这是juqery-ui-touch修改之后的init代码

                              $.ui.mouse.prototype._mouseInit=function(){
                                var _this=this; 

                                _this.element.bind("touchstart",$.proxy(_this,"_touchStart"))

                                  .bind("touchmove",$.proxy(_this,"_touchMove"))

                                  .bind("touchend",$.proxy(_this,"_touchEnd"));
                                  oldMouseInit.call(_this);

                              };

                          };                     

                      this._trigger( "create", null, this._getCreateEventData() );
                      this._init(); // 空函数
                    }

sortable入口函数就是创建一个实例保存在container元素属性中,先调用通用插件widget代码创建实例,再调用sortable插件的初始化方法,sortable插件初始化方法
创建items[]保存在sortable实例中,执行mouse初始化代码,如果执行了jquery-ui-touch-punch.js,mouse初始化代码被jquery-ui-touch修改了,就是绑定touch事件,touch事件会转换为mouse事件。

因此执行$().sortable()之后,创建了sortable实例,进行了初始化,绑定了touch事件,当touch操作时,执行mouse事件绑定的handler实现拖拽功能。

下面来看sortable插件的重点函数代码:

var sortable = $.widget("ui.sortable", $.ui.mouse, { // sortable的内置方法都加了一层封装,用value.apply方式调用
  _mouseDrag: function(event) { //拖拽时会连续执行多次,拖拽时处理网页样式变化,把placeholder插入到目标元素之前,sortable不修改items[]原数据  

    //Compute the helpers position
    //当拖动一个元素时,其class增加helper成为helper,成为浮动元素,元素跟着拖动浮动,离开了列表,helper的意思就是预览让人能看见元素被拖动走了。

    //placeholder是新建一个元素代替它占据在列表中的位置,就是被拖动元素在列表中的替身,当拖动元素时,placeholder元素就插入相应的位置,这样就能预览插入的效果

    

    this.position = this._generatePosition(event); // 计算helper的位置,来自touch event x/y位置数据,拖动时位置不断变化,helper跟着拖动走
      _generatePosition: function(event) {
        event中有touch位置x/y,计算时涉及到父容器scroll,要把父容器scroll去掉简化处理,调插件传参scroll:false忽略scroll处理。
        return {
          top: (
            pageY - // The absolute mouse position
            this.offset.click.top - // Click offset (relative to the element)
            this.offset.relative.top - // Only for relative positioned nodes: Relative offset from element to offset parent
            this.offset.parent.top + // The offsetParent's offset without borders (offset + border)
            ( ( this.cssPosition === "fixed" ? -this.scrollParent.scrollTop() : ( scrollIsRootNode ? 0 : scroll.scrollTop() ) ))
          ),
          left: (
            pageX - // The absolute mouse position
            this.offset.click.left - // Click offset (relative to the element)
            this.offset.relative.left - // Only for relative positioned nodes: Relative offset from element to offset parent
            this.offset.parent.left + // The offsetParent's offset without borders (offset + border)
            ( ( this.cssPosition === "fixed" ? -this.scrollParent.scrollLeft() : scrollIsRootNode ? 0 : scroll.scrollLeft() ))
          )
        };
      }

    

    //Rearrange,产生placeholder元素插入到touch位置
    for (i = this.items.length - 1; i >= 0; i--) { //

      intersection = this._intersectsWithPointer(item); // intersect是touch与元素重叠的意思

          

        _intersectsWithPointer: function(item) {
          var isOverElementHeight = (this.options.axis === "x") || this._isOverAxis(this.positionAbs.top + this.offset.click.top, item.top, item.height),
            isOverElementWidth = (this.options.axis === "y") || this._isOverAxis(this.positionAbs.left + this.offset.click.left, item.left, item.width),

            _isOverAxis: function( x, reference, size ) { // x-touch位置 reference-元素位置
              return ( x >= reference ) && ( x < ( reference + size ) ); // 判断touch位置是否在元素中,在水平方向和垂直方向分别判断
            }

            isOverElement = isOverElementHeight && isOverElementWidth,
              verticalDirection = this._getDragVerticalDirection(),
              horizontalDirection = this._getDragHorizontalDirection();

            return this.floating ?
              ( ((horizontalDirection && horizontalDirection === "right") || verticalDirection === "down") ? 2 : 1 )
                : ( verticalDirection && (verticalDirection === "down" ? 2 : 1) );

     

      if (!intersection) {
        continue;
      }
      //如果item与touch重叠,要修改列表,把placeholder插入到item元素之前
      this._rearrange(event, item); 

          _rearrange: function(event, i, a, hardRefresh) {  // i是touch目标item元素            

              i.item[0].parentNode.insertBefore(this.placeholder[0], (this.direction === "down" ? i.item[0] : i.item[0].nextSibling));
              //向上向左拖时direction=down,向下向右拖时direction=up,placeholder插入位置有可能是item之前,也有可能是item下一个元素之前

      this._trigger("change", event, this._uiHash());

    }

    

    //Call callbacks
    this._trigger("sort", event, this._uiHash()); //如果有绑定"sort"逻辑事件,则执行handler,实际上没有

  }

  

  _mouseStop: function(event, noPropagation) { // 单步看开始执行之前start/end元素已经交换了位置,是由drag过程处理的,stop只是善后处理
    $.ui.ddmanager.drop(this, event); // debug看没有处理内容
    that._clear(event);

      _clear: function(event, noPropagation) {

        this.placeholder.before(this.currentItem); // sortbale实例里面的placeholder对象的before方法

            before: function() {
              return domManip( this, arguments, function( elem ) {
                  if ( this.parentNode ) {
                    this.parentNode.insertBefore( elem, this ); // elm是helper(drag)元素,this是placeholder,把拖动的元素插入到placeholder位置,这也是整个sortable操作的最终目的,helper元素已经在网页中,insert只是移动位置,插入之后还要修改css样式(修改class即可)。
                  }
              } );
            }

                

                function domManip( collection, args, callback, ignored ) { // dommanip非常复杂
                  callback.call( collection[ i ], node, i );
                  return collection;
                }

          

        this._trigger("update", event, this._uiHash());
        this._trigger("beforeStop", event, this._uiHash());
        this.placeholder[0].parentNode.removeChild(this.placeholder[0]); // 删除placeholder
        this._trigger("stop", event, this._uiHash());

sortable处理网页在拖动时插入placeholder最后再把拖动的元素插入到placeholder位置。

sortable是根据鼠标的位置数据进行计算判断目标位置元素,所以也适用于touch触发,因为touch事件数据中含位置数据,只要把touch事件转换成mouse事件即可,
如果sortable是按drag target元素设计的,就没法适用于touch,因为touchend没有target元素,没法获取到target元素。

sortable插件代码非常复杂,很难控制,本人参考它的处理思路和算法,自己写了一个简化的sortable插件来实现touch触发排序,测试效果没问题:

因为用前端框架比如vue开发,touch拖动时无需处理拖拽效果(实际上鼠标操作时h5会自动产生拖拽效果),只需要在touchend时计算出target元素,有了start元素和end元素,只需要把原数组变换一下,框架会自动更新页面,无需写代码处理页面更新,而jquery-ui-sortable的功能就是在拖拽时更新页面效果,我不需要它这个功能,只需要参考它计算mouseup/touchend元素的算法,它的算法很复杂,简化一下测试结果也是可以的。

_touchEnd(e){
  //console.log(e);
  e.preventDefault();
  e.stopPropagation();
  var _this = this;
  var intersection = false;
  $("#tf_box > li").each(function(k,v){
    //console.log(v.offsetLeft);
    intersection = _this._intersectsWithTouch(e,v);
    if (intersection) {
      //item与touchend位置重叠,则item即为touchend/drag目标元素,修改原数组交换start/end元素
      _this.endIndex = k;
      _this.updateData(); // 修改原数组,页面会自动更新显示拖拽交换后的效果
    }
  });
},
_intersectsWithTouch(event,item) { // 计算touchend元素位置
  var touchY = event.originalEvent.changedTouches[0].clientY; // 注意不要用screenX/Y,那是在屏幕中的位置,要用在浏览器窗口中的位置
  var touchX = event.originalEvent.changedTouches[0].clientX;
  //console.log(touchX+":"+item.offsetLeft);
  var isOverElementHeight = touchY >= item.offsetTop && ( touchY < ( item.offsetTop + item.offsetHeight )),
    isOverElementWidth = touchX >= item.offsetLeft && ( touchX < ( item.offsetLeft + item.offsetWidth )),
    isOverElement = isOverElementHeight && isOverElementWidth;
    //console.log(isOverElementHeight+":"+isOverElementWidth);

    if (!isOverElement) {
      return false;
    }

    return true;

},

本来无意研究jquery-ui源代码,因为发现有些sortble程序在手机运行无效果,有些在手机运行有效果,而jquery-ui-sortable在手机运行也是有效的,有点好奇,才研究了jquery-ui-sortable源代码,搞清楚了其中的是非,涉及到mouse/touch/drag事件原理和算法,还是非常复杂的,拖拽比较复杂,比拖拽更复杂的就是动画了。

文中错误之处欢迎大家指正交流。

原文地址:https://www.cnblogs.com/pzhu1/p/9212753.html