[BetterScroll 源码]事件派发流程

BScrollConstructor类是入口,其中创建了Scroller实例,管理滚动动作。

Scroller中创建以下类的实例:

  1. Behavior 管理手势动作,计算滚动方向,滚动距离,边界计算
  2. Translater 修改滚动元素的css transform属性的值
  3. Transition 用transition实现滑动动画效果
  4. ActionsHandler 监听用户触发的事件(点触,滑动)
  5. ScrollerActions

下面看一下scroll事件的派发流程(移动端)。

查看BetterScroll的文档,scroll事件通过以下代码监听:

// bs 是BScrollConstructor的实例
bs.on('scroll', (position) => {
    console.log(position.x, position.y)
})

BScrollConstructor类继承自EventEmitter,EventEmitter是典型的订阅发布模式,通过on监听,通过trigger触发。

看一下EventEmitter里的关键代码:

var EventEmitter = /** @class */ (function () {
    // 参数names是一个数组,数组中的项是字符串
    function EventEmitter(names) {
        this.events = {};
        this.eventTypes = {};
        // 把names中的项添加到eventTypes中
        this.registerType(names);
    }
    // 注册事件type
    EventEmitter.prototype.on = function (type, fn, context) {
        if (context === void 0) {
            context = this;
        }
        this.hasType(type);
        // 每一种type创建一个数组,保存事件处理函数和作用域
        if (!this.events[type]) {
            this.events[type] = [];
        } 
        this.events[type].push([fn, context]);
        return this;
    };

    // 触发事件,执行事件处理函数
    EventEmitter.prototype.trigger = function (type) {
        var args = [];
        for (var _i = 1; _i < arguments.length; _i++) {
            args[_i - 1] = arguments[_i];
        }
        this.hasType(type);
        var events = this.events[type];
        if (!events) {
            return;
        }
        var len = events.length;
        var eventsCopy = __spreadArrays(events);
        var ret;
        for (var i = 0; i < len; i++) {
            var event_1 = eventsCopy[i];
            var fn = event_1[0],
                context = event_1[1];
            if (fn) {
                ret = fn.apply(context, args);
                if (ret === true) {
                    return ret;
                }
            }
        }
    };
    EventEmitter.prototype.registerType = function (names) {
        var _this = this;
        names.forEach(function (type) {
            _this.eventTypes[type] = type;
        });
    };

    // 检查是type是否在eventTypes上存在,如果不存在打印错误
    EventEmitter.prototype.hasType = function (type) {
        var types = this.eventTypes;
        var isType = types[type] === type;
        if (!isType) {
            warn("EventEmitter has used unknown event type: \"" + type +
                "\", should be oneof [" +
                ("" + Object.keys(types).map(function (_) {
                    return JSON.stringify(_);
                })) +
                "]");
        }
    };
    return EventEmitter;
}());

eventTypes属性保存事件名字,events属性保存事件处理函数及其作用域。
调用on函数是向events对象对应键的数组中添加事件处理函数和作用域;调用trigger函数是把events对象对应键的数组中保存的时间处理函数全部执行。

比如最开始那段注册scroll事件的代码,执行之后,bs的eventTypesevents属性值可能是:

bs.eventTypes = {
   //...
   scroll: 'scroll'
};
bs.events = {
   //...
   scroll: [fn1, bs]
};

bs注册的scroll事件是通过Scroller类的实例冒泡触发的,相关源代码如下:

// 事件冒泡
BScrollConstructor.prototype.eventBubbling = function () {
    // scroller的钩子事件冒泡到 BScrollConstructor
    // 原理是BScrollConstructor注册的钩子,通过bubbling函数scroller也同样注册,并在注册回调里触发BScrollConstructor的钩子
    bubbling(this.scroller.hooks, this, [
        this.eventTypes.beforeScrollStart,
        this.eventTypes.scrollStart,
        this.eventTypes.scroll,
        this.eventTypes.scrollEnd,
        this.eventTypes.scrollCancel,
        this.eventTypes.touchEnd,
        this.eventTypes.flick
    ]);
};

