current position:Home>Analysis and expansion of Vue infinite scroll source code

Analysis and expansion of Vue infinite scroll source code

2021-08-27 09:42:49 Answer 321


Recently, the project is doing a web Version customer service chat tool , There is a chat window inside, which needs to scroll and load chat records , Just remember when you were hungry? The team had a vue-infinite-scroll plug-in unit , After looking at the source code, I found that it only supports scrolling down , But chat logs are scrolled up , Therefore, the function of upward scrolling is expanded on its basis . The following is mainly about vue-infinite-scroll Plug in source code analysis and how to expand the function of upward scrolling loading .

plug-in unit Usage

usage 1: Set the instruction host element itself to overflow:auto, Internal elements are used to support rolling , When scrolling to the bottom , Increasing the height of the internal elements simulates 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>
 Copy 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 code 
var app = document.querySelector('.app');
new Vue({
 el: app,
 directives: {
 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; = height + 300 + 'px';
    console.log('end... ' + new Date());
    self.busy = false;
   }, 1000);
 Copy code 

The effect is as follows :

usage 2: Set the parent element to scroll , When you scroll to the bottom of the parent element , Increase your height , Simulate the operation of pulling the next page of data .:

<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>
 Copy code 

The effect is exactly the same as the above .

The source code parsing


Let's start at the entrance , Because this plug-in is a vue Instructions , So the entrance is quite understandable

export default {
  bind (el, binding, vnode) {
    //  Official warning : except  el  outside , All other parameters should be read-only , Do not modify . If you need to share data between hooks , It is recommended that the  dataset  To carry out .
    el[ctx] = {
      vm: vnode.context, // vue example 
      expression: binding.value //  Callback function required to scroll to the bottom or top , Usually used to load the next page of data 
    const args = arguments
    el[ctx].vm.$once('hook:mounted', function () {
      el[ctx].vm.$nextTick(function () {
        //  Determine whether the element is already on the page 
        if (isAttached(el)) {
[ctx], args)

        el[ctx].bindTryCount = 0

        //  interval 50ms polling 10 Time , Determine whether the element is already on the page 
        var tryBind = function () {
          if (el[ctx].bindTryCount > 10) return; //eslint-disable-line
          if (isAttached(el)) {
  [ctx], args)
          } else {
            setTimeout(tryBind, 50)


  unbind (el) {
    //  Scroll event unbound 
    if (el && el[ctx] && el[ctx].scrollEventTarget) { el[ctx].scrollEventTarget.removeEventListener('scroll', el[ctx].scrollListener) }
 Copy code 

The core is after the host element is rendered , perform doBind Method , We guess it will be in doBind Bind the parent element of the scroll scroll event .

isAttached Method is used to determine whether an element has been rendered on the page , The judgment method is to check 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  Indicates that the node is  DocumentFragment
    if (currentNode.nodeType === 11) {
      return false
    currentNode = currentNode.parentNode
  return false
 Copy code 

Little knowledge :

nodeType==11  Node is  DocumentFragment,DocumentFragment  node   Does not belong to the document tree , If the parent node of an element is  DocumentFragment, Then it means that this element has not been inserted into Document tree in , There is no parent node .

DocumentFragment  The interface represents part of the document ( Or a paragraph ). To be more exact , It represents one or more adjacent  Document  Nodes and all their descendants .
DocumentFragment  The node does not belong to the document tree , inherited  parentNode  Property always  null.
 But it has a special behavior , This behavior makes it very useful , That is, when a request is sent to  DocumentFragment  When the node is inserted into the document tree , It's not  DocumentFragment  Oneself , But all its descendants . This makes  DocumentFragment  Become a useful placeholder , Temporarily store the nodes inserted into the document at one time . It also facilitates document clipping 、 Copy and paste operations , Especially with  Range  This is especially true when interfaces are used together .
 It can be used  Document.createDocumentFragment()  Method to create a new empty  DocumentFragment  node .
 Copy code 


Here, the user configuration item is obtained by obtaining the element attribute , And find the nearest scrollable parent element of the host, and then bind the scrolling event , Scroll through events to check when an event should be triggered

var doBind = function () {
  //  Bind only once , After binding, it returns 
  if (this.binded) return
  this.binded = true

  var directive = this
  var element = directive.el

  //  Closure interval 
  var throttleDelayExpr = element.getAttribute('infinite-scroll-throttle-delay')
  //  Default 200 millisecond 
  var throttleDelay = 200
  if (throttleDelayExpr) {
    //  Give priority to... On the instance throttleDelayExpr Corresponding properties 
    throttleDelay = Number(directive.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', directive.scrollListener)

  //  Whether to disable infinite scrolling 
  var disabledExpr = element.getAttribute('infinite-scroll-disabled')
  //  Not disabled by default 
  var disabled = false

  //  If this item is configured , Then listen for disabledExpr Corresponding properties 
  if (disabledExpr) {
    this.vm.$watch(disabledExpr, function (value) {
      directive.disabled = value
      //  When disable by false when , restart check
      if (!value && directive.immediateCheck) {
    disabled = Boolean(directive.vm[disabledExpr])
  directive.disabled = disabled

  //  The distance between the scroll bar and the top or bottom , Execute when less than this value doCheck
  var distanceExpr = element.getAttribute('infinite-scroll-distance')
  //  The default is 0
  var distance = 0
  if (distanceExpr) {
    distance = Number(directive.vm[distanceExpr] || distanceExpr)
    if (isNaN(distance)) {
      distance = 0
  directive.distance = distance

  //  Whether to execute immediately doCheck
  var immediateCheckExpr = element.getAttribute('infinite-scroll-immediate')
  //  The default is true
  var immediateCheck = true
  if (immediateCheckExpr) {
    immediateCheck = Boolean(directive.vm[immediateCheckExpr])
  directive.immediateCheck = immediateCheck

  if (immediateCheck) {, false)

  //  When this event set on the component is triggered , Perform an inspection , It is generally used for manual trigger inspection 
  var eventName = element.getAttribute('infinite-scroll-listen-for-event')
  if (eventName) {
    directive.vm.$on(eventName, function () {
 Copy code 

doBind In fact, it is to get the user configuration , Including trigger interval 、 Whether to trigger immediately 、 Whether to disable 、 The distance triggers 、 Trigger events manually , Control through these configuration items doCheck Execution opportunity .

When looking for a scrolling parent element, it starts from itself , So we can use it like 1 That way, set the instruction on the scroll element itself

//  Start with yourself , Find the parent element with scrolling set . overflow-y  by scroll or auto
var getScrollEventTarget = function(element) {
 var currentNode = element;
 //  Solve listening body and html Compatibility problem with scrolling events on 
 // 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 code 

Core logic doCheck

This function is used to check whether it has scrolled to the bottom . Here, the elements to be scrolled can be themselves and 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;
  //  The distance between the top of the scrolling element and the top of the document coordinates 
 var viewportScrollTop = getScrollTop(scrollEventTarget);
 // viewportBottom:  The distance between the bottom of the scrolling element and the top of the document coordinates ; visibleHeight: The height of a scrolling element without a border 
 var viewportBottom = viewportScrollTop + getVisibleHeight(scrollEventTarget);
//  Whether to trigger 
 var shouldTrigger = false;

 //  The scrolling element is the host element itself 
 if (scrollEventTarget === element) {
  shouldTrigger = scrollEventTarget.scrollHeight - viewportBottom <= distance;
 } else {
   //  The scroll element is the parent element of the host element 
   //  The difference between itself and the top of the parent element 
  var elementBottom = getElementTop(element) - getElementTop(scrollEventTarget) + element.offsetHeight + viewportScrollTop;

  shouldTrigger = viewportBottom + distance >= elementBottom;

 if (shouldTrigger && this.expression) {
  this.expression(); //  Trigger bound infinite scroll function 
 Copy code 

The scroll element is the host element

The scroll element is the parent element of the host element


We can see from the source code that , The plug-in only judges the scroll down , In the preface, we mentioned that the business needs to scroll up . Analyzed the source code , We know that the internal of the plug-in is through ele.getAttribute() To get the name of the configuration item , And then in doCheck Make a judgment and trigger the wireless scrolling function bound by the user . So we have the following ideas :

1、 Add configuration items of trigger type

//  The trigger type is scroll up or scroll down 
  var triggerTypeExpr = element.getAttribute('infinite-scroll-trigger-type')
  //  The default trigger type is scroll down 
  var triggerType = 'scrollDown'
  if (triggerTypeExpr) {
    triggerType = directive.vm[triggerTypeExpr] || triggerTypeExpr
    //  Optional value is 'scrollDown', 'scrollUp'
    if (!['scrollDown', 'scrollUp'].includes(triggerType)) {
      triggerType = 'scrollDown'
  directive.triggerType = triggerType
 Copy code 

2、doCheck It is the trigger type when 'scrollUp' Make trigger condition judgment

Here, we actually need to judge 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, that is distance

//  The scrolling element is the host element itself 
  if (scrollEventTarget === element) {
    if (triggerType === 'scrollDown') {
      shouldTrigger = scrollEventTarget.scrollHeight - viewportBottom <= distance
    } else {
      shouldTrigger = viewportScrollTop <= distance
  } else {
    //  The scroll element is the parent element of the host element 
    //  The difference between itself and the top of the parent element 
    var topGap = getElementTop(element) - getElementTop(scrollEventTarget)
    // elementBottom: The distance between the bottom of the host element and the top of the document coordinates 
    var elementBottom = topGap + element.offsetHeight + viewportScrollTop

    if (triggerType === 'scrollDown') {
      shouldTrigger = viewportBottom + distance >= elementBottom
    } else {
      shouldTrigger = topGap <= distance
 Copy code 

The expanded configuration items are as follows , The usage is basically the same as the above , Just more configuration items

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'.

Warehouse address

copyright notice
author[Answer 321],Please bring the original link to reprint, thank you.