1. 程式人生 > >最新版jQuery v3.3.1的BUG以及解決辦法(什麼問題不重要,怎麼解決問題才重要)

最新版jQuery v3.3.1的BUG以及解決辦法(什麼問題不重要,怎麼解決問題才重要)

發現問題

最新版的 FineUIPro v5.2.0 中,我們將內建的 jQuery v1.12.4 升級到 jQuery v3.3.1 ,可以看升級記錄

+升級到jQuery v3.3.1。
    -jQuery v3.x支援的瀏覽器:Chrome,Edge,Firefox,Safari,IE9+。
    -增加型別JSLibrary列舉值JQv1,用來引入jQuery v1.x。
    -如果需要支援IE8,請在Web.config中增加配置項JSLibrary=JQv1。
    -IE8有限支援並且複雜頁面可能會有效能問題,建議大家積極引導使用者使用現代瀏覽器。

之所以做這個升級,主要考慮如下因素:

1. IE8的市場份額逐漸萎縮,可以考慮將IE8的支援放到次要位置

2. 通過全部配置引數 JSLibrary=JQv1 引入老版本的 jQuery v1.12.4,這樣老客戶仍然可以支援IE8

3. 預設使用 jQuery v3.3.1,緊跟jQuery版本意味著較好的效能和安全性,以及遇到問題能夠及時解決

4. 考慮到 jQuery 這麼多年的發展,穩定性應該不是問題

整個升級過程還是很平緩的,沒有大的改動。

唯一讓我頭疼的時,好像表格中節點的位置總計算不對,比如在表格的單元格編輯時,如果表格沒有滾動條,顯示的編輯框是正確的:

但是,表格的滾動條一出現,編輯框就錯位了:

而這個問題,在使用 jQuery v1.12.4 時是不存在的!

分析問題

經過一段時間的除錯,最終確定這是最新版 jQuery v3.3.1 的一個BUG,出現這個問題需滿足如下條件:

1. 引用最新版 jQuery v3.3.1

2. 計算表格 tr 或者 td 的相對位置時出錯,比如:$('table tr:eq(1) td(0)').position()

為了更直觀的演示這個問題,我寫了個例子,其中HTML程式碼塊:

<div class="container">
  <table>
    <tr>
      <
td>row-1</td> </tr> <tr> <td id="theTD">row-2</td> </tr> </table> </div> <div id="result"> </div>

CSS程式碼塊:

.container {
  background-color: lightgreen;
  padding: 50px;
  position: relative;
}

table {
  width: 100%;
  border-collapse: collapse;
  border-spacing: 0;
  background-color: green;
}

table td {
  padding: 0;
  height: 50px;
  color: #fff;
  vertical-align: top;
}

為了更直觀的描述問題,我們用不同的背景色標識外層的容器(.container)和內部的表格(table)。

JavaScript程式碼塊:

$(function() {
    var tdPosition = $('#theTD').position();
    $('#result').html('top:' + tdPosition.top + ' left:' + tdPosition.left);
});

按照正常的思維模式,上面的 theTD 的相對位置應該是相對 .container 的偏移量(因為 .container 是 td 的第一個遇到的相對定位的父元素)。 

因為 .container 設定了 50px 的內邊距,表格每行的高度為 50px,所以 theTD 距離 .container 的垂直高度應該是 100px。

所以我們期望的輸出結果應該是:

top:100 left:50

可見,在 jQuery v3.3.1 中,獲取表格中 td 節點的相對位置(position)得到的結果並不是我們想要的。那這個值到底是什麼?

top: 50 left:0

解決問題

經過認真分析,我認為這個值是 td 相對外部 table 節點的偏移量,而不是相對於第一個浮動父節點的位置(position: relative / absolute)。

曾經一度我對 position() 和 offset() 的確切含義產生了懷疑,難道是我理解錯了?後來發現我的理解沒有問題:

.offsetParent() is supposed to return the nearest positioned element, where "positioned" means it has a css position attribute of "relative", "absolute", or "fixed".

我在很多地方依賴於 position 的計算解決,難道是 jQuery 計算 offsetParent 節點時出了差錯:

開啟瀏覽器的除錯視窗,我輸入如下程式碼:

$('#theTD').offsetParent()

可見,通過 jQuery 獲取到的依然是 .container, 這個方法返回的沒問題。

沒辦法,既然出了問題,只好先在自己的程式碼中修正了。

第一次嘗試

既然 td 的相對位置是相對於 table,那不如用 table 做箇中轉,計算出 table 的 position 加上去不就行了,如下所示:

$(function() {
    var tdPosition = $('#theTD').position();
   var tablePosition = $('#theTD').parents('table').position();
  
    $('#result').html('top:' + 
      (tdPosition.top + tablePosition.top) + ' left:' + 
      (tdPosition.left + tablePosition.left));
    
});

看著好像是正確的,後來測試發現遇到巢狀表格就不行了,如下示例:

<div class="container">
<table class="table-outer">
  <tr>
    <td>table1-row1</td>
  </tr>
  <tr>
      <td>
        <table>
          <tr>
            <td>row-1</td>
          </tr>
          <tr>
            <td id="theTD">row-2</td>
          </tr>
        </table>
    </td>
   </tr>
 </table>
</div>
<div id="result">
</div>
.container {
  background-color: lightgreen;
  padding: 50px;
  position: relative;
}

