之前在做兩個相同的頁面的事件同步時發(fā)現(xiàn)了這個問題,現(xiàn)在把它記錄下來。
頁面中的jqueryui對話框,如果把它拖動到靠近瀏覽器窗口右側(cè)邊緣,并快速從對話框左側(cè)調(diào)整對話框窗口大小時,對話框右側(cè)會偏離瀏覽器窗口右側(cè)邊緣,其實就是對話框窗口寬度計算不準(zhǔn)確。為了更好地說明問題,下面給出幾張示意圖。(黑色背景框是瀏覽器窗口)
圖1、對話框窗口開始放在瀏覽器右側(cè)邊緣,從左側(cè)緩慢調(diào)整窗口大小過程中,對話框窗口右側(cè)會發(fā)生“抖動”
  
圖2、對話框窗口開始放在瀏覽器右側(cè)邊緣,稍微快一點從左側(cè)調(diào)整窗口大小,對話框右側(cè)跟瀏覽器窗口邊緣出現(xiàn)間距
  
圖3、對話框窗口開始放在瀏覽器右側(cè)邊緣,快速從左側(cè)調(diào)整窗口大小,對話框右側(cè)的偏離情況更加明顯
  
圖4、如果對話框窗口的位置沒有靠近瀏覽器窗口右側(cè)邊緣,調(diào)整對話框大小的情況正常
  
以上幾張圖說明,當(dāng)對話框初始位置放在瀏覽器右側(cè)邊緣時,從左側(cè)調(diào)整對話框大小會出現(xiàn)對話框?qū)挾扔嬎悴徽_的問題。
你也可以自己試一試:http://jqueryui.com/dialog/
我們知道,鼠標(biāo)移動的事件并不是連續(xù)觸發(fā)的,即使兩次鼠標(biāo)移動事件之間的間隔很短。所以如果鼠標(biāo)移動得很快,在兩次鼠標(biāo)移動事件之間鼠標(biāo)移動的距離會比較大。快速調(diào)整對話框大小時,在兩次鼠標(biāo)移動事件觸發(fā)的間隔中鼠標(biāo)移動了較長距離。我們可以通過腳本創(chuàng)建并觸發(fā)事件的方式來模擬快速調(diào)整對話框大小的過程,看看在這個過程中執(zhí)行了什么操作并分析哪里出了問題。
為了模擬快速從對話框左側(cè)調(diào)整大小,下面會依次創(chuàng)建mouSEOver,mousedown,mousemove事件并在對話框左側(cè)的resize handler上面觸發(fā)(mousedown和mousemove事件的e.pageX相差較大)。
圖5、對話框的初始寬度和位置
  
