解決頁面滾動時吸頂操做不能及時響應bug

  • position: sticky;css

  • fixed 吸頂html

  • 頁面滾動結束後頁面才渲染jquery

需求

常常會有這樣的需求,當頁面滾動到某一個位置fixedTopValue時,須要某個元素fixedElement固定在屏幕頂部。基本方法是獲取頁面的scrollTop值作判斷:
若是 scrollTop > fixedTopValue; 則添加position:fixed;top: 0;不然刪除position:fixed;屬性。web

當在pc瀏覽器操做的時候正常。真機測試時總會出現千奇百怪的現象。好比:
一、 當頁面往下滾時,fixedElement須要等頁面滾動中止以後纔會出現。
二、往上滾動時出現到固定的位置時不恢復原樣,而是到達頂部、等頁面中止滾動以後纔會唰的一下恢復原樣
三、滾動到頂部以後,會出現兩個同樣的fixedElement, 過一會才恢復正常。
這樣的用戶體驗真的不好,因此迫切須要解決這個問題。chrome

滾動bug.png

解決方法主要涉及一下三個方面
一、使用新的定位屬性 position: sticky; (若是支持)
二、若是不支持1,使用window.requestAnimationFrame方法確保改變定位屬性在固定時間內執行一次
三、 給fixedElement開啓硬件加速編程

基本邏輯以下圖:瀏覽器

頁面滾動元素fixed置頂邏輯設計 (2).png

測試頁面二維碼:微信

Paste_Image.png

position:sticky是個什麼鬼?

對與css的position屬性咱們只知道有static、relative、absolute、fixed這四個值,何時又多出了sticky這個值。看下MDN文檔解釋app

Sticky positioning
Sticky positioning is a hybrid of relative and fixed positioning. The element is treated as relative positioned until it crosses a specified threshold, at which point it is treated as fixed positioned.dom

大概意思是:sticky定位時relative定位與fixed定位的混合體。對於設置了sticky定位的元素,在它的頂部到達一個指定的界限以前會被看成relative定位,超過這個界限字後則被看成fixed定位。這個界限就是 該元素頂部距離窗口頂部的距離等於該元素設置的top值
好比如下demo:

<div>
  <div class="top">60像素分割線</div>
  <div class="sticky">當個人頂部距離窗口頂部爲10px(top值)時,我就會像fixed同樣fixed在距離窗口10px(top值處)</div>
  <div class="content">sticky的co時代發送分ntent</div>