bubbling函数的代码:

function bubbling(source, target, events) {
    events.forEach(function (event) {
        var sourceEvent;
        var targetEvent;
        if (typeof event === 'string') {
            sourceEvent = targetEvent = event;
        } else {
            sourceEvent = event.source;
            targetEvent = event.target;
        }
        source.on(sourceEvent, function () {
            var args = [];
            for (var _i = 0; _i < arguments.length; _i++) {
                args[_i] = arguments[_i];
            }
            return target.trigger.apply(target, __spreadArrays([targetEvent], args));
        });
    });
}

上面代码中scroller.hooks监听了scroll事件,在事件回调里触发了bs监听的scroll事件。
那么看看scroller.hooksscroll事件是如何触发的。

下面是相关源代码:

//...
this.actions = new ScrollerActions(this.scrollBehaviorX, this.scrollBehaviorY, this.actionsHandler, this.animater, this.options);
//...
Scroller.prototype.bindActions = function () {
    var _this = this;
    var actions = this.actions;
    bubbling(actions.hooks, this.hooks, [
        // ...
        {
            source: actions.hooks.eventTypes.scroll,
            target: this.hooks.eventTypes.scroll,
        },
        // ...
    ]);
    // ...
};

从上面的源码看出,Scroller的scroll钩子也是通过冒泡机制触发的,不过是通过ScrollerActions的实例冒泡触发的。
那么再看一下ScrollerActions的scroll钩子是如何触发的。

下面只列出关键代码:

// this.actionsHandler 是 ActionsHandler类的实例
ScrollerActions.prototype.bindActionsHandler = function () {
    var _this = this;
    // [mouse|touch]start event
    this.actionsHandler.hooks.on(this.actionsHandler.hooks.eventTypes.start, function (
        e) {
        if (!_this.enabled)
            return true;
        return _this.handleStart(e);
    });
    // [mouse|touch]move event 向ActionsHandler的move事件上注册处理函数
    // ActionsHandler的move事件在内部是通过touchmove浏览器事件派发的
    this.actionsHandler.hooks.on(this.actionsHandler.hooks.eventTypes.move, function (
        _a) {
        var deltaX = _a.deltaX,
            deltaY = _a.deltaY,
            e = _a.e;
        if (!_this.enabled)
            return true;
        var _b = applyQuadrantTransformation(deltaX, deltaY, _this.options.quadrant),
            transformateDeltaX = _b[0],
            transformateDeltaY = _b[1];
        var transformateDeltaData = {
            deltaX: transformateDeltaX,
            deltaY: transformateDeltaY,
        };
        _this.hooks.trigger(_this.hooks.eventTypes.coordinateTransformation,
            transformateDeltaData);
        return _this.handleMove(transformateDeltaData.deltaX,
            transformateDeltaData.deltaY, e);
    });
    // ...
};
ScrollerActions.prototype.handleMove = function (deltaX, deltaY, e) {
    // ...
    if (this.contentMoved && positionChanged) {
        this.animater.translate({
            x: newX,
            y: newY
        });
        // 派发scroll事件(这不是真正的浏览器scroll事件,是通过 requestAnimationFrame 函数模拟的)
        this.dispatchScroll(timestamp);
    }
};
ScrollerActions.prototype.dispatchScroll = function (timestamp) {
    // dispatch scroll in interval time
    if (timestamp - this.startTime > this.options.momentumLimitTime) {
        // refresh time and starting position to initiate a momentum
        this.startTime = timestamp;
        this.scrollBehaviorX.updateStartPos();
        this.scrollBehaviorY.updateStartPos();
        
        if (this.options.probeType === 1 /* Throttle */ ) {
            this.hooks.trigger(this.hooks.eventTypes.scroll, Object.assign(this.getCurrentPos(),{t:'123'}));
        }
    }
    // dispatch scroll all the time
    if (this.options.probeType > 1 /* Throttle */ ) {
        this.hooks.trigger(this.hooks.eventTypes.scroll, Object.assign(this.getCurrentPos(),{t:'456'}));
    }
};

