Four implementation ideas of round robin components from one-time service requirements

  1. home page
  2. special column
  3. javascript
  4. Article details
0

Four implementation ideas of round robin components from one-time service requirements

Afterward Published 5 minutes ago

Requirement prototype

Suppose there is a column of data with unknown quantity and length, and you want to make a rotation display in a container. The basic structure is as follows

<Carousel>
    <ul>
      <li
        v-for="item in 10"
        :key="item"
      >item</li>
    </ul>
</Carousel>

What we need to implement is the < carousel > component

Implementation idea I

We use the translateX of css3 to translate and slide, and reset it to the invisible place on the right side of the container at the moment when the list moves out of the container

The first step is to complete the layout

Determine the basic structure of the component

<div class="carousel">
  <div class="carousel-wrap" ref="wrapRef">
    <div
      ref="contentRef"
      class="carousel-content"
      :style="style"
    >
      <slot></slot>
    </div>
  </div>
</div>

Set basic style structure

.carousel {
  position: relative;
  display: flex;
  align-items: center;
  &-wrap {
    overflow: hidden;
    position: relative;
    display: flex;
    flex: 1;
    align-items: center;
    height: 100%;
  }
  &-content {
    transition-timing-function: linear;
  }
}

This completes the overall layout. See view 1 above for details. The next question is how to make the elements move

The second step is to animate

Set basic parameters

const state = reactive({
  offset: 0,
  duration: 0
})

Main animation implementation methods

// Style control
const getStyle = (data) => {
  return {
    transform: data.offset ? `translateX(${data.offset}px)` : '',
    transitionDuration: `${data.duration}s`,
  }
}
// Rotation style control
const style = computed(() => getStyle(state))

The third step is to calculate the animation logic

First of all, you must obtain specific elements and calculate parameters

// Container width
let wrapWidth = 0
// Content width
let contentWidth = 0
let startTime = null
const wrapRef = ref(null)
const contentRef = ref(null)

// Start computing logic after the element is mounted
onMounted(reset)

General exposure to external options

const props = defineProps({
  show: {
    type: Boolean,
    default: true,
  },
  speed: {
    type: [Number, String],
    default: 30,
  },
  delay: {
    type: [Number, String],
    default: 0,
  },
})

Initial mount calculation logic

const reset = () => {
  // reset parameters 
  wrapWidth = 0
  contentWidth = 0
  state.offset = 0
  state.duration = 0
  clearTimeout(startTime)
  
  startTime = setTimeout(() => {
    // Intercept DOM non rendered phases
    if (!wrapRef.value || !contentRef.value) return

    const wrapRefWidth = useRect(wrapRef).width
    const contentRefWidth = useRect(contentRef).width

    // Content width exceeds container width before running
    if (contentRefWidth > wrapRefWidth) {
      wrapWidth = wrapRefWidth;
      contentWidth = contentRefWidth;
      // Repeat call
      doubleRaf(() => {
        state.offset = -contentWidth / 2;
        state.duration = -state.offset / +props.speed;
      })
    }
  }, props.delay);
}

This code actually has several points to think about

What does the doubleRaf function do

This is the complete function code. You can see that it only executes the callback when it is redrawn for the second time

export function raf(fn) {
  return requestAnimationFrame(fn)
}

export function doubleRaf(fn) {
  raf(() => raf(fn));
}

Let's review what the requestAnimationFrame does

window.requestAnimationFrame() tells the browser that you want to execute an animation and ask the browser to update the animation before calling the specified callback function before next redraw.

The number of callback function executions is usually 60 times per second, but in most browsers that follow W3C recommendations, the number of callback function executions usually matches the number of browser screen refreshes. In order to improve performance and battery life, in most browsers, when requestAnimationFrame() runs in the Background tab or hidden iframe, requestAnimationFrame() will be suspended to improve performance and battery life.

In general, it will provide the best and fastest execution time according to the browser, and will be automatically called temporarily in the invisible running mode

This raises the second question

Why wait until the second redraw to execute the callback

This usage is actually what I saw when I learned the source code of vantUI library. Their code comments say so

// use double raf to ensure animation can start

The PC simulator has a trial run. It is OK to directly execute the callback in the next redraw, but there will be write factors in the actual operation. Suppose that in the simplest mode, we want an element displacement to be

translateX(0) -> translateX(1000px) -> translateX(500px)

If the code is as follows

box.style.transform = 'translateX(1000px)'
requestAnimationFrame(() => {
  box.style.transform = 'translateX(500px)'
})

Its execution order will be. For specific reasons, recall the role of requestanimation frame

translateX(0) -> translateX(500px)

Now that you know the problem, the answer comes out

Step 4: repeat the animation

As shown in Figure 2 above, the moment the animation ends, it is reset to the invisible position on the right side of the container,

First, we use CSS3 to realize the whole animation, directly set the offset value, and then form the animation through the transition effect

