I had never really written a component before. Before, in the process of writing business code is encapsulated with the others good components, this time try to write a picture by components, although less well-known wheel component, but it’s basic functions intact, and in the process of writing this component, learned a lot, to share, and came here if there are any omissions, welcome to correct!

Before making the component, the author Google a lot of articles about shuffling, found to implement a shuffling thinking although is different, but the big logic is actually about the same, this article main basis for classes online focus by special effects in this class, but mu class is mainly written in native JS, while the author reconstructed using the Vue, and made a little modification. The component rendering after completion is as follows:

One, clear thinking, understand the needs and principles

1. What kind of rotation do you want to write?

  • When clicking the right arrow, slide the image left to the next one; When you click the left arrow, the image slides right to the next one
  • Click on the dot below to slide to the corresponding picture, and the style of the dot will also change
  • It’s gonna have a transition effect. It’s gonna slide through
  • Hover pauses when the mouse hover over an image, and continues when the mouse leave
  • Autoplay function
  • Infinite scrolling, which means when you get to the last one, you click on the next one and you keep sliding left to the first one, not all the way to the first one, which is kind of hard

2. Understand the principle of infinite rotation

Let’s take a look at the schematic:

The red line area in the figure is the image we see. This rotation shows only five images, but it also has two images at the beginning and the end. Figure 5 is placed in front of Figure 1, and Figure 1 is placed behind figure 5. The principle of infinite scrolling is that when the whole graph scrolls left to the right of Figure 5, it continues forward to Figure 1, and when figure 1 is fully displayed, it pulls right back to the leftmost figure 1 at a speed invisible to the naked eye. Thus, even if you slide to the left, you see Figure 2.

As shown below: After the final transition is complete and fully displayed in Figure 1, the entire list is momentarily pulled right to Figure 1 on the left. The other edge diagram, Figure 5, also scrolls in the opposite direction.

Two, let the picture switch first

1. Layout and preparation

<template>
  <div id="slider">
    <div class="window"> // window <ul class="container" :style="containerStyle"<img: SRC = "//" > <img: SRC = "//""sliders[sliders.length - 1].img" alt="">
        </li>
        <li v-for="(item, index) in sliders" :key="index"Word-wrap: break-word! Important; "> <img: SRC =" box-sizing: border-box! Important"item.img" alt=""> </li> <li> // The auxiliary image at the bottom of the list, which, like Figure 1, is used for infinite scrolling <img: SRC ="sliders[0].img" alt="">
        </li>
      </ul>
      <ul class="direction"> // Arrows on both sides <li class="left">
          <svg class="icon" width="30px" height="30.00 px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path fill="#ffffff" d="M481.233 904c8.189 0 16.379-3.124 22.628-9.372 12.496-12.497 12.496-32.759 0-45.256L166.488 512l3373-337.373-c12.496-12.497 12.496-32.758 0-45.255-12.498-12.497-32.758-12.497-45.256 0L -360 360C-12.496 12.497-12.496 32.758 0 45.255l360 360c6.249 6.249 14.439 9.373 22.628 9.373z"  /></svg>          
        </li>
        <li class="right">
          <svg class="icon" width="30px" height="30.00 px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path fill="#ffffff" d="M557.179 904c-8.189 0-16.379-3.124-22.628-9.372-12.496-12.496-32.759 0-45.256L871.924 512 534.551 174.627c-12.496-12.497-12.496-32.758 0-45.255 12.498-12.497 32.758-12.497 45.256 0l360 360c12.496 12.497 12.496 32.758 0 45.255-360-360-3.369 9.373-22.629 9.373-22.628 9.373-z"  /></svg>          
        </li>
      </ul>
      <ul class="dots"> // <li v-for="(dot, i) in sliders" :key="i" 
        :class="{dotted: i === (currentIndex-1)}"
        >
        </li>
      </ul>
    </div>
  </div>
</template>

