As you know, Vue’s support for animation comes with a built-in native component called Transition
Shuffle: Shuffle: Shuffle: Shuffle: Shuffle: Shuffle: Shuffle: Shuffle: Shuffle: Shuffle: Shuffle: Shuffle In fact, the Vue website also gave a hint – FLIP
FLIP thought
Invert-play FLIP invert-play FLIP Invert-play FLIP Invert-play FLIP Invert-play FLIP invert-play
- F: First, as the name implies, is the starting position. We need to record the starting position of the node before the animation starts
- L: Last stands for where we end. We also need to record where we end so that our trajectory can be calculated from where we end
- I: Invert means regression. We return the position of the current node to the original position so that it looks like we are moving from the beginning to the end
- P: Play means Play, that is, start from the beginning to the end
Vue official website animation analysis
We use this FLIP idea to analyze how Vue official website animation is achieved
Before we parse the animation, we need to understand one concept
- So instead of getting DOM properties from RenderTree, we get DOM properties from DomTree. What does that mean?
this.$nextTick(() = > {
console.log(latest DOM)})Copy the code
We know that the updated DOM is usually obtained in nextTick (note that the nextTick callback function needs to be placed after the dependency update code to obtain the updated DOM), if you are not familiar with the internal workings of nextTick, please take a lookYou don’t know about nextTick
With these concepts in mind, we can get down to business. Look at the code below
new Vue({
el: '#root'.template: `
{{brand}}
`.data() {
return {
brands: [...Array(81).keys()]
}
},
methods: {
shuffle() {
this.$refs.brands.forEach(brand= > {
const { left, top } = brand.getBoundingClientRect()
brand.dataset.left = left
brand.dataset.top = top
brand.style.transition = 'unset'
})
this.brands.sort((a, b) = > Math.random() > . 5 ? -1 : 1)
this.$nextTick().then(() = > {
this.$refs.brands.forEach(brand= > {
const { left, top } = brand.getBoundingClientRect()
const { left: oldLeft, top: oldTop } = brand.dataset
brand.style.transform = `translate3d(${oldLeft - left}px, ${oldTop - top}px, 0)`
})
})
requestAnimationFrame(() = > {
this.$refs.brands.forEach(brand= > {
const { left, top } = brand.getBoundingClientRect()
brand.style.transform = `translate3d(0, 0, 0)`
brand.style.transition = 'all .3s'})})})Copy the code
Results show
- We can see that we first define a 9 by 9 grid
- The source data is then sorted irregularly when we click on the button
- Here is the point. We first get the left and top values of each element before sorting (F in FLIP). Then we get the updated DOM position (L in FILP) in $nextTick, and then we revert the DOM position back to the original position (I in FLIP), which is translate3D (0, 0, 0). We just need to change the final state to 0,0,0 to achieve the effect of moving from start to finish position (P in FLIP), but there is an important rendering basics in the last step, We can see that the Play step is executed in the requestAnimationFrame, so let’s focus on why we need to execute it in this function
Browser Rendering optimization
Let’s take a look at this code
for (var i = 0; i < 100; i++) {
document.body.style.background = 'red'
}
Copy the code
The above code does not actually render 100 times, we know that a browser frame contains the following steps
- JavaScript: JavaScript implements animation effects, DOM element manipulation, etc.
- Style (computed Style) : Determines what CSS rules should be applied to each DOM element.
- Layout: Calculates the size and position of each DOM element on the final screen. Since the layout of elements on a Web page is relative, changes in the position of any element will cause changes in other elements. This process is called reflow. (One render layer per DOM)
- Paint: Paint the text, colors, images, borders, and shadows of DOM elements on multiple layers. This process is called repaint.
- Composite: When each DOM is on the same layer, RenderLayers are combined in the proper order and displayed on the screen.
We loop a hundred times and it actually only needs the last state bit for the browser, and the browser pushes the javascript action into a render queue, and when it’s done it pulls out the render queue and renders, The code in requestAnimationFrame will fire before the layout and will not enter the render queue, the browser will render from the last state bit in the render queue as the starting point, the requestAnimationFrame will operate on the layout, will trigger the second render, This is equivalent to a state bit of the second browser rendering (the previous render queue can be regarded as the first rendering), which is the state bit of the last state of the first rendering to the requestAnimationFrame change, which needs to be refined
Of course, not only requestAnimationFrame can achieve this effect, we know that it is essentially the last state of the first render to the state of the second render, so we just need to trigger the render queue update immediately, The next render operation is the state of the second render, which triggers the immediate render operation as follows:
- OffsetTop, offsetLeft, offsetWidth, offsetHeight
- ScrollTop, scrollLeft, scrollWidth, scrollHeight
- ClientTop, clientLeft, clientWidth, clientHeight
- getComputedStyle()
- getBoundingClientRect
Attach a non-VUE version of the code
<! DOCTYPEhtml>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="Width = device - width, initial - scale = 1.0">
<title>Document</title>
<style>
* {
margin: 0;
height: 0;
}
.container {
margin: 50px auto;
width: 450px;
height: 450px;
display: flex;
flex-wrap: wrap;
}
.c-random__brand {
width: 50px;
height: 50px;
text-align: center;
box-sizing: border-box;
line-height: 50px;
border: 1px solid # 000;
}
button {
padding: 20px;
}
</style>
</head>
<body>
<div class="container">
</div>
<button onclick="shuffle()">shuffle</button>
<script>
const container = document.getElementsByClassName('container') [0]
function createBrands() {
return [...Array(81).keys()].map(brand= > {
const div = document.createElement('div')
div.innerHTML = brand
div.className = 'c-random__brand'
return div
})
}
function calculatePlace() {
brands.forEach(brand= > {
const { left, top } = brand.getBoundingClientRect()
brand.dataset.left = left
brand.dataset.top = top
brand.style.transition = 'unset'
})
brands.sort((a, b) = > Math.random() > . 5 ? -1 : 1)
Promise.resolve().then(() = > {
brands.forEach(brand= > {
const { left, top } = brand.getBoundingClientRect()
const { left: oldLeft, top: oldTop } = brand.dataset
brand.style.transform = `translate3d(${oldLeft - left}px, ${oldTop - top}px, 0)`})})}function patchBrandDom() {
brands.map(container.appendChild.bind(container))
requestAnimationFrame(() = > {
brands.forEach(brand= > {
const { left, top } = brand.getBoundingClientRect()
brand.style.transform = `translate3d(0, 0, 0)`
brand.style.transition = 'all 1s'})})}function shuffle() {
calculatePlace()
patchBrandDom()
}
const brands = createBrands()
shuffle()
</script>
</body>
</html>
Copy the code
The same DOM node is not appendChild twice
conclusion
Never dabble, a lot of knowledge can be dug deep ~