.table-outer {
  background-color: blue;
}
table {
  width: 100%;
  border-collapse: collapse;
  border-spacing: 0;
  background-color: green;
}

table td {
  padding: 0;
  height: 50px;
  color: #fff;
  vertical-align: top;
}

第二次嘗試

一計不成,再生一計。既然 table 的 position 出問題,那麼 div 的 position 應該是正常的吧。這次我就來在 td 裡面動態建立一個 div 節點怎麼樣?

$(function() {
  var tmpDiv = $('<div style="position:relative;"/>').prependTo($('#theTD'));
  var tdPosition = tmpDiv.position();
  tmpDiv.remove();
  
    $('#result').html('top:' + tdPosition.top + ' left:' + tdPosition.left);
});

看著好像沒有問題,可以問題依舊,當設定 td 的 vertical-align: middle 時,問題暴露出來了,因為此時 td 內的 div 是居中顯示的:

第三次嘗試

就在我一籌莫展時,我忽然想到前面的 offsetParent() 函式,這個函式能獲取正確的父節點。既然 position() 函式有問題,我何不嘗試 offset() 函式。

1. jQuery.position(): 獲取元素相對於父元素的偏移量(position: relative / absolute)

2. jQuery.offset(): 獲取元素相對於視口的偏移量

$(function() {
    var tdOffset = $('#theTD').offset();
    var tdParentOffset = $('#theTD').offsetParent().offset();
    $('#result').html('top:' + (tdOffset.top  - tdParentOffset.top) + ' left:' + (tdOffset.left - tdParentOffset.left));
});

這次使用當前元素和相對父元素的 offset 相減,得到的結果是否我們所需要的呢?

不錯,正是我們所需要的。因為這個邏輯判斷很簡單,和是否表格巢狀沒關係,所以測試下第一次嘗試失敗的情況:

 Bingo! 一切正常。此問題圓滿解決!

真是的jQuery的BUG嗎?

想想就不可思議,一個被全球使用者使用的公共JavaScript居然有這樣的BUG,難道就沒人發現麼?

網上搜索了一圈,看起來我多慮了,早就有使用者提出這個問題:

https://github.com/jquery/api.jquery.com/issues/1081

 Per the spec, an offsetParent is defined as an element with a position that is non-static or is a table, th, or td element. Since 3.3.0, position started using the native offsetParent property which started respected table, th, and td as offset parents, but the .offsetParent() method was left unchanged. We'll fix this in an upcoming release, but we'll need to note the special behavior for those 3 elements in the docs.

這個是 jQuery 核心開發團隊給出的答案,大意是說 offsetParent 這個節點屬性指的是這樣一個父節點:

1. 節點是非靜態的,也就是擁有 position: relative / absolute / fixed 樣式

2. 節點是 table, th 或者 td

哈,這麼多年過去了,我居然第一次聽說 offsetParent 還有相對於 td,th,table這個說法,儘管這個td,th,table節點是 position:static,下面使用程式碼來驗證一下:

$(function() {
    var tdPosition = $('#theTD').position();
  var offsetParent = $('#theTD').offsetParent()[0].tagName;
  var nativeOffsetParent = $('#theTD')[0].offsetParent.tagName;
    $('#result').html('top:' + tdPosition.top + ' left:' + tdPosition.left + 
           '<br>offsetParent():' + offsetParent + 
      '<br>Native offsetParent:' + nativeOffsetParent);
});

分別在不同瀏覽器中檢視結果。

Chrome:

Firefox:

Edge:

IE11(由於IE11打不開jsfiddle網站,我們單獨建立了一個頁面):

甚至IE9也是這樣的:

好吧,看來我真是孤陋而寡聞了。

jQuery 是在 v3.3.0 開始考慮到第二種情況的,但是 jQuery.offsetParent() 的定義沒有改變。這種不一致性本身就是一個BUG。

而不和之前的 jQuery v1.x, v2.x 乃至於 v3.3.0 之前的版本相容,對於一個廣泛使用的公共庫而言,更應該算是一個BUG,或者至少提供相容之間的方法。

考慮另一個更現實的問題:在實際應用中,我們為什麼要獲取 td ,table 的 position() 呢?

不是為了拿獲取到的值去更新其他 td 或者 table 的 top/left 屬性!

而是為了拿獲取的值去更新其他相關 div 節點的 top/left 屬性,而 div 節點的 position() 是相對於 position: relative / absolute / fixed 父節點的,而非 td ,table 節點,這就一定會出問題。

所以,我們還是認定這是一個BUG。

但是 jQuery 官方貌似沒有修正這個問題的意思,而是可能在將來版本修改  jQuery.offsetParent() 的定義:

但是jQuery你這樣做真的好麼,都10多年了你都沒遵守標準,突然來個小版本號稱支援標準,搞的和之前的程式碼不相容,你讓之前的程式碼情何以堪!

不管怎樣,我們有了自己的解決辦法。

小結

這篇文章描述了我們 FineUIPro 產品 更新中遇到的一個問題,最終將問題定位到 jQuery.position() 函式,雖然jQuery的做法是依照HTML規範來的,但是 jQuery.offsetParent() 和 jQuery.position() 兩個函式有衝突,並且會導致之前的jQuery外掛出錯,應該算是一個BUG吧。

好在本文給出了一個補救的方法,如果這裡的方法能夠對你有所啟發或者幫助,就給個推薦唄。