<script>
export default {
  name: 'slider'.data () {
    return {
      sliders:[
        {
          img:'.. /.. /static/images/1.jpg'
        },
        {
          img:'.. /.. /static/images/2.jpg'
        },
        {
          img:'.. /.. /static/images/3.jpg'
        },
        {
          img:'.. /.. /static/images/4.jpg'
        },
        {
          img:'.. /.. /static/images/5.jpg'
        }
      ],
      currentIndex:1,
      distance:-600
    }
  },
  computed:{
    containerStyle() {// Use the calculated property, transform to move the entire list of imagesreturn {
        transform:`translate3d(${this.distance}px, 0, 0)`
      }
    }
  }
}
</script>

Copy the code

Ok, the layout is roughly like this, the renderings are as follows:

The above code has been commented out, with a few points to mention here:

  • Window is the red box,Width is 600 px, it does not move. What it moves is the container that wraps the image:style="containerStyle", this is a calculated property, usingtransform:translate3d(${this.distance, 0, 0})To control left and right movement
  • The data in thedistanceandcurrentIndexIs the key.distanceControls the distance moved, default is -600, to show the second of the seven images, figure 1.currentIndexIs the index of the images displayed on the window, which defaults to 1 and is the second of seven images.
  • There are only 5 images to show, but figure 5 is placed in front of Figure 1 and Figure 1 is placed behind figure 5 to do infinite scrolling, as explained earlier
  • When you click the arrow on the right, the Container moves to the left,distanceIt gets smaller and smaller; When you click the arrow to the left, the Container moves to the right,distanceIt’s getting bigger and bigger. Don’t get it wrong

2. Picture switching

We add click events to the left and right arrows:

      <ul class="direction">
        <li class="left" @click="move(600, 1)">
          <svg class="icon" width="30px" height="30.00 px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path fill="#ffffff" d="M481.233 904c8.189 0 16.379-3.124 22.628-9.372 12.496-12.497 12.496-32.759 0-45.256L166.488 512l3373-337.373-c12.496-12.497 12.496-32.758 0-45.255-12.498-12.497-32.758-12.497-45.256 0L -360 360C-12.496 12.497-12.496 32.758 0 45.255l360 360c6.249 6.249 14.439 9.373 22.628 9.373z"  /></svg>          
        </li>
        <li class="right" @click="move(600, -1)">
          <svg class="icon" width="30px" height="30.00 px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path fill="#ffffff" d="M557.179 904c-8.189 0-16.379-3.124-22.628-9.372-12.496-12.496-32.759 0-45.256L871.924 512 534.551 174.627c-12.496-12.497-12.496-32.758 0-45.255 12.498-12.497 32.758-12.497 45.256 0l360 360c12.496 12.497 12.496 32.758 0 45.255-360-360-3.369 9.373-22.629 9.373-22.628 9.373-z"  /></svg>          
        </li>
      </ul>
      
      ......
      
      methods:{
            move(offset, direction) {
                this.distance += this.distance * direction
                if (this.distance < -3000) this.distance = -600
                if (this.distance > -600) this.distance = -3000
            }
      }
      
Copy the code

To explain the above code: click the arrow on the left or right and call move. Move takes offset and direction. Direction Only two values are passed: 1 indicates that the Container moves to the right, -1 indicates that the Container moves to the left. The offset is 600, which is the width of an image. If you move to the last of the seven images, pull the container to the second of the seven images. If you move to the first of the seven images, pull the container to the fifth of the seven images.

Effect:

As you can see, the image has been changed, but the dots below are not changed. Now let’s add this effect. > < p class=”{dotted: I === (currentIndex – 1)}”

Change the code in the move method:

. move(offset, direction) { direction === -1 ? this.currentIndex++ : this.currentIndex--if (this.currentIndex > 5) this.currentIndex = 1
    if (this.currentIndex < 1) this.currentIndex = 5
    this.distance = this.distance + offset * direction
    if (this.distance < -3000) this.distance = -600
    if (this.distance > -600) this.distance = -3000
    }

Copy the code

The three added lines above make sense: if you click the right arrow, container moves to the left, this.currentIndex is subtracted by 1, and if not, it is added by 1.

