iscroll源码实现原理(一)

iscroll是移动端h5开发中为了实现原生滚动体验的一个库

他的主要特点是

  • 接近原生的滚动体验(说明性能不差)
  • 兼容性非常好(相信没哪个库敢说在H5上完美)
  • 使用方便(只需要dom结构按照一定的规则)
  • 功能强大(横向,纵向滚动,滚动条自定义,PC浏览器上的鼠标滚动都支持)

即便如此,当使用这个库的时候依然会踩到坑,比如配置没正确等,因此很有必要研读一下源码了解其使用原理,以便在遇到问题的时候能够快速解决,甚至构造自己的轻量级的iscroll组件

iscroll的实现核心

  • 对touch事件的处理,包括开始,移动和结束
  • css3或者降级(requestAnimationFrame,setTimeout等)来实现滚动动画
  • 动画的物理效果,如加速,缓动,动量势能的计算等

iscroll源码

主要说明的几个重要文件iscroll

源码

default文件夹下的文件
default文件夹

utils.js

utils的接口都为一些辅助方法,首先定义了requestAnimationFrame的兼容性方法

var rAF = window.requestAnimationFrame    ||  
    window.webkitRequestAnimationFrame  ||
    window.mozRequestAnimationFrame     ||
    window.oRequestAnimationFrame       ||
    window.msRequestAnimationFrame      ||
    function (callback) { window.setTimeout(callback, 1000 / 60); };

定义css的transform前缀判断来实现兼容

var _elementStyle = document.createElement('div').style;  
    var _vendor = (function () {
        var vendors = ['t', 'webkitT', 'MozT', 'msT', 'OT'],
            transform,
            i = 0,
            l = vendors.length;

        for ( ; i < l; i++ ) {
            transform = vendors[i] + 'ransform';
            if ( transform in _elementStyle ) return vendors[i].substr(0, vendors[i].length-1);
        }

        return false;
    })();

    function _prefixStyle (style) {
        if ( _vendor === false ) return false;
        if ( _vendor === '' ) return style;
        return _vendor + style.charAt(0).toUpperCase() + style.substr(1);
    }

获取当前时间,注意后面的me都为utils对象本身

me.getTime = Date.now || function getTime () { return new Date().getTime(); };  

潜复制对象属性

me.extend = function (target, obj) {  
        for ( var i in obj ) {
            target[i] = obj[i];
        }
    };

事件的注册与取消

me.addEvent = function (el, type, fn, capture) {  
        el.addEventListener(type, fn, !!capture);
};

me.removeEvent = function (el, type, fn, capture) {  
    el.removeEventListener(type, fn, !!capture);
};

定义势能函数,这里会根据输入参数设置滚动的加速度

me.momentum = function (current, start, time, lowerMargin, wrapperSize, deceleration) {  
        var distance = current - start,
            speed = Math.abs(distance) / time,
            destination,
            duration;

        deceleration = deceleration === undefined ? 0.0006 : deceleration;

        destination = current + ( speed * speed ) / ( 2 * deceleration ) * ( distance < 0 ? -1 : 1 );
        duration = speed / deceleration;

        if ( destination < lowerMargin ) {
            destination = wrapperSize ? lowerMargin - ( wrapperSize / 2.5 * ( speed / 8 ) ) : lowerMargin;
            distance = Math.abs(destination - current);
            duration = distance / speed;
        } else if ( destination > 0 ) {
            destination = wrapperSize ? wrapperSize / 2.5 * ( speed / 8 ) : 0;
            distance = Math.abs(current) + destination;
            duration = distance / speed;
        }

        return {
            destination: Math.round(destination),
            duration: duration
        };
    };

给utils定义一些bool值属性,用来检测浏览器是否支持某些特性

me.extend(me, {  
        hasTransform: _transform !== false,//
        hasPerspective: _prefixStyle('perspective') in _elementStyle,
        hasTouch: 'ontouchstart' in window,
        hasPointer: window.PointerEvent || window.MSPointerEvent, // IE10 is prefixed
        hasTransition: _prefixStyle('transition') in _elementStyle
    });

筛选某些安卓版本,appVersion中有android字符没有chrome字符应该是比较旧的版本,应该是会填坑用的

me.isBadAndroid = /Android /.test(window.navigator.appVersion) && !(/Chrome\/\d/.test(window.navigator.appVersion));  

设置transform的css3过渡属性

me.extend(me.style = {}, {  
        transform: _transform,
        transitionTimingFunction: _prefixStyle('transitionTimingFunction'),
        transitionDuration: _prefixStyle('transitionDuration'),
        transitionDelay: _prefixStyle('transitionDelay'),
        transformOrigin: _prefixStyle('transformOrigin')
    });