从上面代码的调用顺序可以看出,最终是在ActionsHandler类的move钩子中触发了ScrollerActions的scroll钩子。

下面看一下ActionsHandler类的相关源码:

var ActionsHandler = /** @class */ (function () {
    function ActionsHandler(wrapper, options) {
        this.wrapper = wrapper;
        this.options = options;
        // 注册该模块支持的钩子函数
        this.hooks = new EventEmitter([
            'beforeStart',
            'start',
            'move',
            'end',
            'click',
        ]);
        this.handleDOMEvents();
    }
    ActionsHandler.prototype.handleDOMEvents = function () {
        var _a = this.options,
            bindToWrapper = _a.bindToWrapper,
            disableMouse = _a.disableMouse,
            disableTouch = _a.disableTouch,
            click = _a.click;
        var wrapper = this.wrapper;
        var target = bindToWrapper ? wrapper : window;
        var wrapperEvents = [];
        var targetEvents = [];
        var shouldRegisterTouch = !disableTouch;
        var shouldRegisterMouse = !disableMouse;
        if (click) {
            wrapperEvents.push({
                name: 'click',
                handler: this.click.bind(this),
                capture: true,
            });
        }
        if (shouldRegisterTouch) {
            wrapperEvents.push({
                name: 'touchstart',
                handler: this.start.bind(this),
            });
            targetEvents.push({
                name: 'touchmove',
                handler: this.move.bind(this),
            }, {
                name: 'touchend',
                handler: this.end.bind(this),
            }, {
                name: 'touchcancel',
                handler: this.end.bind(this),
            });
        }
        if (shouldRegisterMouse) {
            wrapperEvents.push({
                name: 'mousedown',
                handler: this.start.bind(this),
            });
            targetEvents.push({
                name: 'mousemove',
                handler: this.move.bind(this),
            }, {
                name: 'mouseup',
                handler: this.end.bind(this),
            });
        }
        // EventRegister的实例上有两个属性
        // wrapper -- dom元素
        // events  -- dom元素上注册的事件([{name,handler}])
        this.wrapperEventRegister = new EventRegister(wrapper, wrapperEvents);
        this.targetEventRegister = new EventRegister(target, targetEvents);
    };
    ActionsHandler.prototype.move = function (e) {
        if (eventTypeMap[e.type] !== this.initiated) {
            return;
        }
        this.beforeHandler(e, 'move');
        var point = (e.touches ? e.touches[0] : e);
        var deltaX = point.pageX - this.pointX; // 水平方向上的偏移量
        var deltaY = point.pageY - this.pointY; // 垂直方向上的偏移量
        this.pointX = point.pageX;
        this.pointY = point.pageY;

        // 派发move事件 
        if (this.hooks.trigger(this.hooks.eventTypes.move, {
                deltaX: deltaX,
                deltaY: deltaY,
                e: e,
            })) {
            return;
        }
        // 当滑动的触摸点超出视口区域(滚动区域)时
        // auto end when out of viewport
        // 读取元素滚动条到元素左边的距离,即元素在x方向上滚动的距离
        var scrollLeft = document.documentElement.scrollLeft ||
            window.pageXOffset ||
            document.body.scrollLeft;
        // 获取元素在垂直方向上的滚动距离
        var scrollTop = document.documentElement.scrollTop ||
            window.pageYOffset ||
            document.body.scrollTop;
        var pX = this.pointX - scrollLeft;// 触摸点距离视口(滚动区域)左侧的距离
        var pY = this.pointY - scrollTop; // 触摸点距离视口(滚动区域)顶部的距离
        var autoEndDistance = this.options.autoEndDistance;// line 898
        if (pX > document.documentElement.clientWidth - autoEndDistance ||
            pY > document.documentElement.clientHeight - autoEndDistance ||
            pX < autoEndDistance ||
            pY < autoEndDistance) {
            // 触发end事件(滑动结束)
            this.end(e);
        }
    };
    return ActionsHandler;
}());

