之前在做两个相同的页面的事件同步时发现了这个问题,现在把它记录下来。
页面中的jqueryui对话框,如果把它拖动到靠近浏览器窗口右侧边缘,并快速从对话框左侧调整对话框窗口大小时,对话框右侧会偏离浏览器窗口右侧边缘,其实就是对话框窗口宽度计算不准确。为了更好地说明问题,下面给出几张示意图。(黑色背景框是浏览器窗口)
图1、对话框窗口开始放在浏览器右侧边缘,从左侧缓慢调整窗口大小过程中,对话框窗口右侧会发生“抖动”
图2、对话框窗口开始放在浏览器右侧边缘,稍微快一点从左侧调整窗口大小,对话框右侧跟浏览器窗口边缘出现间距
图3、对话框窗口开始放在浏览器右侧边缘,快速从左侧调整窗口大小,对话框右侧的偏离情况更加明显
图4、如果对话框窗口的位置没有靠近浏览器窗口右侧边缘,调整对话框大小的情况正常
以上几张图说明,当对话框初始位置放在浏览器右侧边缘时,从左侧调整对话框大小会出现对话框宽度计算不正确的问题。
你也可以自己试一试:http://jqueryui.com/dialog/
我们知道,鼠标移动的事件并不是连续触发的,即使两次鼠标移动事件之间的间隔很短。所以如果鼠标移动得很快,在两次鼠标移动事件之间鼠标移动的距离会比较大。快速调整对话框大小时,在两次鼠标移动事件触发的间隔中鼠标移动了较长距离。我们可以通过脚本创建并触发事件的方式来模拟快速调整对话框大小的过程,看看在这个过程中执行了什么操作并分析哪里出了问题。
为了模拟快速从对话框左侧调整大小,下面会依次创建mouSEOver,mousedown,mousemove事件并在对话框左侧的resize handler上面触发(mousedown和mousemove事件的e.pageX相差较大)。
图5、对话框的初始宽度和位置
首先在控制台执行下面的代码,依次创建mouseover、mousedown和mousemove事件并触发。
1 var mouseover_event = new MouseEvent('mouseover', { 2 bubbles: true, 3 clientX: 1389, 4 clientY: 475, 5 layerX: 1389, 6 layerY: 475, 7 pageX: 1389, 8 pageY: 475 9 });10 var mousedown_event = new MouseEvent('mousedown', {11 bubbles: true,12 clientX: 1389,13 clientY: 475,14 layerX: 1389,15 layerY: 475,16 pageX: 1389,17 pageY: 47518 });19 var mousemove_event = new MouseEvent('mousemove', {20 bubbles: true,21 clientX: 1000,22 clientY: 475,23 layerX: 1000,24 layerY: 475,25 pageX: 1000,26 pageY: 47527 });28 var resize_handler = document.querySelector('.ui-resizable-handle.ui-resizable-w');29 resize_handler.dispatchEvent(mouseover_event);30 resize_handler.dispatchEvent(mousedown_event);31 resize_handler.dispatchEvent(mousemove_event);
接着跟踪相应事件处理程序的执行。在jqueryui源代码中,当触发mousemove事件时,下面的的代码片段会执行(添加了实时执行结果的注释):
1 _mouseDrag: function(event) { 2 3 var data, PRops, 4 smp = this.originalMousePosition, // 鼠标刚按下时的位置(1389, 475) 5 a = this.axis, 6 dx = (event.pageX - smp.left) || 0, // -389 7 dy = (event.pageY - smp.top) || 0, 8 trigger = this._change[a]; 9 10 this._updatePrevProperties(); // 设置prevPosition和prevSize,prevPosition.left=1389, prevSize.width=522.611 12 if (!trigger) {13 return false;14 }15 16 data = trigger.apply(this, [ event, dx, dy ]); // 计算对话框最终应设置的left和width,data={left:1000,width:911.6}17 18 this._updateVirtualBoundaries(event.shiftKey);19 if (this._aspectRatio || event.shiftKey) {20 data = this._updateRatio(data, event);21 }22 23 data = this._respectSize(data, event);24 25 this._updateCache(data); // 更新对话框最终应设置的position和size,执行完之后this.position.left=1000,this.size.width=911.626 27 this._propagate("resize", event);28 29 props = this._applyChanges();30 31 if ( !this._helper && this._proportionallyResizeElements.length ) {32 this._proportionallyResize();33 }34 35 if ( !$.isEmptyObject( props ) ) {36 this._updatePrevProperties();37 this._trigger( "resize", event, this.ui() );38 this._applyChanges();39 }40 41 return false;42 }43
当执行到上面代码的第27行时(this._propagate("resize", event);),用Chrome开发者工具跟踪到以下这段代码:
1 resize: function( event ) { 2 var woset, hoset, isParent, isOffsetRelative, 3 that = $( this ).resizable( "instance" ), 4 o = that.options, 5 co = that.containerOffset, 6 cp = that.position, 7 pRatio = that._aspectRatio || event.shiftKey, 8 cop = { 9 top: 0,10 left: 011 },12 ce = that.containerElement,13 continueResize = true;14 15 if ( ce[ 0 ] !== document && ( /static/ ).test( ce.CSS( "position" ) ) ) {16 cop = co;17 }18 19 if ( cp.left < ( that._helper ? co.left : 0 ) ) {20 that.size.width = that.size.width +21 ( that._helper ?22 ( that.position.left - co.left ) :23 ( that.position.left - cop.left ) );24 25 if ( pRatio ) {26 that.size.height = that.size.width / that.aspectRatio;27 continueResize = false;28 }29 that.position.left = o.helper ? co.left : 0;30 }31 32 if ( cp.top < ( that._helper ? co.top : 0 ) ) {33 that.size.height = that.size.height +34 ( that._helper ?35 ( that.position.top - co.top ) :36 that.position.top );37 38 if ( pRatio ) {39 that.size.width = that.size.height * that.aspectRatio;40 continueResize = false;41 }42 that.position.top = that._helper ? co.top : 0;43 }44 45 isParent = that.containerElement.get( 0 ) === that.element.parent().get( 0 );46 isOffsetRelative = /relative|absolute/.test( that.containerElement.css( "position" ) );47 48 if ( isParent && isOffsetRelative ) {49 that.offset.left = that.parentData.left + that.position.left;50 that.offset.top = that.parentData.top + that.position.top;51 } else { // 会执行到这里52 that.offset.left = that.element.offset().left; // 初始状态的offset,that.offset.left=138953 that.offset.top = that.element.offset().top;54 }55 56 woset = Math.abs( that.sizeDiff.width +57 (that._helper ?58 that.offset.left - cop.left :59 (that.offset.left - co.left)) ); // 加上that.sizeDiff.width(6.4),woset=1395.460 61 hoset = Math.abs( that.sizeDiff.height +62 (that._helper ?63 that.offset.top - cop.top :64 (that.offset.top - co.top)) );65 66 if ( woset + that.size.width >= that.parentData.width ) { // 就是这一段出问题,单独把这一段拿出来分析67 that.size.width = that.parentData.width - woset;68 if ( pRatio ) {69 that.size.height = that.size.width / that.aspectRatio;70 continueResize = false;71 }72 }73 74 if ( hoset + that.size.height >= that.parentData.height ) {75 that.size.height = that.parentData.height - hoset;76 if ( pRatio ) {77 that.size.width = that.size.height * that.aspectRatio;78 continueResize = false;79 }80 }81 82 if ( !continueResize ) {83 that.position.left = that.prevPosition.left;84 that.position.top = that.prevPosition.top;85 that.size.width = that.prevSize.width;86 that.size.height = that.prevSize.height;87 }88 }
再单独看看上面代码第66行的条件判断语句:
1 if ( woset + that.size.width >= that.parentData.width ) {2 that.size.width = that.parentData.width - woset;3 if ( pRatio ) {4 that.size.height = that.size.width / that.aspectRatio;5 continueResize = false;6 }7 }
that.parentData.width=1920,是指对话框父节点的宽度,也就是document的宽度,
that.size.width=911.6,前面的代码也有提过,这个是指对话框最终应被设置的宽度,
woset=1395.4,这个其实就是鼠标一开始按下时鼠标的位置,也就是对话框初始状态时的left值1389,相差的只是that.sizeDiff.width=6.4(上面的代码有提到),
对话框在初始状态下,因为对话框贴近浏览器右侧边缘,所以 对话框初始left(1389)+对话框初始width(530)=1919(约等于浏览器宽度1920),
再看看上面条件语句if(woset + that.size.width >= that.parentData.width), that.size.width用的是最新计算出来的对话框最终应被设置的宽度,
但woset却用了对话框初始left而不是最新计算出来的对话框最终应被设置的left(1000), 这样1395.4 + 911.6 = 2307 > 1920,导致最后这条语句被执行that.size.width = that.parentData.width - woset=1920 - 1395.4 = 524, 对话框最终应被设置的宽度被重置了!
对话框的宽度最终被设置为524(初始宽度是530),相当于对话框left值改变了,宽度却没有改变,调整大小的操作变成了拖拽的操作。对话框最终的大小和位置如下图6(width和left写反了):
所以,引起对话框宽度设置不正确的原因是上面那个判断语句中,对话框的宽度用了最新计算出来的宽度,left值却用了之前的值。或者说既然对话框最终应被设置的宽度和位置都已经计算出来了,就不用再做判断和重置对话框宽度了。
如果慢慢调整对话框大小的话,最新计算出来的left值和之前的left值相差不大,就算执行了宽度重置语句“that.size.width = that.parentData.width - woset;”,最终对话框被设置的宽度也不会相差太大,而且一般情况下鼠标抬起之前的两次mousemove事件鼠标的坐标基本一样或相差很小,也就是说鼠标抬起之前的一次鼠标移动事件中计算出来的对话框应被设置的left值跟前一次被设置的left值基本相等,所以鼠标抬起之前对话框的宽度又被“修正”了。
在上面的图1中也可以看出,再向左调整对话框大小的过程中,对话框的右侧一直在“抖动”和不断修正的过程,而且随着鼠标移动速度的增大,这种“抖动”过程会更加明显。
而从上面的图4中我们可以看到,如果对话框的位置不是放在靠近浏览器窗口的右侧边缘,是不会出现宽度设置不正确的问题的。因为这种情况下这个判断语句(woset + that.size.width >= that.parentData.width)的结果不太容易为真,特别是当对话框的位置离浏览器窗口有边缘较远的时候,下面的对话框宽度重置语句也不会执行。
根据上面的分析,当对话框底部贴近浏览器窗口底部并从对话框上面调整大小,或者对话框右下角贴近浏览器窗口右下角并从左上角方向调整大小时,都会出现对话框宽度或者高度设置不正确的问题。
如果把上面的对话框宽度重置语句去掉,或者把woset的值改为用最新的计算出来的对话框left值,就不会出现对话框宽度或者高度设置不正确的问题。如下:
1 woset = event.pageX; // 添加这一句,或者woset = cp2 if ( woset + that.size.width >= that.parentData.width ) {3 that.size.width = that.parentData.width - woset; // 或者把这一句注释,二选一4 if ( pRatio ) {5 that.size.height = that.size.width / that.aspectRatio;6 continueResize = false;7 }8 }
图7、修改了代码之后,无论怎么拖动调整大小,对话框窗口右侧不会发生偏离。
图8、快速调整大小也正常
图9、用脚本触发的方式也正常
至于jqueryui源代码里面为什么要把对话框宽度重置,以及为什么判断时使用“之前的left值”,还没想明白。
使用的jqueryui的版本为1.11.4 。
可能大家会有这样的疑问,即使jqueryui dialog有这样的问题,但是这根本不影响使用,可以说这根本就不算是什么问题,而且有谁会像我这样快速地对对话框进行拖拽?
但考虑一下下面的场景,我也是在这种情况下发现的这个问题。
最近我在做两个相同页面之间的事件同步,例如小屏端和大屏端运行同一个页面,我在小屏上做的操作需要同步到大屏上,其实这也是某个项目的需求。这时候我在小屏调整某个对话框的大小,如果实时同步到大屏的话,将会发送大量的请求。为了避免这个问题,就只把鼠标抬起之前的那次mousemove事件发送过去。所以在小屏端完成一次调整大小的操作,会把mouseover,mousedown,mousemove和mouseup事件的信息各发送一次到大屏端,大屏端收到消息之后再触发一次这些事件。这就跟我上面用事件模拟的过程一样,因为jqueryui对话框的重置宽度的问题,小屏端对对话框的调整大小操作,同步到大屏端之后就像是对话框的拖拽操作一样,只是对话框位置改变而大小没有改变。
在这种情况下,jqueryui对话框的这个问题就会造成比较大的影响。
新闻热点
疑难解答