- home page
- special column
- javascript
- Article details
Four implementation ideas of round robin components from one-time service requirements
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
- 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.
- Detect when a value changes: call the set handler on the proxy.
- 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
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
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
The original qdfunds is closed and can only start again in another community. I wish each other well
0 comments
The original qdfunds is closed and can only start again in another community. I wish each other well