handleDOMEvents函数的作用是向wrapper元素(初始化bs时传入的根元素)注册点击事件,手势事件和鼠标事件,我们重点关注touchmove事件。

touchmove事件的处理函数是move函数,move函数中触发了自身的move钩子。

至此我们追踪到的scroll事件的派发链条是:

wrapper元素的touchmove事件 --> ActionsHandler的move钩子 --> ScrollerActions的scroll钩子 --> Scroller的scroll钩子 --> BScrollConstructor的scroll自定义事件

但是还没有完。

上面的派发链条是当用户的手指没有离开设备屏幕时的派发链条,当用户在设备屏幕上快速滑动一段距离时,也会出现滚动,这个滚动过程也会派发scroll事件。

下面看一下用户手指离开设备屏幕后的scroll事件是如何派发的。

相关源代码:

 Scroller.prototype.bindAnimater = function () {
    var _this = this;
    // reset position
    this.animater.hooks.on(this.animater.hooks.eventTypes.end, function (pos) {
        if (!_this.resetPosition(_this.options.bounceTime)) {
            _this.animater.setPending(false);
            _this.hooks.trigger(_this.hooks.eventTypes.scrollEnd, pos);
        }
    });
    bubbling(this.animater.hooks, this.hooks, [
        {
            source: this.animater.hooks.eventTypes.move,
            target: this.hooks.eventTypes.scroll,
        },
        {
            source: this.animater.hooks.eventTypes.forceStop,
            target: this.hooks.eventTypes.scrollEnd,
        },
    ]);
};

从源代码可以看出Scroller的scroll钩子也会通过animater的move钩子触发的。

创建animater的相关代码:

function createAnimater(element, translater, options) {
    var useTransition = options.useTransition;
    var animaterOptions = {};
    //probeType 决定是否派发 scroll 事件,对页面的性能有影响,尤其是在 useTransition 为 true 的模式下。
    Object.defineProperty(animaterOptions, 'probeType', {
        enumerable: true,
        configurable: false,
        get: function () {
            return options.probeType;
        },
    });
    if (useTransition) {
        // 使用transition实现动画
        return new Transition(element, translater, animaterOptions);
    } else {
        // 使用requestAnimationFrame实现动画
        return new Animation(element, translater, animaterOptions);
    }
}
// ...
this.animater = createAnimater(this.content, this.translater, this.options);
// ...

根据实现滚动动画效果的方式不同,创建animater的类也不同,假设是transition实现的滚动动画,那么animater实例就是由Transition类创建的。

看一下Transition类的源代码:

var Transition = /** @class */ (function (_super) {
    __extends(Transition, _super);
    function Transition() {
        return _super !== null && _super.apply(this, arguments) || this;
    }
    // 用来派发事件,不是用来制作动画
    Transition.prototype.startProbe = function (startPoint, endPoint) {
        var _this = this;
        var prePos = startPoint;
        var probe = function () {
            var pos = _this.translater.getComputedPosition();
            if (isValidPostion(startPoint, endPoint, pos, prePos)) {
                // 派发scroll事件
                _this.hooks.trigger(_this.hooks.eventTypes.move, pos);
            }
            // call bs.stop() should not dispatch end hook again.
            // forceStop hook will do this.
            /* istanbul ignore if  */
            if (!_this.pending) {
                if (_this.callStopWhenPending) {
                    _this.callStopWhenPending = false;
                } else {
                    // transition ends should dispatch end hook.
                    _this.hooks.trigger(_this.hooks.eventTypes.end, pos);
                }
            }
            prePos = pos;
            if (_this.pending) {
                _this.timer = requestAnimationFrame(probe);
            }
        };
        // when manually(手动地) call bs.stop(), then bs.scrollTo() 
        // we should reset callStopWhenPending to dispatch end hook
        if (this.callStopWhenPending) {
            this.setCallStop(false);
        }
        cancelAnimationFrame(this.timer);
        probe();
    };

    Transition.prototype.move = function (startPoint, endPoint, time, easingFn) {
        this.setPending(time > 0);
        this.transitionTimingFunction(easingFn);
        this.transitionProperty();
        this.transitionTime(time);
        this.translate(endPoint);
        //probeType 为3表示任何时候都派发 scroll 事件,包括调用 scrollTo 或者触发 momentum 滚动动画
        var isRealtimeProbeType = this.options.probeType === 3 /* Realtime */ ;
        if (time && isRealtimeProbeType) {
            this.startProbe(startPoint, endPoint);
        }
        // if we change content's transformY in a tick
        // such as: 0 -> 50px -> 0
        // transitionend will not be triggered
        // so we forceupdate by reflow
        if (!time) {
            this._reflow = this.content.offsetHeight;
            if (isRealtimeProbeType) {
                this.hooks.trigger(this.hooks.eventTypes.move, endPoint);
            }
            this.hooks.trigger(this.hooks.eventTypes.end, endPoint);
        }
    };
    // ...
    return Transition;
}(Base));

Transition类的move钩子是在startProbe函数中触发的。

我们知道transition实现的动画效果是通过修改元素的css属性值,然后浏览器根据css相关属性的配置添加的动画效果。
整个动画过程我们只能控制动画的开始和结束,中间过程是没有提供任何API接口的。
所以在startProbe函数中是通过requestAnimationFrame递归模拟的scroll事件。

下面继续梳理整个事件派发的链条。

startProbe函数是在move函数中调用的,而Transition类的move函数是在Scroller类的scrollTo函数中调用的,Scroller类的scrollTo函数是在其momentum函数中调用的,而momentum函数是在ScrollerActions类的scrollEnd钩子里调用的:

Scroller.prototype.bindActions = function () {
    var _this = this;
    var actions = this.actions;
    // ...
    actions.hooks.on(actions.hooks.eventTypes.scrollEnd, function (pos, duration) {
        var deltaX = Math.abs(pos.x - _this.scrollBehaviorX.startPos);
        var deltaY = Math.abs(pos.y - _this.scrollBehaviorY.startPos);
        if (_this.checkFlick(duration, deltaX, deltaY)) {
            _this.animater.setForceStopped(false);
            _this.hooks.trigger(_this.hooks.eventTypes.flick);
            return;
        }
        if (_this.momentum(pos, duration)) {
            _this.animater.setForceStopped(false);
            return;
        }
        if (actions.contentMoved) {
            _this.hooks.trigger(_this.hooks.eventTypes.scrollEnd, pos);
        }
        if (_this.animater.forceStopped) {
            _this.animater.setForceStopped(false);
        }
    });
};
Scroller.prototype.momentum = function (pos, duration) {
    // ...
    // when x or y changed, do momentum animation now!
    if (meta.newX !== pos.x || meta.newY !== pos.y) {
        // change easing function when scroller goes out of the boundaries
        if (meta.newX > this.scrollBehaviorX.minScrollPos ||
            meta.newX < this.scrollBehaviorX.maxScrollPos ||
            meta.newY > this.scrollBehaviorY.minScrollPos ||
            meta.newY < this.scrollBehaviorY.maxScrollPos) {
            meta.easing = ease.swipeBounce;
        }
        this.scrollTo(meta.newX, meta.newY, meta.time, meta.easing);
        return true;
    }
};
Scroller.prototype.scrollTo = function (x, y, time, easing, extraTransform) {
    // ...
    this.animater.move(startPoint, endPoint, time, easingFn);
};