const getStyle = (data) => {
  return {
    transform: data.offset ? `translateX(${data.offset}px)` : '',
    transitionDuration: `${data.duration}s`,
  }
}

Since we know that CSS3 transition animation is implemented, we can use transition and monitor

The transitionend event is triggered after the CSS completes the transition.

Note: if the transition is removed before completion, for example, the CSS transition property property property is removed, the transition event will not be triggered.

const onTransitionEnd = () => {
  state.offset = wrapWidth
  state.duration = 0

  raf(() => {
    // use double raf to ensure animation can start
    doubleRaf(() => {
      state.offset = -contentWidth;
      state.duration = (contentWidth + wrapWidth) / +props.speed;
    });
  });
}

You can see that after filtering, the attribute will be reset immediately, and then the status will be updated

Why embed another layer of raf callback

Review Vue3's responsive principle and its key steps

  1. Track when a value is read: the track function in the get processing function of the proxy records the property and the current side effects.
  2. Detect when a value changes: call the set handler on the proxy.
  3. Rerun the code to read the original value: the trigger function looks for which side effects depend on the property and executes them.

The template of a component is compiled into a render function. The rendering function creates VNodes to describe how the component should be rendered. It is wrapped in a side effect that allows Vue to track "touched" properties at run time.

A render function is conceptually very similar to a computed property. Vue does not exactly track how dependencies are used, it only knows that these dependencies are used at a certain point in time when the function runs. If any of these properties subsequently changes, it will trigger a side effect to run again, rerunning the render function to generate new VNodes. These actions are then used to make the necessary changes to the DOM.

Vue executes asynchronously when updating the DOM. When data changes, Vue will open an asynchronous update queue. The view needs to be updated uniformly after all data changes in the queue are completed

So theoretically, considering the performance loss, we should update the animation in the next queue. Vue provides a global APInextTick

Postpone the callback until after the next DOM update cycle. Use it immediately after you change some data to wait for the DOM to update.

Why not nest a layer of raf instead of nextTick? There are relevant comments from the vant source code

// wait for Vue to render offset
// using nextTick won't work in iOS14

Insufficient

The whole code implementation idea has been completed, but this writing method will have an obvious deficiency. The whole animation can only be integrated in and out, and there will be a blank period of elements in the middle of the interface waiting for the animation to enter

Therefore, it needs to be expanded to consider how to achieve the effect of seamless rotation

Realization idea 2

We directly copy two identical elements, and then use delayed execution to achieve a seamless connection effect

The first step is to complete the layout

<div
  ref="contentRef"
  class="carousel-content"
  :style="style1"
  @transitionend="onTransitionEnd"
>
  <slot></slot>
</div>

<div
  class="carousel-content"
  :style="style2"
  @transitionend="onTransitionEnd"
>
  <slot></slot>
</div>

Change to absolute positioning

.carousel {
  position: relative;
  display: flex;
  align-items: center;
  &-wrap {
    overflow: hidden;
    position: relative;
    display: flex;
    flex: 1;
    align-items: center;
    height: 100%;
  }
  &-content {
    position: absolute;
    display: flex;
    white-space: nowrap;
    transition-timing-function: linear;
  }
}

The second step is to animate

Copy the basic variables of idea 1 in two copies, so omit the code

The third step is to calculate the animation logic

This is the key code core

const reset = () => {
    ...ellipsis...
    // Content width exceeds container width before running
    if (contentRefWidth > wrapRefWidth) {
      wrapWidth = wrapRefWidth
      contentWidth = contentRefWidth;
      
      // The actual animation properties are the same
      const offset = -(contentWidth + wrapWidth)
      const duration = -offset / +props.speed
      
      // Element 1 animation
      doubleRaf(() => {
        state1.offset = offset;
        state1.duration = duration;
      })
      
      // Element 2 animation
      setTimeout(() => {
        doubleRaf(() => {
          state2.offset = offset;
          state2.duration = duration;
        })
      }, contentWidth / +props.speed * 1000); // Join behind element 1 and start sliding
    }
    ...ellipsis...
}

Offset value calculation method

Because the distance from the right side of the container to the left side of the container is actually equal to the width of the container + the width of the element, the offset duration can be known if the speed is constant

const offset = -(contentWidth + wrapWidth)
const duration = -offset / +props.speed

Delay calculation method

In order to achieve seamless connection, element 2 needs to start sliding when element 1 just does not coincide, that is, when the element offset distance is equal to its own width

contentWidth / +props.speed * 1000

Step 4: repeat the animation

At this time, it can be found that element 1 and element 2 are repeating the same animation, so they share the same event

const onTransitionEnd = () => {
  state.offset = 0
  state.duration = 0

  raf(() => {
    doubleRaf(() => {
      state.offset = -(contentWidth + wrapWidth);
      state.duration = -state.offset / +props.speed;
    });
  });
}

Insufficient

