JS奇技之利用scroll來監(jiān)聽resize詳解
前言
大家都知道知道原生的 resize 事件只能作用于 defaultView 即 window 上,那么我們應(yīng)該通過什么樣的方式來監(jiān)聽其他元素的大小改變呢?筆者最近學(xué)習(xí)發(fā)現(xiàn)了一種神奇的方法,通過 scroll 事件來間接實現(xiàn) resize 事件的監(jiān)聽,本文將對這種方式進(jìn)行原理的剖析與代碼實現(xiàn)。
原理
首先,我們先來看一下 scroll 事件是干嘛的。
The scroll event is fired when the document view or an element has been scrolled.
當(dāng)文檔視圖或者元素滾動的時候會觸發(fā) scroll 事件。
也就是說元素滾動的時候會觸發(fā)這個事件,那么什么時候元素會滾動?當(dāng)元素大于其父級元素,且父級元素允許其滾動的時候,該元素可以進(jìn)行滾動。換句話說,元素可以滾動意味著父子元素大小不一致,這是這個方法的核心。
那么我們需要讓元素大小發(fā)生改變時,使得 scrollLeft 或者 scrollTop 發(fā)生改變,從而觸發(fā) scroll 事件,進(jìn)一步得知其大小發(fā)生了改變。
監(jiān)聽元素變大
元素變大的時候,我們可以看到更多,其內(nèi)部可滾動區(qū)域?qū)⒙郎p小,但這并不會造成滾動條位置的改變,但當(dāng)元素大到讓滾動條消失的時候會讓 scrollLeft 或者 scrollTop 變成 0,這樣我們就知道了元素變大了,因此我們其實只需要 1px 來判斷,其圖示如下:

監(jiān)聽元素變小
當(dāng)元素變小的時候,可滾動區(qū)域會變大,滾動條的位置其實并不會進(jìn)行改變,這里采取的做法是,讓可滾動區(qū)域和父元素成一定的比例一起縮小,讓父元素來擠壓滾動區(qū)域,從而間接改變滾動條 scrollLeft 或者 scrollTop 的大小,文字描述可能不是很清楚,我們看下圖:

通過以上兩種方式,我們可以就可以獲得 resize 事件。
實現(xiàn)
首先,為了不影響原有的元素,我們應(yīng)當(dāng)創(chuàng)建一個和要監(jiān)聽元素等大的元素,并對其進(jìn)行相關(guān)操作,然后我們需要兩個子元素來分別監(jiān)聽元素變大和元素變小兩個情況。因此構(gòu)造如下的 HTML 結(jié)構(gòu):
<div class="resize-triggers"> <div class="expand-trigger"> <div></div> </div> <div class="contract-trigger"></div> </div>
他們對應(yīng)的 CSS 如下:
.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 的子元素寬高應(yīng)當(dāng)保持大于父元素 1px,且兩個觸發(fā)器都應(yīng)當(dāng)保持在最右下角的狀態(tài),因此我們可以實現(xiàn)如下的狀態(tài)重置函數(shù),并在初始化和每次滾動事件的時候調(diào)用:
/**
* 重置觸發(fā)器
* @param element 要處理的元素
*/
const resetTrigger = function(element) {
const trigger = element.__resizeTrigger__; // 要重置的觸發(fā)器
const expand = trigger.firstElementChild; // 第一個子元素,用來監(jiān)聽變大
const contract = trigger.lastElementChild; // 最后一個子元素,用來監(jiān)聽變小
const expandChild = expand.firstElementChild; // 第一個子元素的第一個子元素,用來監(jiān)聽變大
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; // 滾動到最右
};
我們可以用如下函數(shù)檢測元素大小是否發(fā)生了改變:
/**
* 檢測觸發(fā)器狀態(tài)
* @param element 要檢查的元素
* @returns {boolean} 是否改變了大小
*/
const checkTriggers = function(element) {
// 寬度或高度不一致就返回true
return element.offsetWidth !== element.__resizeLast__.width || element.offsetHeight !== element.__resizeLast__.height;
};
最終,我們可以實現(xiàn)簡單的事件監(jiān)聽的添加:
/**
* 添加大小更改監(jiān)聽
* @param element 要監(jiān)聽的元素
* @param fn 回調(diào)函數(shù)
*/
export const addResizeListener = function(element, fn) {
if (isServer) return; // 服務(wù)器端直接返回
if (attachEvent) { // 處理低版本ie
element.attachEvent('onresize', fn);
} else {
if (!element.__resizeTrigger__) { // 如果沒有觸發(fā)器
if (getComputedStyle(element).position === 'static') {
element.style.position = 'relative'; // 將static改為relative
}
createStyles();
element.__resizeLast__ = {}; // 初始化觸發(fā)器最后的狀態(tài)
element.__resizeListeners__ = []; // 初始化觸發(fā)器的監(jiān)聽器
const resizeTrigger = element.__resizeTrigger__ = document.createElement('div'); // 創(chuàng)建觸發(fā)器
resizeTrigger.className = 'resize-triggers';
resizeTrigger.innerHTML = '<div class="expand-trigger"><div></div></div><div class="contract-trigger"></div>';
element.appendChild(resizeTrigger); // 添加觸發(fā)器
resetTrigger(element); // 重置觸發(fā)器
element.addEventListener('scroll', scrollListener, true); // 監(jiān)聽滾動事件
/* Listen for a css animation to detect element display/re-attach */
// 監(jiān)聽CSS動畫來檢測元素顯示或者重新添加
if (animationStartEvent) { // 動畫開始
resizeTrigger.addEventListener(animationStartEvent, function(event) { // 增加動畫開始的事件監(jiān)聽
if (event.animationName === RESIZE_ANIMATION_NAME) { // 如果是大小改變事件
resetTrigger(element); // 重置觸發(fā)器
}
});
}
}
element.__resizeListeners__.push(fn); // 加入該回調(diào)
}
};
以及如下的函數(shù)來移除事件監(jiān)聽:
/**
* 移除大小改變的監(jiān)聽
* @param element 被監(jiān)聽的元素
* @param fn 對應(yīng)的回調(diào)函數(shù)
*/
export const removeResizeListener = function(element, fn) {
if (attachEvent) { // 處理ie
element.detachEvent('onresize', fn);
} else {
element.__resizeListeners__.splice(element.__resizeListeners__.indexOf(fn), 1); // 移除對應(yīng)的回調(diào)函數(shù)
if (!element.__resizeListeners__.length) { // 如果全部時間被移除
element.removeEventListener('scroll', scrollListener); // 移除滾動監(jiān)聽
element.__resizeTrigger__ = !element.removeChild(element.__resizeTrigger__); // 移除對應(yīng)的觸發(fā)器,但保存下來
}
}
};
其他
其中有部分內(nèi)容是用來優(yōu)化的,并不影響基礎(chǔ)功能,如對服務(wù)器渲染、客戶端渲染的區(qū)分,對 IE 的特殊處理,以及通過 opacity 的動畫來解決 chrome 上的bug。
好了,以上就是這篇文章的全部內(nèi)容了,希望本文的內(nèi)容對大家的學(xué)習(xí)或者工作能帶來一定的幫助,如有疑問大家可以留言交流,謝謝大家對腳本之家的支持。
相關(guān)文章
在JS中a標(biāo)簽加入單擊事件屏蔽href跳轉(zhuǎn)頁面
這篇文章主要介紹了JS中a標(biāo)簽加入單擊事件屏蔽href跳轉(zhuǎn)頁面的相關(guān)資料,需要的朋友可以參考下2016-12-12
JavaScript實現(xiàn)解析INI文件內(nèi)容的方法
這篇文章主要介紹了JavaScript實現(xiàn)解析INI文件內(nèi)容的方法,結(jié)合實例形式分析了javascript通過自定義函數(shù)實現(xiàn)針對ini文件解析操作的相關(guān)處理技巧,需要的朋友可以參考下2016-11-11
深入理解JavaScript系列(11) 執(zhí)行上下文(Execution Contexts)
本章我們要講解的是ECMAScript標(biāo)準(zhǔn)里的執(zhí)行上下文和相關(guān)可執(zhí)行代碼的各種類型2012-01-01
解決IE下select標(biāo)簽innerHTML插入option的BUG(兼容IE,FF,Opera,Chrome,Safa
在ie下面使用innerHTML來插入option選項的話,ie會去掉前面的<option>,并拆分成多個節(jié)點(diǎn),這樣會造成select的出錯2010-05-05
js實現(xiàn)增加數(shù)字顯示的環(huán)形進(jìn)度條效果
本文主要分享了js實現(xiàn)增加數(shù)字顯示的環(huán)形進(jìn)度條效果的示例代碼。具有一定的參考價值,下面跟著小編一起來看下吧2017-02-02

