preface

Recently, the project is making a web version of customer service chat tool, in which the chat window needs to scroll to load chat records. I just remember that the Ele. me team has a vUE -infinite Scroll plug-in. After looking at the source code, it is found that only scroll down is supported, but chat records are scroll up to load, so we expanded the function of scroll up on its basis. The following is mainly for vue-infinite-scroll plug-in source code analysis and talk about how to expand the function of scrolling up loading.

Plug-in the Usage

Usage 1: Set overflow:auto on the instruction host element itself. The inner element is used to support scrolling. When scrolling to the bottom, increase the height of the inner element to simulate infinite scrolling.

<div class="app" v-infinite-scroll="loadMore" infinite-scroll-disabled="busy" infinite-scroll-distance="10"> <div class="content"></div> <div class="loading" v-show="busy">loading..... </div> </div>Copy the code
.app {
 height: 1000px;
 border: 1px solid red;
 width: 600px;
 margin: 0 auto;
 overflow: auto;
}
.content {
 height: 1300px;
 background-color: #ccc;
 width: 80%;
 margin: 0 auto;
}
.loading {
 font-weight: bold;
 font-size: 20px;
 color: red;
 text-align: center;
}
Copy the code
var app = document.querySelector('.app'); new Vue({ el: app, directives: { InfiniteScroll, }, data: function() { return { busy: false }; }, methods: { loadMore: function() { var self = this; self.busy = true; console.log('loading... ' + new Date()); setTimeout(function() { var target = document.querySelector('.content'); var height = target.clientHeight; target.style.height = height + 300 + 'px'; console.log('end... ' + new Date()); self.busy = false; }, 1000); ,}}});Copy the code

The effect is as follows:

Usage 2: Set the parent element to scroll. When scrolling to the bottom of the parent element, increase its height to simulate the operation of pulling data from the next page. :

<div class="app"> <div class="content" v-infinite-scroll="loadMore" infinite-scroll-disabled="busy" infinite-scroll-distance="10"></div> <div class="loading" v-show="busy">loading..... </div> </div>Copy the code

The result is exactly the same as above.

The source code parsing

The entrance

Let’s start with the entry, which makes sense since the plug-in is a VUE directive