Effect:

As you can see, the dot is already working.

Transition animation

The above code has implemented the switch, but without the animation effect, it looks very stiff, the next step is to add a transition effect to each image switch process.

The author does not use Vue’s own class hook or CSS’s transition property. Instead, the author uses moOCs’ setTimeout method to recurse.

In fact, I have tried using Vue hooks, but there are always some minor issues; Take this example found below: example

This example has some problems with the boundaries of transitions, which I have also encountered, and again intermittently. If the CSS transition method is used, there will always be a flash in The Chrome browser to handle the infinite scroll boundary, even if the -webkit-transform-style:preserve-3d; And -webkit-backface-visibility: Hidden is still useless, and the Transition End event that needs to be used with the transition doesn’t work well in Internet Explorer.

If you see a better way, let us know in the comments

Here we write the transition effect, mainly rewriting:

methods:{
    move(offset, direction) {
        direction === -1 ? this.currentIndex++ : this.currentIndex--
        if (this.currentIndex > 5) this.currentIndex = 1
        if (this.currentIndex < 1) this.currentIndex = 5

        const destination = this.distance + offset * direction
        this.animate(destination, direction)
    },
    animate(des, direc) {
        if ((direc === -1 && des < this.distance) || (direc === 1 && des > this.distance)) {
            this.distance += 30 * direc        
            window.setTimeout(() => {
                this.animate(des, direc)
            }, 20)
        } else {
            this.distance = des
            if (des < -3000) this.distance = -600
            if (des > -600) this.distance = -3000
      }
    }
}
Copy the code

The code above is what I find most troublesome and difficult to understand about this rotation.

First, we rewrite the move method, because we need to move a little bit, so we need to calculate the target distance to move to. We then write an animate function to make the transition. The animate function takes two parameters, the distance to be moved and the direction. After clicking the arrow on the right, the container moves to the left. If it does not reach the desired distance, subtract some distance from this.distance. If it does not reach the desired distance, after 20 millimeters, call this.animate. If it moves to the target distance, it assigns the target distance to this.distance, and then determines the boundary and infinite scroll.

Of course, window.setinterval () can also do this, and it’s a little easier to understand because there’s no recursion:

methods:{
    move(offset, direction) {
        direction === -1 ? this.currentIndex++ : this.currentIndex--
        if (this.currentIndex > 5) this.currentIndex = 1
        if (this.currentIndex < 1) this.currentIndex = 5

        const destination = this.distance + offset * direction
        this.animate(destination, direction)
    },
    animate(des, direc) {
        const temp = window.setInterval(() => {
            if ((direc === -1 && des < this.distance) || (direc === 1 && des > this.distance)) {
                this.distance += 30 * direc
            } else {
                window.clearInterval(temp)
                this.distance = des
                if (des < -3000) this.distance = -600
                if (des > -600) this.distance = -3000
            }
        }, 20)
    }  
}
Copy the code

The result is as follows:

Four, a simple throttling

At this point, the effect is there, but there is a problem. If you click multiple times quickly, the following situation may occur:

Has this kind of situation the reason is very simple, because of transition is to use a timer, so the continuous rapid click can appear disorder, simple throttling is good: click on the arrow is invalid, before the transition was done is actually set a brake, click to open the gate for the first time, before the gates open again, for part of the code to perform, and then at the right time to open the gate.

We place the gate in the move function:

move(offset, direction) {
    if(! this.transitionEnd)return// this. TransitionEnd =falseDirection === -1? this.currentIndex++ : this.currentIndex--if (this.currentIndex > 5) this.currentIndex = 1
    if (this.currentIndex < 1) this.currentIndex = 5

    const destination = this.distance + offset * direction
    this.animate(destination, direction)
}
Copy the code

This.transitionend is the key to the gate, and we place it in data:

this.transitionEnd: true
Copy the code

This.tranisitonend = false this.tranisitonEnd = false this.tranisitonEnd = false this.tranisitonEnd = false The next step is to open the gate at the appropriate time, when the transition is complete, in the animate function:

animate(des, direc) {
    if (this.temp) { 
        window.clearInterval(this.temp)
        this.temp = null 
    }
    this.temp = window.setInterval(() => {
        if ((direc === -1 && des < this.distance) || (direc === 1 && des > this.distance)) {
          this.distance += 30 * direc  
        } else {
          this.transitionEnd = true      //闸再次打开
          window.clearInterval(this.temp)
          this.distance = des
          if (des < -3000) this.distance = -600
          if (des > -600) this.distance = -3000
        }
    }, 20)
}      
Copy the code

This quick click eliminates the previous problem:

5. Click the dot to switch pictures

Code so far:

<template>
  <div id="slider">
    <div class="window">
      <ul class="container" :style="containerStyle">
        <li>
          <img :src="sliders[sliders.length - 1].img" alt="">
        </li>
        <li v-for="(item, index) in sliders" :key="index">
          <img :src="item.img" alt="">
        </li>
        <li>
          <img :src="sliders[0].img" alt="">
        </li>
      </ul>
      <ul class="direction">
        <li class="left" @click="move(600, 1)">
          <svg class="icon" width="30px" height="30.00 px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path fill="#ffffff" d="M481.233 904c8.189 0 16.379-3.124 22.628-9.372 12.496-12.497 12.496-32.759 0-45.256L166.488 512l3373-337.373-c12.496-12.497 12.496-32.758 0-45.255-12.498-12.497-32.758-12.497-45.256 0L -360 360C-12.496 12.497-12.496 32.758 0 45.255l360 360c6.249 6.249 14.439 9.373 22.628 9.373z"  /></svg>          
        </li>
        <li class="right" @click="move(600, -1)">
          <svg class="icon" width="30px" height="30.00 px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path fill="#ffffff" d="M557.179 904c-8.189 0-16.379-3.124-22.628-9.372-12.496-12.496-32.759 0-45.256L871.924 512 534.551 174.627c-12.496-12.497-12.496-32.758 0-45.255 12.498-12.497 32.758-12.497 45.256 0l360 360c12.496 12.497 12.496 32.758 0 45.255-360-360-3.369 9.373-22.629 9.373-22.628 9.373-z"  /></svg>          
        </li>
      </ul>
      <ul class="dots">
        <li v-for="(dot, i) in sliders" :key="i" 
        :class="{dotted: i === (currentIndex-1)}"
        >
        </li>
      </ul>
    </div>
  </div>
</template>

<script>
export default {
  name: 'slider'.data () {
    return {
      sliders:[
        {
          img:'.. /.. /static/images/1.jpg'
        },
        {
          img:'.. /.. /static/images/2.jpg'
        },
        {
          img:'.. /.. /static/images/3.jpg'
        },
        {
          img:'.. /.. /static/images/4.jpg'
        },
        {
          img:'.. /.. /static/images/5.jpg'
        }
      ],
      currentIndex:1,
      distance:-600,
      transitionEnd: true
    }
  },
  computed:{
    containerStyle() {
      return {
        transform:`translate3d(${this.distance}px, 0, 0)`
      }
    }
  },
  methods:{
    move(offset, direction) {
      if(! this.transitionEnd)return
      this.transitionEnd = false
      direction === -1 ? this.currentIndex++ : this.currentIndex--
      if (this.currentIndex > 5) this.currentIndex = 1
      if (this.currentIndex < 1) this.currentIndex = 5

      const destination = this.distance + offset * direction
      this.animate(destination, direction)
    },
    animate(des, direc) {
      if (this.temp) { 
        window.clearInterval(this.temp)
        this.temp = null 
      }
      this.temp = window.setInterval(() => {
        if ((direc === -1 && des < this.distance) || (direc === 1 && des > this.distance)) {
          this.distance += 30 * direc
        } else {
          this.transitionEnd = true
          window.clearInterval(this.temp)
          this.distance = des
          if (des < -3000) this.distance = -600
          if (des > -600) this.distance = -3000
        }
      }, 20)
    }
  }
}
</script>
Copy the code

