整合營銷服務(wù)商

          電腦端+手機端+微信端=數(shù)據(jù)同步管理

          免費咨詢熱線:

          前端基礎(chǔ)面試:什么是滾動穿透?有哪些解決滾動穿透問題

          前端基礎(chǔ)面試:什么是滾動穿透?有哪些解決滾動穿透問題的方法?

          著移動端市場的份額越大,需求就越多樣化。今天討論的是移動端的滾動穿透問題。需求中彈窗浮層還是挺常見的,那這個和滾動穿透有什么聯(lián)系呢?

          先解釋下什么是滾動穿透

          頁面滑出了一個彈窗,我們用手指觸摸屏幕滑動時,會發(fā)現(xiàn)彈窗下面的內(nèi)容還是在滾動。這個現(xiàn)象就是滾動穿透

          需求

          需求: 希望在點擊圖片的時候,從下方彈一個全屏的彈框來描述這張圖片的詳情。

          方案

          接到這個需求覺得沒有難度,很快就提測了。突然意識到寫彈窗的時候忘記處理滾動穿透的問題了。

          方案一:

          第一個方法就是當彈窗觸發(fā)的時候,給 overflow: scroll的元素加上一個 class (一般都是 body 元素)。退出的時候去掉這個 class。為了方便,會直接用 body 元素來代指彈窗下方的元素。

          // css 部分
          modal_open {
           position: fixed;
           height: 100%;
          }
          
          
          // js 部分
          document.body.classList.add('modal_open');
          document.body.classList.remove('modal_open');
          

          上面的這個方法可以解決滾動穿透問題,卻也會帶來新的問題。

          即:

          body 的滾動位置會丟失,也就是body 的 scrollTop 屬性值會變?yōu)?0。

          這個新問題比起滾動穿透本身來說更加麻煩,所以這個方案是要進行優(yōu)化的。

          方案二:

          既然添加 modal_open 這個 class 會使 body 的滾動位置會丟失,那么我們?yōu)槭裁床辉跐L動位置丟失之前先保存下來,等到退出彈窗的前在將這個保存下來的滾動位置在設(shè)置回去。

          // css 部分
          .modal_open {
           position: fixed;
           height: 100%;
          }
          // js 部分
          /**
           * ModalHelper helpers resolve the modal scrolling issue on mobile devices
           * https://github.com/twbs/bootstrap/issues/15852
           */
          var ModalHelper=(function(bodyClass) {
           var scrollTop;
           return {
           afterOpen: function() {
           scrollTop=document.scrollingElement.scrollTop ||
           document.documentElement.scrollTop || 
           document.body.scrollTop;
           document.body.classList.add(bodyClass);
           document.body.style.top=-scrollTop + 'px';
           },
           beforeClose: function() {
           document.body.classList.remove(bodyClass);
           document.scrollingElement.scrollTop=document.documentElement.scrollTop=document.body.scrollTop=scrollTop;
           }
           };
          })('modal_open');
          // method
          modalSwitch: function(){
           let self=this;
           if( self.switchFlag==='close' ){
           ModalHelper.afterOpen();
           self.switchFlag='open';
           }else{
           ModalHelper.beforeClose();
           self.switchFlag='close';
           }
          }
          

          jQuery:

          方案二可以達到以下效果:

          1. 彈窗滾動的時候,下方的 body 是固定的無法滾動;
          2. body 的滾動位置不會丟失;
          3. body 有 scroll 事件;

          方案二可以適應(yīng)絕大多數(shù)的彈窗需求,提測后測試方也沒有在提其他問題,這個問題算是完美的解決了。不過有一個疑問:

          IOS 自有的橡皮筋效果會導致頁面會出現(xiàn)短暫卡頓現(xiàn)象,暫時沒有找到原因,請教各位。

          其他方案:

          使用 preventDefault 阻止瀏覽器默認事件:

          var modal=document.getElementById('modalBox');
          modal.addEventListener('touchmove', function(e) {
           e.preventDefault();
          }, false);
          

          這個方案只適用于這個彈窗本身的高度小于屏幕的高度,即不可滾動的時候。touchmove 比 touchstart 更加合適。因為 touchstart 會連點擊事件都阻止。

          使用插件:

          除非是自己實現(xiàn)起來太復雜,否則還是自己花點時間去實現(xiàn)。

          原因有二:

          1. 使用插件就意味著需要引入的文件至少多了一個。
          2. 插件過多,擔心日后項目升級維護成本加大。

          、首先看一看是什么效果?

          2、一行background-attachment:fixed搞定鏤空信息流廣告

          這個方法理論上是最簡單效果也最好的方法。

          CSS中有個background-attachment屬性,當我們設(shè)置屬性值為fixed的時候,背景圖片相對于窗體定位,不受滾動影響。

          于是,我們的實現(xiàn)就很簡單:信息流列表HTML中插入個廣告<a>鏈接,然后廣告圖作為背景圖呈現(xiàn),設(shè)置background-attachment:fixed效果就可以實現(xiàn)了,就這么簡單。

          HTML和CSS代碼示意:

          <div class="list">信息流列表1</div>
          <div class="list">信息流列表2</div>
          <a href="#" class="ad" target="_blank">廣告</a>
          <div class="list">信息流列表3</div>
          <div class="list">信息流列表4</div>
          .ad {
           display: block;
           height: 600px;
           background: url(./ad.jpg) no-repeat top fixed;
           background-size: 100%;
          }
          

          唯一的不足:iOS Safari不支持background-attachment:fixed

          iOS Safari很早時候position:fixed也不支持,后來妥協(xié)了,支持了;但是background-attachment:fixed還是老樣子,不支持,怕是嫌棄background-attachment:fixed燒性能,對于一個連IE6,IE7瀏覽器都支持良好的CSS聲明,Safari不支持(包括iOS微信),我也無力說些什么。

          因此,我們還需要額外做些功夫,兼容下iPhone手機瀏覽器。

          我的做法是如果是iPhone手機,廣告圖片postion:fixed定位,配合JS實時clip剪裁。核心JS如下:

          // ele就是廣告元素DOM對象
          window.addEventListener('scroll', function () {
           var bound=ele.parentElement.getBoundingClientRect();
           var clip='rect('+ [bound.top + 'px', ele.parentElement.clientWidth + 'px', bound.bottom + 'px', 0].join() +')';
           ele.style.clip=clip;
          });
          

          查了下瀏覽器兼容性資料,發(fā)現(xiàn)Android4.4+版本開始,放棄了對background-attachment:fixed的支持,但是Android Chrome瀏覽器卻支持,這有些令人不解(見下圖)。

          ?

          我用家里人的Android手機測試,背景效果表現(xiàn)為scroll,看來JS補丁要多個Android設(shè)備。

          3、position:fixed也可以實現(xiàn)藏在后面的信息流廣告

          position:fixed也可以實現(xiàn)藏在后面的信息流廣告,實現(xiàn)原理就是藏在其他信息流元素的背后,以及頭部或者底部元素(如果有)的底部,關(guān)鍵就是z-index層級控制了。雖然原理簡單,但是實際操作還是有些啰嗦的,通常信息流頁面的HTML結(jié)構(gòu)都比較復雜,此時再z-index屬性各種設(shè)置,很容易造成z-index混亂。

          效果大致如下GIF截屏:

          ?

          HTML和CSS代碼原理示意:

          <div class="list">信息流列表1</div>
          <div class="list">信息流列表2</div>
          <a href="#" class="ad" target="_blank">
           <img src="./ad.jpg">
          </a>
          <div class="list">信息流列表3</div>
          <div class="list">信息流列表4</div>
          
          .list {
           background-color: #fff;
           position: relative;
           z-index: 1;
          }
          .ad {
           display: block;
           height: 576px;
          }
          .ad img {
           position: fixed; top: 0;
           width: 400px;
          }
          

          優(yōu)點和不足

          基于position:fixed實現(xiàn)的優(yōu)點在于:

          1. 我們的廣告內(nèi)容可以支持復雜HTML,而不僅僅是一張圖片;

          2. 所有瀏覽器都兼容,包括iPhone Safari瀏覽器。

          不足在于:

          1. 需要其他元素進行層級配合,相互耦合增加了CSS的復雜度。

          如果實際開發(fā)時候發(fā)現(xiàn)z-index層級控制比較麻煩,可以試試第一個demo中使用的CSS clip剪裁,直接只顯示當前廣告區(qū)域內(nèi)容,不過需要JS配合,不是純CSS實現(xiàn)了,自己權(quán)衡。

          4、結(jié)束語

          采用position:fixed固定定位實現(xiàn)的時候,我們還可以把廣告元素從信息流列表中抽離,直接放在整個容器的后面,然后借助visibility屬性實現(xiàn)點擊穿透,如下示意:

          <a href="#" class="ad">廣告</a>
          <ul>
           <li>信息流列表1</li>
           <li>信息流列表2</li>
           <li></li> <!-- 撐開高度 -->
           <li>信息流列表3</li>
           <li>信息流列表4</li>
          </ul>
          
          .ad {
           position: fixed;
          }
          ul {
           position: relative;
           visibility: hidden;
          }
          li:empty {
           /* 撐開高度,實際開發(fā)請使用類名控制,這里精簡HTML才使用:empty */
           height: 576px;
          }
          li:not(:empty) {
           visibility: visible;
          }
          

          具體就不展開了。

          英格蘭涼了,比利時很強。

          希望本文內(nèi)容可以幫助需要的人。

          然后,如果你有更好地實現(xiàn)方法,歡迎不吝賜教!

          相信大多數(shù)前端開發(fā)者在日常工作中都碰過元素滾動時造成的一些非預期行為。

          這篇文章就和大家來聊聊那些滾動中的非預期行為的出現(xiàn)原理和解決方案。

          Scroll Chaining

          By default, mobile browsers tend to provide a "bounce" effect or even a page refresh when the top or bottom of a page (or other scroll area) is reached. You may also have noticed that when you have a dialog box with scrolling content on top of a page of scrolling content, once the dialog box's scroll boundary is reached, the underlying page will then start to scroll — this is called scroll chaining.

          上述是 MDN 中對于 overscroll-behavior 屬性的描述,上述這段話恰恰描述了為什么會發(fā)生"滾動穿透"現(xiàn)象。

          簡單直譯過來是說默認情況下,當?shù)竭_頁面的頂部或底部(或其他滾動區(qū)域)時,移動瀏覽器傾向于提供“彈跳”效果甚至頁面刷新。您可能還注意到,當滾動內(nèi)容頁面頂部有一個包含滾動內(nèi)容的對話框時,一旦到達對話框的滾動邊界,底層頁面就會開始滾動 - 這稱為滾動鏈接

          現(xiàn)象

          直觀上來說所謂的 Scroll Chaining(滾動鏈接)通常會在兩種情況下被意外觸發(fā):

          • 拖動不可滾動元素時,可滾動背景意外滾動。

          通常情況下,當我們對于某個不可滾動元素進行拖拽時往往會意外觸發(fā)其父元素(背景元素)的滾動。

          常見的業(yè)務(wù)場景比如在 Dialog、Mask 等存在全屏覆蓋的內(nèi)容中,當我們拖動不可滾動的彈出層元素內(nèi)容時,背后的背景元素會被意外滾動。

          比如上方圖片中有兩個元素,一個為紅色邊框存在滾動條的父元素,另一個則為藍色邊框黑色背景不存在滾動條的子元素。

          當我們拖動不可滾動的子元素時,實際會意外造成父元素會跟隨滾動。

          • 將可滾動元素拖動至頂部或者底部時,繼續(xù)拖動觸發(fā)最近可滾動祖先元素的滾動。

          還有另一種常見場景,我們在某個可滾動元素上進行拖動時,當該元素的滾動條已經(jīng)到達頂部/底部。繼續(xù)沿著相同方向進行拖動,此時瀏覽器會尋找當前元素最近的可滾動祖先元素從而意外觸發(fā)祖先元素的滾動。

          同樣,動畫中紅色邊框為擁有滾動區(qū)域的父元素,藍色邊框為父元素中同樣擁有滾動區(qū)域的子元素。我們在子元素區(qū)域內(nèi)進行拖拽時,當子元素滾動到底部(頂部)時,仍然繼續(xù)往下(上)進行拖動。

          原理

          上述兩種情況相信大家也日常業(yè)務(wù)開發(fā)中碰到過不少次。這樣的滾動意外行為用專業(yè)術(shù)語來說,被稱為滾動鏈接(Scroll Chaining)

          那么,它是如何產(chǎn)生的呢?或者換句話說,瀏覽器哪條約束規(guī)定了這樣的行為?

          仔細查閱 w3c 上的 scroll-event 并沒有明確的此項規(guī)定。

          手冊上僅僅明確了,滾動事件的 Target 可以是 Document 以及里邊的 Element ,當 Target 為 Document 時事件會發(fā)生冒泡,而 Target 為 Element 時并不會發(fā)生冒泡,僅僅會 fire an event named scroll at target.

          換句話說,也就是規(guī)范并沒有對于 scroll chaining 這樣的意外行為進行明確規(guī)定如何實現(xiàn)。

          就比如,手冊上規(guī)定了在 Element 以及 Document 中滾動必要的特性以及在代碼層面應(yīng)該如何處理這些特性,但是手冊中并沒有強制規(guī)定某些行為不可以被實現(xiàn),就好比 scroll chaining 的行為。

          不同的瀏覽器廠商私下里都遵從了 scroll chaining 的行為,而手冊中并沒有強制規(guī)定這種行為不應(yīng)該被實現(xiàn),自然這種行為也并不屬于不被允許。

          解決思路

          通過上邊的描述我們已經(jīng)了解了”滾動穿透“的原理:絕大多數(shù)瀏覽器廠商對于滾動,如果目標節(jié)點不能滾動則會嘗試觸發(fā)祖先節(jié)點的滾動,就比如上述第一種現(xiàn)象。而對于目標節(jié)點可以滾動時,當滾動到頂部/底部繼續(xù)進行滾動時,同樣會意外觸發(fā)祖先節(jié)點的滾動。

          在移動端,我們完全可以使用一種通用的解決方案來解決上述造成“滾動穿透”意外行為:

          無論元素是否可以滾動時,每次元素的拖拽事件觸發(fā)時我們只需要進行判斷:

          1. 尋找當前觸發(fā) touchMove 事件 event.target 距離事件綁定元素最近的(event.currentTarget)(包含)可滾動祖先元素。

          之所以尋找 event.target 元素至 event.currentTarget(包含)可滾動祖先元素,是因為我們需要判斷本次滾動是否有效。

          • 如果在上述的范圍內(nèi),祖先元素中不存在可滾動的元素,表示整個區(qū)域?qū)嶋H上是不可滾動的。那么不需要觸發(fā)任何父元素的意外滾動行為,直接進行 event.preventDefault() 阻止默認。
          1. 如果在上述的范圍內(nèi),祖先元素中存在可滾動的元素: 首先我們需要區(qū)域內(nèi)的元素可以正常滾動。 其次,如果該元素已經(jīng)滾動了頂部/底部,此時我們需要調(diào)用 event.preventDefault() 阻止繼續(xù)相同方向滾動時的父元素意外滾動行為。

          通用 Hook 方案

          useTouch 拖動位置

          首先,我們先來看一個有關(guān)于移動端滾動的簡單 Hook:

          tsx復制代碼import { useRef } from 'react'
          
          const MIN_DISTANCE=10
          
          type Direction='' | 'vertical' | 'horizontal'
          
          function getDirection(x: number, y: number) {
            if (x > y && x > MIN_DISTANCE) {
              return 'horizontal'
            }
            if (y > x && y > MIN_DISTANCE) {
              return 'vertical'
            }
            return ''
          }
          
          export function useTouch() {
            const startX=useRef(0)
            const startY=useRef(0)
            const deltaX=useRef(0)
            const deltaY=useRef(0)
            const offsetX=useRef(0)
            const offsetY=useRef(0)
            const direction=useRef<Direction>('')
          
            const isVertical=()=> direction.current==='vertical'
            const isHorizontal=()=> direction.current==='horizontal'
          
            const reset=()=> {
              deltaX.current=0
              deltaY.current=0
              offsetX.current=0
              offsetY.current=0
              direction.current=''
            }
          
            const start=((event: TouchEvent)=> {
              reset()
              startX.current=event.touches[0].clientX
              startY.current=event.touches[0].clientY
            }) as EventListener
          
            const move=((event: TouchEvent)=> {
              const touch=event.touches[0]
              // Fix: Safari back will set clientX to negative number
              deltaX.current=touch.clientX < 0 ? 0 : touch.clientX - startX.current
              deltaY.current=touch.clientY - startY.current
              offsetX.current=Math.abs(deltaX.current)
              offsetY.current=Math.abs(deltaY.current)
          
              if (!direction.current) {
                direction.current=getDirection(offsetX.current, offsetY.current)
              }
            }) as EventListener
          
            return {
              move,
              start,
              reset,
              startX,
              startY,
              deltaX,
              deltaY,
              offsetX,
              offsetY,
              direction,
              isVertical,
              isHorizontal,
            }
          }
          

          上述代碼我相信大家一看便知,useTouch 這個 hook 定義了三個 start、move、reset 方法。

          • start 方法中接受 TouchEvent 對象,同時調(diào)用 reset 清空 delta、offset 以及 direction 值。同時記錄事件對象發(fā)生時距離視口的距離 clientX、clientY 值作為初始值。
          • move 方法中同樣接受 TouchEvent 對象作為入?yún)ⅲ鶕?jù) TouchEvent 上的位置屬性分別計算:
            • deltaX、deltaY 兩個值,表示移動時相較初始值的距離,不同方向可為負數(shù)。
            • offsetX、offsetY 分別表示移動時相較初始值 X 方向和 Y 方向的絕對距離。
            • direction 則是通過 offsetX、offsetY 相較計算出移動的方向。
          • reset 方法則是對于上述提到的變量進行一次統(tǒng)一的清空重制。

          通過 useTouch 這個 hook 我們可以在移動端配合 touchstart、onTouchMove 輕松的計算出手指拖動時的方向和距離。

          getScrollParent 尋找區(qū)域內(nèi)可滾動祖先元素

          tsx復制代碼// canUseDom 方法是對于是否可以使用 Dom 情況下的判斷,主要為了甄別( Server Side Render )
          import { canUseDom } from './can-use-dom'
          
          type ScrollElement=HTMLElement | Window
          
          const defaultRoot=canUseDom ? window : undefined
          
          const overflowStylePatterns=['scroll', 'auto', 'overlay']
          
          function isElement(node: Element) {
            const ELEMENT_NODE_TYPE=1
            return node.nodeType===ELEMENT_NODE_TYPE
          }
          export function getScrollParent(
            el: Element,
            root: ScrollElement | null | undefined=defaultRoot
          ): Window | Element | null | undefined {
            let node=el
          
            while (node && node !==root && isElement(node)) {
              if (node===document.body) {
                return root
              }
              const { overflowY }=window.getComputedStyle(node)
              if (
                overflowStylePatterns.includes(overflowY) &&
                node.scrollHeight > node.clientHeight
              ) {
                return node
              }
              node=node.parentNode as Element
            }
            return root
          }
          
          

          getScrollParent 方法本質(zhì)上從 el(event.target) 到 root(event.currentTarget) 范圍內(nèi)尋找最近的滾動祖先元素。

          代碼同樣也并不是特別難理解,在 while 循環(huán)中從傳入的第一個參數(shù) el 一層一層往上尋找。要么尋找到可滾動的元素,要么一直尋找到 node===root 直接返回 root。

          比如這樣的場景:

          tsx復制代碼import { useEffect, useRef } from 'react';
          import './App.css';
          import { getScrollParent } from './hooks/getScrollParent';
          
          function App() {
            const ref=useRef<HTMLDivElement>(null);
          
            const onTouchMove=(event: TouchEvent)=> {
              const el=getScrollParent(event.target as Element, ref.current);
              console.log(el, 'el'); // child-1
            };
          
            useEffect(()=> {
              document.addEventListener('touchmove', onTouchMove);
            }, []);
          
            return (
              <>
                <div ref={ref} className="parent">
                  <div
                    className="child-1"
                    style={{
                      height: '300px',
                      overflowY: 'auto',
                    }}
                  >
                    <div
                      style={{
                        height: '600px',
                      }}
                    >
                      This is child-2
                    </div>
                  </div>
                </div>
              </>
            );
          }
          
          export default App;
          

          我們在頁面中拖拽滾動 This is child-2 內(nèi)容時,此時控制臺會打印 getScrollParent 從 event.target (也就是 This is child-2 元素開始)尋找到的類名為 .parent 區(qū)域內(nèi)的最近滾動元素 .child-1 元素。

          useScrollLock 通用解決方案

          上邊我們了解了一個基礎(chǔ)的 useTouch 關(guān)于拖拽位置計算的 hook 以及 getScrollParent 獲取區(qū)域內(nèi)最近的可滾動祖先元素的方法,接下來我們就來看看在移動端中關(guān)于阻止 scroll chaining 意外滾動行為的通用 hook。

          這里,我直接貼一段 ant-design-mobile 中的實現(xiàn)代碼,(實際這是 ant-design-mobile 中從 vant 中搬運的代碼):

          tsx復制代碼import { useTouch } from './use-touch'
          import { useEffect, RefObject } from 'react'
          import { getScrollParent } from './get-scroll-parent'
          import { supportsPassive } from './supports-passive'
          
          let totalLockCount=0
          
          const BODY_LOCK_CLASS='adm-overflow-hidden'
          
          function getScrollableElement(el: HTMLElement | null) {
            let current=el?.parentElement
          
            while (current) {
              if (current.clientHeight < current.scrollHeight) {
                return current
              }
          
              current=current.parentElement
            }
          
            return null
          }
          
          export function useLockScroll(
            rootRef: RefObject<HTMLElement>,
            shouldLock: boolean | 'strict'
          ) {
            const touch=useTouch()
          
            /**
             * 當手指拖動時
             * @param event 
             * @returns 
             */
            const onTouchMove=(event: TouchEvent)=> {
              touch.move(event)
          
              // 獲取拖動方向
              // 如果 deltaY 大于0,拖動的當前Y軸位置大于起始位置即從下往上拖動將 direction 變?yōu)?'10',否則則會 `01`
              const direction=touch.deltaY.current > 0 ? '10' : '01'
          
              // 我們在上邊提到過,找到范圍內(nèi)可滾動的元素
              const el=getScrollParent(
                event.target as Element,
                rootRef.current
              ) as HTMLElement
              if (!el) return
          
              // This has perf cost but we have to compatible with iOS 12
              if (shouldLock==='strict') {
                const scrollableParent=getScrollableElement(event.target as HTMLElement)
                if (
                  scrollableParent===document.body ||
                  scrollableParent===document.documentElement
                ) {
                  event.preventDefault()
                  return
                }
              }
          
              // 獲取可滾動元素的位置屬性
              const { scrollHeight, clientHeight, offsetHeight, scrollTop }=el
          
              // 定義初始 status 
              let status='11'
          
              if (scrollTop===0) {
                // 滾動條在頂部,表示還未滾動
                // 滾動條在頂部時,需要判斷是當前元素不可以滾動還是可以滾動但是未進行任何滾動
          
                // 當 offsetHeight >=scrollHeight 表示當前元素不可滾動,此時將 status 變?yōu)?00,
                // 否則表示當前元素可滾動但滾動條在頂部,將status變?yōu)?01
                status=offsetHeight >=scrollHeight ? '00' : '01'
              } else if (Math.abs(scrollHeight - clientHeight - scrollTop) < 1) {
                // 滾動條已經(jīng)到達底部(表示已經(jīng)滾動到底),將 status 變?yōu)?'10'
                status='10'
              }
          
              // 1. 完成上述的判斷后,如果 status===11 表示當前元素可滾動并且滾動條不在頂部也不在底部(即在中間),表示 touchMove 事件不應(yīng)該阻止元素滾動(當前滾動為正常現(xiàn)象)
          
              // 2. 同時 touch.isVertical() 明確確保是垂直方向的拖動
          
              // 3. parseInt(status, 2),當 status 不為 11 時,分為以下三種情況分別代表:
              
                // 3.1 status 00 表示區(qū)域內(nèi)未尋找到任何可滾動元素
                // 3.2 status 01 表示尋找到可滾動元素,當前元素為滾動條在頂部
                // 3.3 status 10 表示尋找到可滾動元素,當前元素滾動條在底部 
              // 自然 parseInt(status, 2) & parseInt(direction, 2) 這里使用了二進制的方式,
          
                // 3.4 當 status 為 00 時, 0 & 任何數(shù)都是 0.自然 !(parseInt(status, 2) & parseInt(direction, 2)) 會變?yōu)?true (對應(yīng) 3.1 情況),需要阻止意外的滾動行為。
          
                // 3.5 當 status 為 01 時(對應(yīng) 3.2 滾動條在頂部),此時當用戶從下往上拖動時,需要阻止意外的滾動行為發(fā)生。否則,則不需要阻止正常滾動。 自然 status==='01' ,direction===10(從下往上拖動),!(parseInt(status, 2) & parseInt(direction, 2)) 為 true 需要進行阻止默認滾動行為。(進制上 1 & 1 為 1 ,1 & 2 為 0)
          
                // 3.6 根據(jù) 3.5 的情況,當 status 為 10 (對應(yīng) 3.3)滾動到達底部,自然對于從上往下拖動時 direction 為 01 時也應(yīng)該阻止,所以 (2&1=0) 自然 !(parseInt(status, 2) & parseInt(direction, 2)) 為 true,同樣會進入 if 語句阻止意外滾動。
                
              if (
                status !=='11' &&
                touch.isVertical() &&
                !(parseInt(status, 2) & parseInt(direction, 2))
              ) {
                if (event.cancelable) {
                  event.preventDefault()
                }
              }
            }
          
            /**
             * 鎖定方法
             * 1. 添加 touchstart 和 touchmove 事件監(jiān)聽
             * 2. 根據(jù) totalLockCount,當 hook 運行時為 body 添加 overflow hidden 的樣式類名稱
             */ 
            const lock=()=> {
              document.addEventListener('touchstart', touch.start)
              document.addEventListener(
                'touchmove',
                onTouchMove,
                supportsPassive ? { passive: false } : false
              )
          
              if (!totalLockCount) {
                document.body.classList.add(BODY_LOCK_CLASS)
              }
          
              totalLockCount++
            }
          
            /**
             * 組件銷毀時移除事件監(jiān)聽方法,以及清空 body 上的 overflow hidden 的類名
             */
            const unlock=()=> {
              if (totalLockCount) {
                document.removeEventListener('touchstart', touch.start)
                document.removeEventListener('touchmove', onTouchMove)
          
                totalLockCount--
          
                if (!totalLockCount) {
                  document.body.classList.remove(BODY_LOCK_CLASS)
                }
              }
            }
          
            useEffect(()=> {
              // 如果傳入 shouldLock 表示需要防止意外滾動
              if (shouldLock) {
                lock()
                return ()=> {
                  unlock()
                }
              }
            }, [shouldLock])
          }
          

          我在上述代碼片段中每一行都進行了詳細的注釋,認真看這段代碼相信大家不難看懂。上述的代碼仍然是按照我們在文章開頭講述的解決思路來解決移動端滾動鏈接的意外行為。

          關(guān)于上邊代碼中有幾個小 Tips ,這里和大家稍微贅述下:

          1. 關(guān)于 shouldLock==='strict' 這種情況 antd 源碼中標明是對于 IOS12 清空的兼容,如果這段代碼混淆了你的思路完全可以忽略它,因為它并不是我們主要想贅述的內(nèi)容。
          2. addEventListener 第三個參數(shù) { passive: false } ,在 safari 以外的瀏覽器默認為 true ,它會導致部分事件函數(shù)中 preventDefault() 無效,所謂的 passive 在 chrome51 版本后出現(xiàn)的,本質(zhì)上是為了通過被動偵聽器提高滾動性能。詳情可以查看 MDN 的解釋,這里我就不在贅述了。
          3. BODY_LOCK_CLASS 的實際樣式其實就是 overflow:hidden,之所以通過 totalLockCount 計數(shù)的方式添加,沒什么特別的。想象一下,如果你的頁面中每個 Modal 彈窗都使用了 useLockScroll 這個 hook ,那么當頁面中開啟兩個彈窗,當關(guān)閉一個時另一個還存在時總不能移除了 BODY_LOCK_CLASS 吧。
          4. 為 body 添加 overflow:hidden 其實在移動端并沒什么太大的實際作用,我們 touchmove 事件中的處理邏輯對于阻止意外滾動行為的發(fā)生已經(jīng)完全足夠了。這點最初我也不太明白為什么這么做,所以我也去 vant 中進行了請教,詳見 vant Discussions。
          5. 實際上源碼中并不是使用 Math.abs(scrollHeight - clientHeight - scrollTop) < 1 判斷滾動條是否到達底部,而是使用 scrollTop + offsetHeight >=scrollHeight 顯然這是不準確的可能會導致 Bug(因為 scrollTop 是一個非四舍五入的數(shù)字(可以為小數(shù)),而 scrollHeight 和 clientHeight 是四舍五入的數(shù)字)所以極端場景下會導致不準確,我就遇到過,有興趣了解的朋友詳見我對于 antd-mobile 的 PR。

          結(jié)語

          文章到這里就和大家說聲再見了,剛好前段時間在公司內(nèi)編寫移動端組件時遇到過這個問題所以拿出來和大家分享。

          當然,如果大家對于文章中的內(nèi)容有什么疑惑或者有更好的解決方案。你可以在評論區(qū)留下你的看法,我們可以一起進行討論,謝謝大家。


          作者:19組清風
          鏈接:https://juejin.cn/post/7261493331188449341


          主站蜘蛛池模板: 成人无码AV一区二区| 中文字幕精品一区二区精品| 亚洲福利一区二区三区| 一区二区三区免费视频网站| 国产在线一区二区三区在线| 风间由美在线亚洲一区| 一本一道波多野结衣一区| 在线中文字幕一区| 亚洲啪啪综合AV一区| 中文人妻av高清一区二区| 日韩精品无码人妻一区二区三区| 亲子乱av一区区三区40岁| 成人精品一区久久久久| 国产视频福利一区| 国产aⅴ一区二区| 亚洲AV无码一区二区三区在线观看 | 亚洲欧美日韩一区二区三区| 丰满爆乳一区二区三区| 国产精品区一区二区三在线播放 | 亚洲国产成人精品无码一区二区| 国产MD视频一区二区三区| 国产在线步兵一区二区三区| 亚洲熟女乱综合一区二区| 久久精品国产免费一区| 国语对白一区二区三区| 久久一区二区三区免费| 国产另类TS人妖一区二区| 无码精品人妻一区二区三区免费| 三上悠亚亚洲一区高清| 日韩一区二区三区精品| 无码一区二区三区爆白浆| 国产一区在线视频| 人妻体体内射精一区二区| 成人免费视频一区二区三区 | 国产伦精品一区二区三区免费迷 | 高清一区二区三区视频| 大香伊人久久精品一区二区| 日韩高清国产一区在线| 亚洲熟妇无码一区二区三区导航| 天天综合色一区二区三区| 久久精品国产一区二区三区不卡 |