This writing method is a simple copy of element splicing to achieve the effect of sliding in turn, but it also has some obvious disadvantages

  1. The code and visual effect are not necessarily unified. If the layout interval in the element is unequal or asymmetric, it will be obvious that two different elements are merged. In fact, this is an unavoidable problem. We can only standardize the element style

  2. Deviation of calculated value

    Because it involves the pixel, offset position, transition time and timer of elements, some numerical calculations with decimals will cause obvious asynchrony, which is specifically reflected in the asynchrony of sliding speed, excessive interval or partial coincidence between elements, etc,

Second, I have no idea for the time being, so I give up this writing method later

Realization idea 3

In fact, it is similar to idea 2, except that the natural offset is not used for absolute positioning, and the elements already appear in the view at the beginning

The first step is to complete the layout

No absolute positioning

The second step is to animate

Copy the basic variables of idea 1 in two copies, so omit the code

The third step is to calculate the animation logic

This is the key code core

const reset = () => {
  // reset parameters 
  wrapWidth = 0
  contentWidth = 0
  state1.offset = 0
  state1.duration = 0
  state2.offset = 0
  state2.duration = 0
  clearTimeout(startTime)
  startTime = setTimeout(() => {
    // Intercept DOM non rendered phases
    if (!wrapRef.value || !contentRef.value) return

    const wrapRefWidth = useRect(wrapRef).width
    const contentRefWidth = useRect(contentRef).width

    // Content width exceeds container width before running
    if (contentRefWidth > wrapRefWidth) {
      wrapWidth = wrapRefWidth
      contentWidth = contentRefWidth;
      doubleRaf(() => {
        state1.offset = -contentWidth;
        state1.duration = -state1.offset / +props.speed;
        state2.offset = -contentWidth * 2;
        state2.duration = -state2.offset / +props.speed;
      })
    }
  }, props.delay);
}

Element 1 offset calculation

Because it is already in the view, you only need to offset its width

state1.offset = -contentWidth;
state1.duration = -state1.offset / +props.speed;

Element 2 offset calculation

Because it is connected behind element 1, the initial offset distance is equal to the combination of the two

state2.offset = -contentWidth * 2;
state2.duration = -state2.offset / +props.speed;

Step 4: repeat the animation

Compared with the three-phase method, because the absolute positioning is not used, the offset value needs to be calculated when resetting, and the two elements also need to be calculated separately

Because the initial offset value of element 1 is 0, but it needs to be positioned to the initial position of element 2 after reset, so

const onTransitionEnd1 = () => {
  state1.offset = contentWidth * 2 - wrapWidth
  state1.duration = 0

  raf(() => {
    doubleRaf(() => {
      state1.offset = -contentWidth * 2;
      state1.duration = -state1.offset / +props.speed;
    });
  });
}

As for element 2, the initial position does not need to be changed, and the offset value does not need to be changed

const onTransitionEnd2 = () => {
  state2.offset = 0
  state2.duration = 0

  raf(() => {
    doubleRaf(() => {
      state2.offset = -contentWidth * 2;
      state2.duration = -state2.offset / +props.speed;
    });
  });
}

Insufficient

I still have the problem of idea 2, so I gave up

Realization idea 4

This is a less rigorous way of writing, but vue3 seamless scroll uses the same method internally

Directly treat the two elements as a whole and roll back the position at the moment when the offset value reaches half of its own width

The first step is to complete the layout

<div
  ref="contentRef"
  class="carousel-content"
  :style="style"
  @transitionend="onTransitionEnd"
>
  <slot></slot>
  <slot></slot>
</div>

The second step is to animate

Copy the basic variables of idea 1 in two copies, so omit the code

The third step is to calculate the animation logic

This is the key code core. Because the element width is calculated together, the offset value needs to be divided by two

const reset = () => {
    ...ellipsis...
      doubleRaf(() => {
        state.offset = -contentWidth / 2;
        state.duration = -state.offset / +props.speed;
      }
    ...ellipsis...
}

Step 4: repeat the animation

It just keeps repeating and doesn't need to be changed

const onTransitionEnd = () => {
  state.offset = 0
  state.duration = 0

  raf(() => {
    doubleRaf(() => {
      state.offset = -contentWidth / 2;
      state.duration = -state.offset / +props.speed;
    });
  });
}

Insufficient

Because it is always self offset, there is no problem 2 deviation above, but there will be a pause at the moment of reset. In addition, it is very smooth, so finally, a Carousel component is implemented with this scheme

Reading 8 was published 5 minutes ago
Like collection

The original qdfunds is closed and can only start again in another community. I wish each other well

473 prestige
39 fans
Focus on the author
Submit comments
You know what?

Register login

The original qdfunds is closed and can only start again in another community. I wish each other well

473 prestige
39 fans
Focus on the author
Article catalog
follow
Billboard

Keywords: Javascript Front-end html5 Vue.js css

Added by DangerousDave86 on Fri, 29 Oct 2021 14:28:17 +0300