Next we need to realize the transition and image switching by clicking on the little dot below.

<ul class="dots">
    <li v-for="(dot, i) in sliders" :key="i" 
    :class="{dotted: i === (currentIndex-1)}"
    @click = jump(i+1)>
    </li>
</ul>
Copy the code

When the dot is clicked we call the jump function and pass the index I +1 to it. Special attention should be paid to that the index of small dots is inconsistent with the index corresponding to the picture. There are 7 pictures in total, and the 5 small dots correspond to the 5 in the middle of the picture, so we pass I +1.

jump(index) { const direction = index - this.currentIndex >= 0 ? Const offset = math.abs (index - this.currentIndex) * 600Copy the code

CurrentIndex = +1; currentIndex = +1; jump = +1;

direction === -1 ? this.currentIndex += offset/600 : this.currentIndex -= offset/600
Copy the code

I’m just going to change a line and compute the currentIndex based on offset.

However, there is another problem, the long distance switching speed is too slow, as follows:

Add a speed parameter to move and animate functions to make it take the same time to animate multiple images:

jump(index) { const direction = index - this.currentIndex >= 0 ? 1: 1 const offset = Math.abs(index - this.currentIndex) * 600 const jumpSpeed = Math.abs(index - this.currentIndex) === 0 ?  this.speed : Math.abs(index - this.currentIndex) * this.speed this.move(offset, direction, jumpSpeed) }Copy the code

Auto play and pause

That’s about it, but it’s pretty simple here. Call the play function:

play() {
    if (this.timer) {
        window.clearInterval(this.timer)
        this.timer = null
    }
    this.timer = window.setInterval(() => {
        this.move(600, -1, this.speed)
    }, 4000)
}
Copy the code

In addition to automatic playback after initialization, we also use mouseover and mouseleave to control pause and playback:

stop() {
    window.clearInterval(this.timer)
    this.timer = null
}
Copy the code

Seven, two small pits

1. window.onblurandwindow.onfocus

So here, the basic functions are pretty much there. However, if the page is switched to another page, the page on which the rotation map is located will lose focus. After a period of time, it will be found that the rotation is crazy. The reason is that setInterval stops running when the page is out of focus, but if it cuts back it will run all of the time. The solution is also very simple, when the page out of focus to stop the rotation, the page focus to start the rotation.

window.onblur = function() { this.stop() }.bind(this)
window.onfocus = function() { this.play() }.bind(this)
Copy the code

2. window.setInterval()A small pit

When the timer window.setInterval() is used in multiple asynchronous callbacks, it is possible to open multiple execution queues at some point, so to be on the safe side, clear the timer not only when it should be cleared, but also before each use.

Props: props props

props: {
    initialSpeed: {
      type: Number,
      default: 30
    },
    initialInterval: {
      type: Number,
      default: 4
    }
},
data() {... speed: this.initialSpeed }, computed:{interval() {
        return this.initialInterval * 1000
    }
}

Copy the code

Then modify it in the appropriate place.

The complete code is as follows:

<template>
  <div id="slider">
    <div class="window" @mouseover="stop" @mouseleave="play">
      <ul class="container" :style="containerStyle">
        <li>
          <img :src="sliders[sliders.length - 1].img" alt="">
        </li>
        <li v-for="(item, index) in sliders" :key="index">
          <img :src="item.img" alt="">
        </li>
        <li>
          <img :src="sliders[0].img" alt="">
        </li>
      </ul>
      <ul class="direction">
        <li class="left" @click="move(600, 1, speed)">
          <svg class="icon" width="30px" height="30.00 px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path fill="#ffffff" d="M481.233 904c8.189 0 16.379-3.124 22.628-9.372 12.496-12.497 12.496-32.759 0-45.256L166.488 512l3373-337.373-c12.496-12.497 12.496-32.758 0-45.255-12.498-12.497-32.758-12.497-45.256 0L -360 360C-12.496 12.497-12.496 32.758 0 45.255l360 360c6.249 6.249 14.439 9.373 22.628 9.373z"  /></svg>          
        </li>
        <li class="right" @click="move(600, -1, speed)">
          <svg class="icon" width="30px" height="30.00 px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path fill="#ffffff" d="M557.179 904c-8.189 0-16.379-3.124-22.628-9.372-12.496-12.496-32.759 0-45.256L871.924 512 534.551 174.627c-12.496-12.497-12.496-32.758 0-45.255 12.498-12.497 32.758-12.497 45.256 0l360 360c12.496 12.497 12.496 32.758 0 45.255-360-360-3.369 9.373-22.629 9.373-22.628 9.373-z"  /></svg>          
        </li>
      </ul>
      <ul class="dots">
        <li v-for="(dot, i) in sliders" :key="i" 
        :class="{dotted: i === (currentIndex-1)}"
        @click = jump(i+1)
        >
        </li>
      </ul>
    </div>
  </div>
</template>

<script>
export default {
  name: 'slider',
  props: {
    initialSpeed: {
      type: Number,
      default: 30
    },
    initialInterval: {
      type: Number,
      default: 4
    }
  },
  data () {
    return {
      sliders:[
        {
          img:'.. /.. /static/images/1.jpg'
        },
        {
          img:'.. /.. /static/images/2.jpg'
        },
        {
          img:'.. /.. /static/images/3.jpg'
        },
        {
          img:'.. /.. /static/images/4.jpg'
        },
        {
          img:'.. /.. /static/images/5.jpg'
        }
      ],
      currentIndex:1,
      distance:-600,
      transitionEnd: true,
      speed: this.initialSpeed
    }
  },
  computed:{
    containerStyle() {
      return {
        transform:`translate3d(${this.distance}px, 0, 0)`
      }
    },
    interval() {
      return this.initialInterval * 1000
    }
  },
  mounted() {
    this.init()
  },
  methods:{
    init() {
      this.play()
      window.onblur = function() { this.stop() }.bind(this)
      window.onfocus = function() { this.play() }.bind(this)
    },
    move(offset, direction, speed) {
      if(! this.transitionEnd)return
      this.transitionEnd = false
      direction === -1 ? this.currentIndex += offset/600 : this.currentIndex -= offset/600
      if (this.currentIndex > 5) this.currentIndex = 1
      if (this.currentIndex < 1) this.currentIndex = 5

      const destination = this.distance + offset * direction
      this.animate(destination, direction, speed)
    },
    animate(des, direc, speed) {
      if (this.temp) { 
        window.clearInterval(this.temp)
        this.temp = null 
      }
      this.temp = window.setInterval(() => {
        if ((direc === -1 && des < this.distance) || (direc === 1 && des > this.distance)) {
          this.distance += speed * direc
        } else {
          this.transitionEnd = true
          window.clearInterval(this.temp)
          this.distance = des
          if (des < -3000) this.distance = -600
          if(des > -600) this.distance = -3000 } }, 20) }, jump(index) { const direction = index - this.currentIndex >= 0 ? 1: 1 const offset = Math.abs(index - this.currentIndex) * 600 const jumpSpeed = Math.abs(index - this.currentIndex) === 0 ?  this.speed : Math.abs(index - this.currentIndex) * this.speed this.move(offset, direction, jumpSpeed) },play() {
      if (this.timer) {
        window.clearInterval(this.timer)
        this.timer = null
      }
      this.timer = window.setInterval(() => {
        this.move(600, -1, this.speed)
      }, this.interval)
    },
    stop() {
      window.clearInterval(this.timer)
      this.timer = null
    }
  }
}
</script>
Copy the code

Making the address

Nine, epilogue

This. Distance and this.currentIndex are highly coupled and can be linked together by calculating attributes. There is a transition, timer method is still some stiff, did not play the advantages of Vue. However, the first component was written with some effort.

This is my fourth post on the Nuggets, thanks for reading!