继续追踪ScrollerActions类的scrollEnd钩子:

ScrollerActions.prototype.bindActionsHandler = function () {
    var _this = this;
    // ...
    // [mouse|touch]end event
    this.actionsHandler.hooks.on(this.actionsHandler.hooks.eventTypes.end, function (e) {
        if (!_this.enabled)
            return true;
        return _this.handleEnd(e);
    });
    // ...
};

ScrollerActions.prototype.handleEnd = function (e) {
    if (this.hooks.trigger(this.hooks.eventTypes.beforeEnd, e)) {
        return;
    }
    var currentPos = this.getCurrentPos();
    this.scrollBehaviorX.updateDirection();
    this.scrollBehaviorY.updateDirection();
    if (this.hooks.trigger(this.hooks.eventTypes.end, e, currentPos)) {
        return true;
    }
    currentPos = this.ensureIntegerPos(currentPos);
    this.animater.translate(currentPos);
    this.endTime = getNow();
    var duration = this.endTime - this.startTime;
    this.hooks.trigger(this.hooks.eventTypes.scrollEnd, currentPos, duration);
};

从源码可知,最终是ActionsHandler类的end钩子触发了ScrollerActions类的scrollEnd的钩子。

继续看ActionsHandler类的相关源码:

ActionsHandler.prototype.handleDOMEvents = function () {
    var _a = this.options,
        bindToWrapper = _a.bindToWrapper,
        disableMouse = _a.disableMouse,
        disableTouch = _a.disableTouch,
        click = _a.click;
    var wrapper = this.wrapper;
    var target = bindToWrapper ? wrapper : window;
    var wrapperEvents = [];
    var targetEvents = [];
    var shouldRegisterTouch = !disableTouch;
    var shouldRegisterMouse = !disableMouse;
    if (click) {
        wrapperEvents.push({
            name: 'click',
            handler: this.click.bind(this),
            capture: true,
        });
    }
    if (shouldRegisterTouch) {
        wrapperEvents.push({
            name: 'touchstart',
            handler: this.start.bind(this),
        });
        targetEvents.push({
            name: 'touchmove',
            handler: this.move.bind(this),
        }, {
            name: 'touchend',
            handler: this.end.bind(this),
        }, {
            name: 'touchcancel',
            handler: this.end.bind(this),
        });
    }
    if (shouldRegisterMouse) {
        wrapperEvents.push({
            name: 'mousedown',
            handler: this.start.bind(this),
        });
        targetEvents.push({
            name: 'mousemove',
            handler: this.move.bind(this),
        }, {
            name: 'mouseup',
            handler: this.end.bind(this),
        });
    }
    // EventRegister的实例上有两个属性
    // wrapper -- dom元素
    // events  -- dom元素上注册的事件([{name,handler}])
    this.wrapperEventRegister = new EventRegister(wrapper, wrapperEvents);
    this.targetEventRegister = new EventRegister(target, targetEvents);
};
ActionsHandler.prototype.end = function (e) {
    if (eventTypeMap[e.type] !== this.initiated) {
        return;
    }
    this.setInitiated();
    this.beforeHandler(e, 'end');
    this.hooks.trigger(this.hooks.eventTypes.end, e);
};

最终是wrapper元素的touchend事件触发了end钩子。

用户的手指离开设备屏幕后,scroll事件的派发链条是:

wrapper元素的touchend事件 --> ActionsHandler的end钩子 --> ScrollerActions的scrollEnd钩子 --> Scroller类的momentum函数 --> Scroller的scrollTo函数 --> Transition类的move函数 --> Transition类的startProbe函数 --> Transition类的move钩子 --> Scroller的scroll钩子 --> BScrollConstructor的scroll自定义事件

posted @ 2023-10-26 16:08  Fogwind  阅读(12)  评论(0编辑  收藏  举报