JS奇技之利用scroll來監聽resize詳解
前言
大家都知道知道原生的 resize 事件只能作用於 defaultView 即 window 上,那麼我們應該通過什麼樣的方式來監聽其他元素的大小改變呢?筆者最近學習發現了一種神奇的方法,通過 scroll 事件來間接實現 resize 事件的監聽,本文將對這種方式進行原理的剖析與程式碼實現。
原理
首先,我們先來看一下 scroll 事件是幹嘛的。
The scroll event is fired when the document view or an element has been scrolled.
當文件檢視或者元素滾動的時候會觸發 scroll 事件。
也就是說元素滾動的時候會觸發這個事件,那麼什麼時候元素會滾動?當元素大於其父級元素,且父級元素允許其滾動的時候,該元素可以進行滾動。換句話說,元素可以滾動意味著父子元素大小不一致,這是這個方法的核心。
那麼我們需要讓元素大小發生改變時,使得 scrollLeft 或者 scrollTop 發生改變,從而觸發 scroll 事件,進一步得知其大小發生了改變。
監聽元素變大
元素變大的時候,我們可以看到更多,其內部可滾動區域將慢慢減小,但這並不會造成滾動條位置的改變,但當元素大到讓滾動條消失的時候會讓 scrollLeft 或者 scrollTop 變成 0,這樣我們就知道了元素變大了,因此我們其實只需要 1px 來判斷,其圖示如下:
監聽元素變小
當元素變小的時候,可滾動區域會變大,滾動條的位置其實並不會進行改變,這裡採取的做法是,讓可滾動區域和父元素成一定的比例一起縮小,讓父元素來擠壓滾動區域,從而間接改變滾動條 scrollLeft 或者 scrollTop 的大小,文字描述可能不是很清楚,我們看下圖:
通過以上兩種方式,我們可以就可以獲得 resize 事件。
實現
首先,為了不影響原有的元素,我們應當建立一個和要監聽元素等大的元素,並對其進行相關操作,然後我們需要兩個子元素來分別監聽元素變大和元素變小兩個情況。因此構造如下的 HTML 結構:
?123456 | < div class = "resize-triggers" > < div class = "expand-trigger" > < div ></ div > </ div > < div class = "contract-trigger" ></ div > </ div > |
他們對應的 CSS 如下:
1234567891011121314151617181920212223242526 | .resize-triggers { visibility : hidden ; opacity: 0 ; } .resize-triggers, .resize-triggers > div, .contract-trigger:before { content : " " ; display : block ; position : absolute ; top : 0 ; left : 0 ; height : 100% ; width : 100% ; overflow : hidden ; } .resize-triggers > div { overflow : auto ; } .contract-triggers:before { width : 200% ; height : 200% ; } |
其中 .expand-triggers
的子元素寬高應當保持大於父元素 1px,且兩個觸發器都應當保持在最右下角的狀態,因此我們可以實現如下的狀態重置函式,並在初始化和每次滾動事件的時候呼叫:
1234567891011121314151617 | /** * 重置觸發器 * @param element 要處理的元素 */ const resetTrigger = function (element) { const trigger = element.__resizeTrigger__; // 要重置的觸發器 const expand = trigger.firstElementChild; // 第一個子元素,用來監聽變大 const contract = trigger.lastElementChild; // 最後一個子元素,用來監聽變小 const expandChild = expand.firstElementChild; // 第一個子元素的第一個子元素,用來監聽變大 contract.scrollLeft = contract.scrollWidth; // 滾動到最右 contract.scrollTop = contract.scrollHeight; // 滾動到最下 expandChild.style.width = expand.offsetWidth + 1 + 'px' ; // 保持寬度多1px expandChild.style.height = expand.offsetHeight + 1 + 'px' ; // 保持高度多1px expand.scrollLeft = expand.scrollWidth; // 滾動到最右 expand.scrollTop = expand.scrollHeight; // 滾動到最右 }; |
我們可以用如下函式檢測元素大小是否發生了改變:
?123456789 | /** * 檢測觸發器狀態 * @param element 要檢查的元素 * @returns {boolean} 是否改變了大小 */ const checkTriggers = function (element) { // 寬度或高度不一致就返回true return element.offsetWidth !== element.__resizeLast__.width || element.offsetHeight !== element.__resizeLast__.height; }; |
最終,我們可以實現簡單的事件監聽的新增:
?123456789101112131415161718192021222324252627282930313233343536373839 | /** * 新增大小更改監聽 * @param element 要監聽的元素 * @param fn 回撥函式 */ export const addResizeListener = function (element, fn) { if (isServer) return ; // 伺服器端直接返回 if (attachEvent) { // 處理低版本ie element.attachEvent( 'onresize' , fn); } else { if (!element.__resizeTrigger__) { // 如果沒有觸發器 if (getComputedStyle(element).position === 'static' ) { element.style.position = 'relative' ; // 將static改為relative } createStyles(); element.__resizeLast__ = {}; // 初始化觸發器最後的狀態 element.__resizeListeners__ = []; // 初始化觸發器的監聽器 const resizeTrigger = element.__resizeTrigger__ = document.createElement( 'div' ); // 建立觸發器 resizeTrigger.className = 'resize-triggers' ; resizeTrigger.innerHTML = '<div class="expand-trigger"><div></div></div><div class="contract-trigger"></div>' ; element.appendChild(resizeTrigger); // 新增觸發器 resetTrigger(element); // 重置觸發器 element.addEventListener( 'scroll' , scrollListener, true ); // 監聽滾動事件 /* Listen for a css animation to detect element display/re-attach */ // 監聽CSS動畫來檢測元素顯示或者重新新增 if (animationStartEvent) { // 動畫開始 resizeTrigger.addEventListener(animationStartEvent, function (event) { // 增加動畫開始的事件監聽 if (event.animationName === RESIZE_ANIMATION_NAME) { // 如果是大小改變事件 resetTrigger(element); // 重置觸發器 } }); } } element.__resizeListeners__.push(fn); // 加入該回調 } }; |
以及如下的函式來移除事件監聽:
?12345678910111213141516 | /** * 移除大小改變的監聽 * @param element 被監聽的元素 * @param fn 對應的回撥函式 */ export const removeResizeListener = function (element, fn) { if (attachEvent) { // 處理ie element.detachEvent( 'onresize' , fn); } else { element.__resizeListeners__.splice(element.__resizeListeners__.indexOf(fn), 1); // 移除對應的回撥函式 if (!element.__resizeListeners__.length) { // 如果全部時間被移除 element.removeEventListener( 'scroll' , scrollListener); // 移除滾動監聽 element.__resizeTrigger__ = !element.removeChild(element.__resizeTrigger__); // 移除對應的觸發器,但儲存下來 } } }; |
其他
其中有部分內容是用來優化的,並不影響基礎功能,如對伺服器渲染、客戶端渲染的區分,對 IE 的特殊處理,以及通過 opacity 的動畫來解決 chrome 上的bug。
好了,以上就是這篇文章的全部內容了,希望本文的內容對大家的學習或者工作能帶來一定的幫助,如有疑問大家可以留言交流,謝謝大家對指令碼之家的支援。