首先在控制臺執(zhí)行下面的代碼,依次創(chuàng)建mouseover、mousedown和mousemove事件并觸發(fā)。
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);
接著跟蹤相應(yīng)事件處理程序的執(zhí)行。在jqueryui源代碼中,當(dāng)觸發(fā)mousemove事件時,下面的的代碼片段會執(zhí)行(添加了實時執(zhí)行結(jié)果的注釋):
1 _mouseDrag: function(event) { 2 3 var data, PRops, 4 smp = this.originalMousePosition, // 鼠標(biāo)剛按下時的位置(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(); // 設(shè)置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 ]); // 計算對話框最終應(yīng)設(shè)置的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); // 更新對話框最終應(yīng)設(shè)置的position和size,執(zhí)行完之后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
當(dāng)執(zhí)行到上面代碼的第27行時(this._propagate("resize", event);),用Chrome開發(fā)者工具跟蹤到以下這段代碼:
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 { // 會執(zhí)行到這里52 that.offset.left = that.element.offset().left; // 初始狀態(tài)的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,是指對話框父節(jié)點的寬度,也就是document的寬度,
that.size.width=911.6,前面的代碼也有提過,這個是指對話框最終應(yīng)被設(shè)置的寬度,
woset=1395.4,這個其實就是鼠標(biāo)一開始按下時鼠標(biāo)的位置,也就是對話框初始狀態(tài)時的left值1389,相差的只是that.sizeDiff.width=6.4(上面的代碼有提到),
對話框在初始狀態(tài)下,因為對話框貼近瀏覽器右側(cè)邊緣,所以 對話框初始left(1389)+對話框初始width(530)=1919(約等于瀏覽器寬度1920),
再看看上面條件語句if(woset + that.size.width >= that.parentData.width), that.size.width用的是最新計算出來的對話框最終應(yīng)被設(shè)置的寬度,
但woset卻用了對話框初始left而不是最新計算出來的對話框最終應(yīng)被設(shè)置的left(1000), 這樣1395.4 + 911.6 = 2307 > 1920,導(dǎo)致最后這條語句被執(zhí)行that.size.width = that.parentData.width - woset=1920 - 1395.4 = 524, 對話框最終應(yīng)被設(shè)置的寬度被重置了!
對話框的寬度最終被設(shè)置為524(初始寬度是530),相當(dāng)于對話框left值改變了,寬度卻沒有改變,調(diào)整大小的操作變成了拖拽的操作。對話框最終的大小和位置如下圖6(width和left寫反了):

所以,引起對話框?qū)挾仍O(shè)置不正確的原因是上面那個判斷語句中,對話框的寬度用了最新計算出來的寬度,left值卻用了之前的值。或者說既然對話框最終應(yīng)被設(shè)置的寬度和位置都已經(jīng)計算出來了,就不用再做判斷和重置對話框?qū)挾攘恕?/span>
如果慢慢調(diào)整對話框大小的話,最新計算出來的left值和之前的left值相差不大,就算執(zhí)行了寬度重置語句“that.size.width = that.parentData.width - woset;”,最終對話框被設(shè)置的寬度也不會相差太大,而且一般情況下鼠標(biāo)抬起之前的兩次mousemove事件鼠標(biāo)的坐標(biāo)基本一樣或相差很小,也就是說鼠標(biāo)抬起之前的一次鼠標(biāo)移動事件中計算出來的對話框應(yīng)被設(shè)置的left值跟前一次被設(shè)置的left值基本相等,所以鼠標(biāo)抬起之前對話框的寬度又被“修正”了。
在上面的圖1中也可以看出,再向左調(diào)整對話框大小的過程中,對話框的右側(cè)一直在“抖動”和不斷修正的過程,而且隨著鼠標(biāo)移動速度的增大,這種“抖動”過程會更加明顯。
而從上面的圖4中我們可以看到,如果對話框的位置不是放在靠近瀏覽器窗口的右側(cè)邊緣,是不會出現(xiàn)寬度設(shè)置不正確的問題的。因為這種情況下這個判斷語句(woset + that.size.width >= that.parentData.width)的結(jié)果不太容易為真,特別是當(dāng)對話框的位置離瀏覽器窗口有邊緣較遠(yuǎn)的時候,下面的對話框?qū)挾戎刂谜Z句也不會執(zhí)行。
根據(jù)上面的分析,當(dāng)對話框底部貼近瀏覽器窗口底部并從對話框上面調(diào)整大小,或者對話框右下角貼近瀏覽器窗口右下角并從左上角方向調(diào)整大小時,都會出現(xiàn)對話框?qū)挾然蛘吒叨仍O(shè)置不正確的問題。
如果把上面的對話框?qū)挾戎刂谜Z句去掉,或者把woset的值改為用最新的計算出來的對話框left值,就不會出現(xiàn)對話框?qū)挾然蛘吒叨仍O(shè)置不正確的問題。如下:
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、修改了代碼之后,無論怎么拖動調(diào)整大小,對話框窗口右側(cè)不會發(fā)生偏離。
  
圖8、快速調(diào)整大小也正常
  
圖9、用腳本觸發(fā)的方式也正常
  
至于jqueryui源代碼里面為什么要把對話框?qū)挾戎刂茫约盀槭裁磁袛鄷r使用“之前的left值”,還沒想明白。
使用的jqueryui的版本為1.11.4 。
可能大家會有這樣的疑問,即使jqueryui dialog有這樣的問題,但是這根本不影響使用,可以說這根本就不算是什么問題,而且有誰會像我這樣快速地對對話框進(jìn)行拖拽?
但考慮一下下面的場景,我也是在這種情況下發(fā)現(xiàn)的這個問題。
最近我在做兩個相同頁面之間的事件同步,例如小屏端和大屏端運行同一個頁面,我在小屏上做的操作需要同步到大屏上,其實這也是某個項目的需求。這時候我在小屏調(diào)整某個對話框的大小,如果實時同步到大屏的話,將會發(fā)送大量的請求。為了避免這個問題,就只把鼠標(biāo)抬起之前的那次mousemove事件發(fā)送過去。所以在小屏端完成一次調(diào)整大小的操作,會把mouseover,mousedown,mousemove和mouseup事件的信息各發(fā)送一次到大屏端,大屏端收到消息之后再觸發(fā)一次這些事件。這就跟我上面用事件模擬的過程一樣,因為jqueryui對話框的重置寬度的問題,小屏端對對話框的調(diào)整大小操作,同步到大屏端之后就像是對話框的拖拽操作一樣,只是對話框位置改變而大小沒有改變。
在這種情況下,jqueryui對話框的這個問題就會造成比較大的影響。
新聞熱點
疑難解答