样式类的操作

me.hasClass = function (e, c) {  
        var re = new RegExp("(^|\\s)" + c + "(\\s|$)");
        return re.test(e.className);
    };

    me.addClass = function (e, c) {
        if ( me.hasClass(e, c) ) {
            return;
        }

        var newclass = e.className.split(' ');
        newclass.push(c);
        e.className = newclass.join(' ');
    };

    me.removeClass = function (e, c) {
        if ( !me.hasClass(e, c) ) {
            return;
        }

        var re = new RegExp("(^|\\s)" + c + "(\\s|$)", 'g');
        e.className = e.className.replace(re, ' ');
    };

获取某个元素相对于根元素的偏移offset,后面会用到

me.offset = function (el) {  
        var left = -el.offsetLeft,
            top = -el.offsetTop;

        // jshint -W084
        while (el = el.offsetParent) {
            left -= el.offsetLeft;
            top -= el.offsetTop;
        }
        // jshint +W084

        return {
            left: left,
            top: top
        };
    };

iscroll默认会阻止默认行为,但一些元素如果阻止默认行为会有很大影响,如输入框input元素等,因此把这些设置为例外,后面会用到

me.preventDefaultException = function (el, exceptions) {  
        for ( var i in exceptions ) {
            if ( exceptions[i].test(el[i]) ) {
                return true;
            }
        }

        return false;
    };

定义事件的类似枚举类型

me.extend(me.eventType = {}, {  
        touchstart: 1,
        touchmove: 1,
        touchend: 1,

        mousedown: 2,
        mousemove: 2,
        mouseup: 2,

        pointerdown: 3,
        pointermove: 3,
        pointerup: 3,

        MSPointerDown: 3,
        MSPointerMove: 3,
        MSPointerUp: 3
    });

定义动画的擦除ease函数

me.extend(me.ease = {}, {  
        quadratic: {
            style: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)',
            fn: function (k) {
                return k * ( 2 - k );
            }
        },
        circular: {
            style: 'cubic-bezier(0.1, 0.57, 0.1, 1)',   // Not properly "circular" but this looks better, it should be (0.075, 0.82, 0.165, 1)
            fn: function (k) {
                return Math.sqrt( 1 - ( --k * k ) );
            }
        },
        back: {
            style: 'cubic-bezier(0.175, 0.885, 0.32, 1.275)',
            fn: function (k) {
                var b = 4;
                return ( k = k - 1 ) * k * ( ( b + 1 ) * k + b ) + 1;
            }
        },
        bounce: {
            style: '',
            fn: function (k) {
                if ( ( k /= 1 ) < ( 1 / 2.75 ) ) {
                    return 7.5625 * k * k;
                } else if ( k < ( 2 / 2.75 ) ) {
                    return 7.5625 * ( k -= ( 1.5 / 2.75 ) ) * k + 0.75;
                } else if ( k < ( 2.5 / 2.75 ) ) {
                    return 7.5625 * ( k -= ( 2.25 / 2.75 ) ) * k + 0.9375;
                } else {
                    return 7.5625 * ( k -= ( 2.625 / 2.75 ) ) * k + 0.984375;
                }
            }
        },
        elastic: {
            style: '',
            fn: function (k) {
                var f = 0.22,
                    e = 0.4;

                if ( k === 0 ) { return 0; }
                if ( k == 1 ) { return 1; }

                return ( e * Math.pow( 2, - 10 * k ) * Math.sin( ( k - f / 4 ) * ( 2 * Math.PI ) / f ) + 1 );
            }
        }
    });

自定义tap事件,用户可以用tap事件来模拟click,同时消除300ms延迟,后面细说

me.tap = function (e, eventName) {  
        var ev = document.createEvent('Event');
        ev.initEvent(eventName, true, true);
        ev.pageX = e.pageX;
        ev.pageY = e.pageY;
        e.target.dispatchEvent(ev);
    };

重写原生的click事件,除了一些特殊标签,后面会用到

me.click = function (e) {  
        var target = e.target,
            ev;

        if ( !(/(SELECT|INPUT|TEXTAREA)/i).test(target.tagName) ) {
            ev = document.createEvent('MouseEvents');
            ev.initMouseEvent('click', true, true, e.view, 1,
                target.screenX, target.screenY, target.clientX, target.clientY,
                e.ctrlKey, e.altKey, e.shiftKey, e.metaKey,
                0, null);

            ev._constructed = true;
            target.dispatchEvent(ev);
        }
    };
作者:shaynegui
喜欢打德州,玩dota,听电音,web前端脑残粉
我的专栏 GitHub