Export default {bind (el, binding, vnode) {// Warning: all parameters except el should be read only. If you need to share data between hooks, it is recommended to do so through the element's dataset. El [CTX] = {el, vm: vnode.context, // vue instance expression: Binding. value // The callback function needed to scroll to the bottom or top, } const args = arguments el[CTX].vm.$once('hook:mounted', Function () {el[CTX].vm.$nextTick(function () {doBind. Call (el[CTX], Args)} el[CTX]. BindTryCount = 0 Var tryBind = function () {if (el[CTX].bindtryCount > 10) return; //eslint-disable-line el[ctx].bindTryCount++ if (isAttached(el)) { doBind.call(el[ctx], args) } else { setTimeout(tryBind, 50) } } tryBind() }) }) }, Unbind (el) {if (el && el[CTX] && el[CTX].scrolleventTarget) { el[ctx].scrollEventTarget.removeEventListener('scroll', el[ctx].scrollListener) } } }Copy the code

The core is that after the host element is rendered, the doBind method is executed, and we assume that the doBind will bind the scroll event of the parent element.

The isAttached method is used to determine whether an element has been rendered on the page by looking at whether the tag name of the ancestor element is HTML:

var isAttached = function (element) { var currentNode = element.parentNode while (currentNode) { if (currentNode.tagName === 'HTML') {return true} // nodeType === 11  currentNode = currentNode.parentNode } return false }Copy the code

Little knowledge:

The DocumentFragment node does not belong to the document tree. If the parent node of an element is a DocumentFragment, it indicates that the element has not been inserted into the document tree and does not have a parent node.

The DocumentFragment interface represents a portion (or segment) of a document. Rather, it represents one or more adjacent Document nodes and all of their descendants. The DocumentFragment node is not part of the document tree and the inherited parentNode property is always null. But it has a special behavior that makes it useful: when a request is made to insert a DocumentFragment node into the document tree, instead of inserting the DocumentFragment itself, it inserts all of its descendants. This makes the DocumentFragment a useful placeholder for temporarily storing nodes that are inserted into the document at once. It is also useful for cutting, copying, and pasting documents, especially when used with the Range interface. Can use the Document. CreateDocumentFragment () method creates a new empty DocumentFragment node.Copy the code

The binding

Here we get the user configuration item by getting the element attribute, and we find the most recent scrollable parent element of the host and then we bind the scrollable event, and we use the scrollable event to check when should we fire an event

Var doBind = function () { If (this.binded) return this.binded = true var directive = this var element = directive.el // ThrottleDelayExpr = element.getAttribute('infinite- Scroll -throttle-delay') // Default 200 milliseconds var throttleDelay = 200 if (throttleDelayExpr) {/ / priority to try instance throttleDelayExpr throttleDelay = Number corresponding attributes (directive. The vm [throttleDelayExpr] | | throttleDelayExpr) if (isNaN(throttleDelay) || throttleDelay < 0) { throttleDelay = 200 } } directive.throttleDelay = throttleDelay directive.scrollEventTarget = getScrollEventTarget(element) directive.scrollListener = throttle(doCheck.bind(directive), directive.throttleDelay) directive.scrollEventTarget.addEventListener('scroll', directive.scrollListener) this.vm.$once('hook:beforeDestroy', function () { directive.scrollEventTarget.removeEventListener('scroll', Var disabledExpr = element.getAttribute(' infinite-scrollListener ') // Disables unlimited scrolling by default Var disabled = false If (disabledExpr) {this.vm.$watch(disabledExpr, Function (value) {directive.disabled = value // If disable is false, restart check if (! value && directive.immediateCheck) { doCheck.call(directive) } }) disabled = Boolean(directive.vm[disabledExpr]) } Directive. disabled = disabled // The distance threshold between the scroll bar and the top or bottom, < this value doCheck var distanceExpr = element.getAttribute('infinite-scroll distance') // Default is 0 var distance = 0 if (distanceExpr) { distance = Number(directive.vm[distanceExpr] || distanceExpr) if (isNaN(distance)) { distance = 0 } } ImmediateCheckExpr = immediateCheckExpr = immediateCheckExpr = ImmediateCheck = true if (immediateCheckExpr) {immediateCheckExpr = true if (immediateCheckExpr) {immediateCheckExpr = true immediateCheck = Boolean(directive.vm[immediateCheckExpr]) } directive.immediateCheck = immediateCheck if ImmediateCheck) {doCheck.call(Directive, false)} // When this event set on the component is triggered, perform a check, Var eventName = element.getAttribute('infinite-scroll-listen-for-event') if (eventName) {var eventName = element.getAttribute('infinite-scroll-listen-for-event') { directive.vm.$on(eventName, function () { doCheck.call(directive) }) } }Copy the code

In fact, doBind obtains user configuration, including triggering interval, whether triggering immediately, whether disabling, how far to trigger, and manually triggering events, and controls doCheck execution timing through these configuration items.

The search for a scroll parent starts with itself, so we can set the directive on the scroll element itself as in Usage 1

// Start with itself and find the parent element with the scroll set. Var currentNode = function(element) {var currentNode = element; // nodeType 1 represents the element node while (currentNode && CurrentNode.tagName! == 'HTML' && currentNode.tagName ! == 'BODY' && currentNode.nodeType === 1) { var overflowY = getComputedStyle(currentNode).overflowY; if (overflowY === 'scroll' || overflowY === 'auto') { return currentNode; } currentNode = currentNode.parentNode; } return window; };Copy the code

Core logic doCheck

This function checks to see if the scroll has reached the bottom. The element that you’re scrolling through can be itself or a parent element.

var doCheck = function(force) { var scrollEventTarget = this.scrollEventTarget; var element = this.el; var distance = this.distance; if (force ! == true && this.disabled) return; Var viewportScrollTop = getScrollTop(scrollEventTarget); // viewportBottom: The distance between the bottom of the scroll element and the top of the document coordinate; Var viewportBottom = viewportScrollTop + getVisibleHeight(scrollEventTarget); Var shouldTrigger = false; / / rolling element is itself if the host elements (scrollEventTarget = = = element) {shouldTrigger = scrollEventTarget. ScrollHeight - viewportBottom < = distance; Var elementBottom = getElementTop(element) - getElementTop(scrollEventTarget) +  element.offsetHeight + viewportScrollTop; shouldTrigger = viewportBottom + distance >= elementBottom; } if (shouldTrigger && this.expression) { this.expression(); // Trigger the binding infinite scroll function}};Copy the code

The scroll element is the host element

The scroll element is the parent of the host element

expand

From the source code, we can see that the plug-in only determines the case of scrolling down. In the introduction, we mentioned that the business needs to scroll up to load. After analyzing the source code, we know that the plug-in internally obtains configuration items through el.getAttribute (), and then makes judgment during doCheck and triggers the wireless scroll function bound by the user. So we came up with the following idea:

1. Add trigger configuration items

Var triggerTypeExpr = element.getAttribute(' infinite-scrolling-trigger-type ') // The default trigger type is scroll down var triggerType = 'scrollDown' if (triggerTypeExpr) { triggerType = directive.vm[triggerTypeExpr] || triggerTypeExpr // Optional values are 'scrollDown', 'scrollUp' if (! ['scrollDown', 'scrollUp'].includes(triggerType)) { triggerType = 'scrollDown' } } directive.triggerType = triggerTypeCopy the code

2. Judge the trigger condition for the trigger type ‘scrollUp’ during doCheck

Here we are actually determining whether the distance between the top of the host element and the top of the scroll element is less than or equal to the distance threshold, distance

If (scrollEventTarget === element) {if (triggerType === 'scrollDown') {shouldTrigger = scrollEventTarget.scrollHeight - viewportBottom <= distance } else { shouldTrigger = viewportScrollTop <= distance } } Var topGap = getElementTop(element) -getelementTop (scrollEventTarget) // ElementBottom: Var elementBottom = topGap + element.offsetheight + viewportScrollTop if (triggerType === 'scrollDown')  { shouldTrigger = viewportBottom + distance >= elementBottom } else { shouldTrigger = topGap <= distance } }Copy the code

The expanded configuration items are as follows. The usage is basically the same as above, but there is one more configuration item

Option Description
infinite-scroll-disabled infinite scroll will be disabled if the value of this attribute is true.
infinite-scroll-distance Number(default = 0) – the minimum distance between the bottom of the element and the bottom of the viewport before the v-infinite-scroll method is executed.
infinite-scroll-immediate-check Boolean(default = true) – indicates that the directive should check immediately after bind. Useful if it’s possible that the content is not tall enough to fill up the scrollable container.
infinite-scroll-listen-for-event infinite scroll will check again when the event is emitted in Vue instance.
infinite-scroll-throttle-delay Number(default = 200) – interval(ms) between next time checking and this time.
infinite-scroll-trigger-type String(default = ‘scrollDown’) – choose between ‘scrollDown’ and ‘scrollUp’.

The warehouse address