</div>
.top{height:60px;background:#f20;width:100%;color: #fff;font-size:16px;text-align:center;line-height:60px;}
.sticky{position:sticky;position:-webkit-sticky;top:10px;height:40px;background:#dd5;color:#fff;line-height:20px;text-align:center;}
.sticky-t10{top:0px;}
.content{height:1000px;width:100%;background:#f8f8f8;text-align:center;padding-top:40px;color:#333;}

效果圖:
sticky測試頁面綜合圖.png

當頁面滾動到距離黃色區塊頂部10px時,黃色區塊就會fixed在窗口頂部10px處,頁面再往下滾動距離也不會變。當頁面網上滾動時,頁面頂部距離黃色區塊頂部大於10px時,黃色區塊又會恢復原樣固定在原來的位置。

position:sticky這個屬性並不會出現當頁面滾動中止以後纔會出現的bug,由於它自己就是屬於正常流。並不會像fixed 與static相互切換時引發重排於重繪,而移動端瀏覽器滾動時是禁止重排跟重繪的,因此纔會致使以上出現的問題。下圖是對於position:sticky的支持狀況:

position:sticky瀏覽器支持狀況

發現支持的瀏覽器通常般,可是通過測試像微信、safari、uc等瀏覽器是支持的,雖然chrome不支持,可是在chrome使用優化後的fixed定位也能夠解決這個問題,基本能知足主流的瀏覽器就夠了,其餘的見鬼去吧。

滾動時減小性能損耗,強制觸發瀏覽器的同步佈局

若是瀏覽器不支持position:sticky,那麼就使用js動態的在節點在fixed定位於static定位中切換,可是須要對切換過程作一些優化。
一、使用函數節流防抖減小dom操做頻繁粗發,可是保證在規定時間內必須執行一次。
二、使用window.requestAnimationFrame 方法在下一幀前觸發瀏覽器的強制同步佈局,是對dom的操做能及時渲染到頁面上。
三、減小對dom的讀寫操做,或者把dom操做把讀、寫操做分開,能夠減小渲染次數。

給須要定位的元素開啓硬件加速

因爲移動設備的硬件限制,致使移動端的瀏覽器的渲染能比較差。此時對須要定位的元素開啓硬件加速,會把須要渲染的元素放到特定的複合層『Composited Layer』中,當該元素改變時能夠較少重繪或重排的範圍。給元素添加 transform: translateZ(0);屬性就行。

參考:

硬件加速:
http://div.io/topic/1348
http://www.cnblogs.com/shyton...
提高頁面性能:
https://developer.mozilla.org...
http://www.ruanyifeng.com/blo...
http://www.jianshu.com/p/a32b...

具體實現請參考如下jquery版本的代碼:

//jquery
(function() {
    function Sticky(){
        this.init.apply(this, arguments);
    }

    /**
     * 滾動fixed組件初始化
     * @param {object}         setting                allocate傳進來的參數
     * @param {object}         setting.stickyNode     須要設置position:sticky的節點,一般是最外層
     * @param {object}         setting.fixedNode      當滾動必定距離時須要fixed在頂部的節點
     * @param {int}            setting.top            fixed以後距離頂部的top值
     * @param {int}            setting.zIndex         fixed以後的z-index值
     * @param {string}         setting.fixedClazz     fixed時給fixedNode添加的類
     * @param {function}     setting.runInScrollFn  滾動期間額外執行的函數
     * @return {void}  
     */
    Sticky.setting = {
        stickyNode: null,
        fixedNode: null,
        top: 0,
        zIndex: 100,
        fixedClazz: '',
        runInScrollFn: null
    };
    var sPro = Sticky.prototype;
    var g = window;

    /**
     * 初始化
     * @param  {object} options 設置
     * @return {void}         
     */
    sPro.init = function(options){
        this.setting = $.extend({}, Sticky.setting, options, true);
        if (options.fixedNode) {
            this.fixedNode = options.fixedNode[0] || options.fixedNode;
            this.stickyNode = options.stickyNode[0] || options.stickyNode;
            this.cssStickySupport = this.checkStickySupport();
            this.stickyNodeHeight = this.stickyNode.clientHeight;
            this.fixedClazz = options.fixedClazz;
            this.top = parseInt(options.top, 10) || 0;
            this.zIndex = parseInt(options.zIndex) || 1;
            this.setStickyCss();
            this.isfixed = false;
            // 把改變定位的操做添加到節流函數與window.requestAnimationFrame方法中,確保必定事件內必須執行一次
            this.onscrollCb = this.throttle(function() {
                this.nextFrame(this.sticky.bind(this));
            }.bind(this), 50, 100);
            this.initCss = this.getInitCss();
            this.fixedCss = this.getFixedCss();
            this.addEvent();
        }
    };

    /**
     * 獲取原始css樣式
     * @return {string} 定位的樣式
     */
    sPro.getInitCss = function() {
        if (!!this.fixedNode) {
            return "position:" + this.fixedNode.style.position + ";top:" + this.fixedNode.style.top + "px;z-index:" + this.fixedNode.style.zIndex + ";";
        }
        return "";
    };

    /**
     * 生成fixed時的css樣式
     * @return {void}
     */
    sPro.getFixedCss = function() {
        return "position:fixed;top:" + this.top + "px;z-index:" + this.zIndex + ";";
    };

    /**
     * 給fixedNode設置fixed定位樣式
     * @param {string} style fixed定位的樣式字符串
     */
    sPro.setFixedCss = function(style) {
        if(!this.cssStickySupport){
            if (!!this.fixedNode){
                this.fixedNode.style.cssText = style;
            }
        }
    };

    /**
     * 檢查瀏覽器是否支持positon: sticky定位
     * @return {boolean} true 支持 false 不支持
     */
    sPro.checkStickySupport = function() {
        var div= null;
        if(g.CSS && g.CSS.supports){
            return g.CSS.supports("(position: sticky) or (position: -webkit-sticky)");
        }
        div = document.createElement("div");
        div.style.position = "sticky";
        if("sticky" === div.style.position){
            return true;
        }
        div.style.position = "-webkit-sticky";
        if("-webkit-sticky" === div.style.position){
            return true;
        }
        div = null;
        return false;
    };

    /**
     * 給sticyNode設置position: sticky定位
     */
    sPro.setStickyCss = function() {
        if(this.cssStickySupport){
            this.stickyNode.style.cssText = "position:-webkit-sticky;position:sticky;top:" + this.top + "px;z-index:" + this.zIndex + ";";
        }
    };

    /**
     * 監聽window的滾動事件
     */
    sPro.addEvent = function() {
        $(g).on('scroll', this.onscrollCb.bind(this));
    };

    /**
     * 讓函數在規定時間內必須執行一次
     * @param {Function} fn     定時執行的函數
     * @param {int}      delay  延遲多少毫秒執行
     * @param {[type]}   mustRunDelay 多少毫秒內必須執行一次
     * @return {[type]}      [description]
     */
    sPro.throttle = function(fn, delay, mustRunDelay){
        var timer = null;
        var lastTime;
        return function(){
            var now = +new Date();
            var args = arguments;
            g.clearTimeout(timer);
            if(!lastTime){
                lastTime = now;
            }
            if(now - lastTime > mustRunDelay){
                fn.apply(this, args);
                lastTime = now;
            }else{
                g.setTimeout(function(){
                    fn.apply(this, args);
                }.bind(this), delay);
            }
        }.bind(this);
    };

    /**
     * window.requestAnimationFrame的兼容性寫法,保證在100/6ms執行一次
     * @param  {Function} fn 100/16ms須要執行的函數
     * @return {void}      
     */
    sPro.nextFrame = (function(fn){
        var prefix = ["ms", "moz", "webkit", "o"];
        var handle = {};
        handle.requestAnimationFrame = window.requestAnimationFrame;
        for(var i = 0; i < prefix.length && !handle.requestAnimationFrame; ++i){
            handle.requestAnimationFrame = window[prefix[i] + "RequestAnimationFrame"];
        }
        if(!handle.requestAnimationFrame){
            handle.requestAnimationFrame = function(fn) {
                var raf = window.setTimeout(function() {
                    fn();
                }, 16);
                return raf;
            };
        }
        return function(fn) {
            handle.requestAnimationFrame.apply(g, arguments);
        }
    })();

    /**
     * 判斷stickyNode的當前位置設置fixed|static|sticky定位
     * @return {void}
     */
    sPro.sticky = function() {
        this.setting.runInScrollFn && this.setting.runInScrollFn();
        var stickyNodeBox = this.stickyNode.getBoundingClientRect();
        if(stickyNodeBox.top <= this.top && !this.isfixed){
            this.setFixedCss(this.fixedCss);
            this.fixedClazz && $(this.fixedNode).addClass(this.fixedClazz);
            this.isfixed = true;
            $(this).trigger('onsticky', true);
        } else if(stickyNodeBox.top > this.top && this.isfixed) {
            this.setFixedCss(this.initCss.replace(/position:[^;]*/, "position:static"));
            g.setTimeout(function() {
                this.setFixedCss(this.initCss)
            }.bind(this), 30);
            this.fixedClazz && $(this.fixedNode).removeClass(this.fixedClazz);
            this.isfixed = false;
            $(this).trigger('onsticky', true);
        }
    };

    $.initSticky = function(options){
        return new Sticky(options);
    };
})();

html 結構

<div class="m-nav">
            <div class="nav-fixed fixed" id="j-nav" style="position: fixed; top: 0px; z-index: 100;">
                <ul class="f-cb">
                    <li class="active" anchor-id="j-understand">瞭解兒童編程</li>
                    <li anchor-id="j-join">參與公益直播課</li>
                    <li anchor-id="j-upload">上傳編程做品</li>
                </ul>
            </div>
        </div>

css 結構

.g-page-box .m-nav {

 height: 1.33333rem;

}

.g-page-box .m-nav .nav-fixed {

 height: .86667rem;
 padding: .22667rem .50667rem;
 background-color: #1aadbb;
 position: relative;
 transform: translate3d(0, 0, 0);
 -webkit-transform: translate3d(0, 0, 0);
 transition: height 4s;

}

.fixed{
position: fixed;
 top: 0px;
 z-index: 100;
}
相關文章
相關標籤/搜索