上週產品小哥哥丟過來一個需求,名曰:沉浸式視頻體驗,大體內容是一個頁面裏有幾十個視頻,用戶點擊其中一個視頻時,該視頻自動滑動到屏幕可視區域的頂部開始播放,並暫停其餘視頻,該視頻滑出屏幕可視區域以後要自動暫停。html
這個需求有兩個關鍵的技術點:前端
其實這兩個技術點有一個共同點,就是需求計算出元素在頁面中的絕對位置,也就是指該元素的左上角相對於整張網頁左上角的座標,有兩種方法能夠計算獲得:git
利用offsetTop和offsetLeft能夠取到當前元素左上角相對於其HTMLElement.offsetParent
節點的左邊界偏移的像素值,而後再利用HTMLElement.offsetParent
能夠獲得一個指向最近的包含該元素的定位元素。github
若是沒有定位的元素,則offsetParent
爲最近的table
,table cell
或根元素(標準模式下爲html
;quirks
模式下爲body
)。web
利用以上三個屬性,寫一個遞歸函數就能夠獲得當前元素在頁面中的絕對位置了:算法
const getElementLeft = element => { let actualLeft = element.offsetLeft; let current = element.offsetParent; while (current !== null){ actualLeft += current.offsetLeft; current = current.offsetParent; } return actualLeft; } const getElementTop = element => { let actualTop = element.offsetTop; let current = element.offsetParent; while (current !== null){ actualTop += current.offsetTop; current = current.offsetParent; } return actualTop; }
注意:因爲在表格和iframe中,offsetParent
對象未必等於父容器,因此上面的函數對於表格和iframe中的元素不適用。小程序
object.getBoundingClientRect()
的返回值包含了一組只讀屬性,包括該元素相對於視口左上角位置的left
、top
、right
和bottom
,以及元素的width
和height
,單位爲像素,具體含義可參見下圖:
segmentfault
從上圖能夠看出,getBoundingClientRect
獲得的值是相對於視口的,而不是絕對的,當視口區域或其餘可滾動元素內發生滾動操做時,top和left屬性值就會隨之當即發生變化。dom
要得到相對於整個網頁左上角定位的屬性值,只要給top、left屬性值加上當前的滾動位置(經過window.scrollX
和window.scrollY
),這樣就能夠獲取與當前的滾動位置無關的值。函數
考慮到getBoundingClientRect
的兼容性較好,且算法複雜度較低,最終我採用了getBoundingClientRect
方法來實現「將視頻滑動到屏幕可視區域的頂部」的功能,使用window.scroll
來實現頁面滾動,使用setTimeout
增長滾動時的動畫,具體實現滾動的函數以下:
const autoScroll = (offsetTop, needScrollTop, hasScrollTop) => { let _needScrollTop = needScrollTop; // 本次遞歸時,離終點的距離 let _hasScrollTop = hasScrollTop; // 本次遞歸時,已經移動的距離總和 const speed = 10; setTimeout(() => { const dist = needScrollTop > 0 ? Math.max(Math.ceil(needScrollTop / speed), 5) : Math.min(Math.ceil(needScrollTop / speed), -5); _needScrollTop -= dist; _hasScrollTop += dist; window.scroll(0, offsetTop + _hasScrollTop); // 若是移動幅度小於十個像素,直接移動,不然遞歸調用,實現動畫效果 if (_needScrollTop > speed || _needScrollTop < -speed) { this.__onScroll(offsetTop, _needScrollTop, _hasScrollTop); } else { window.scroll(0, offsetTop + _hasScrollTop + _needScrollTop); } }, 1); } const rect = element.getBoundingClientRect(); const offsetTop = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || window.screenY; autoScroll(offsetTop, rect.top, 0);
利用getBoundingClientRect
的返回值一樣能夠判斷元素在屏幕可視區域的曝光和滑出,經過監聽頁面的滾動事件,在滾動結束時,觸發checkLeave
和checkExpose
函數,具體代碼以下:
// 檢測滑出可視區域 checkLeave() { const element = document.querySelector(`[data-action-id="${this.__domId}"]`); if (!element) { console.error(`Action: element [data-action-id="${this.__domId}"] not found`); return; } const { top, bottom, left, right } = element.getBoundingClientRect(); if ((top > getWindowHeight() || bottom < 0 || left > getWindowWidth() || right < 0)) { this.onLeave(); //onLeave函數中實現具體業務邏輯 } } // 檢測真實曝光 checkExpose() { const element = document.querySelector(`[data-action-id="${this.__domId}"]`); if (!element) { console.error(`Action: element [data-action-id="${this.__domId}"] not found`); return; } const { top, bottom, left, right } = element.getBoundingClientRect(); if (Math.max(0, top) <= Math.min(getWindowHeight(), bottom) && Math.max(0, left) <= Math.min(getWindowWidth(), right)) { this.onExpose(); //onExpose函數中實現具體業務邏輯 } }
能夠將這兩個事件封裝成了一個<Action>
組件的兩個props參數,使用時只須要在須要的元素外包一層<Action>
父元素,並傳入特定的回調函數便可。
BTW,多說一句我實現document.querySelector
的原理,直接看代碼:
render() { const { children } = this.props; return cloneElement(Children.only(children), { 'data-action-id': this.__domId, }); }
以上。
做者:TNFE 二小
TNFE團隊爲前端開發人員整理出了小程序以及web前端技術領域的最新優質內容,每週更新✨,歡迎star,github地址:https://github.com/Tnfe/TNFE-Weekly歡迎加入騰訊前端技術交流QQ羣:784383520