Skip to content

Commit 05f54ca

Browse files
committed
Add some-notes-on-vnodes.md
1 parent f0906e3 commit 05f54ca

File tree

2 files changed

+191
-2
lines changed

2 files changed

+191
-2
lines changed

docs/.vitepress/config.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { defineConfigWithTheme } from 'vitepress'
2-
import path from 'path'
2+
import { resolve } from 'node:path'
33

44
export default defineConfigWithTheme({
55
base: '/vue-vnode-utils',
@@ -10,7 +10,7 @@ export default defineConfigWithTheme({
1010
vite: {
1111
resolve: {
1212
alias: {
13-
'@skirtle/vue-vnode-utils': path.resolve(__dirname, '../../src/vue-vnode-utils.ts')
13+
'@skirtle/vue-vnode-utils': resolve(__dirname, '../../src/vue-vnode-utils.ts')
1414
}
1515
}
1616
},
@@ -64,6 +64,14 @@ export default defineConfigWithTheme({
6464
link: '/api.html'
6565
}
6666
]
67+
}, {
68+
text: 'Appendices',
69+
items: [
70+
{
71+
text: 'Some notes on VNodes',
72+
link: '/guide/some-notes-on-vnodes.md'
73+
}
74+
]
6775
}
6876
]
6977
}

docs/guide/some-notes-on-vnodes.md

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
# Some notes on VNodes
2+
3+
This page is not specifically about `vue-vnode-utils`, but describes some aspects of how VNodes are used, in particular with the template compiler. Understanding this helps to appreciate why `vue-vnode-utils` does what it does and the problems it attempts to solve. Some prior experience with `render()` functions and VNodes is assumed:
4+
5+
* <https://vuejs.org/guide/extras/rendering-mechanism.html>
6+
* <https://vuejs.org/guide/extras/render-function.html>
7+
8+
One of the least-well understood features of Vue is the `key` attribute. But this feature isn't unique to Vue. Any framework that uses a VDOM-based approach will need something equivalent to `key`, so in some ways it's strange that it isn't more widely understood. In summary, the `key` is used during rendering updates to determine how to pair up the VNodes from the old and new rendering trees.
9+
10+
But even if you do understand the `key` attribute, it can be very easy to overlook cases where it's needed. Consider this template:
11+
12+
```vue
13+
<template>
14+
<div v-if="x">A</div>
15+
<div>B</div>
16+
</template>
17+
```
18+
19+
Does this code need a `key` to handle the case where the `v-if` value changes and shows or hides the first `<div>`? Could the patching algorithm get confused about which `<div>` is which? The answer is *no*, but only because Vue's template compiler does some clever trickery to save us. Before we try to understand that trickery, let's take a step back and first try to understand why the trickery is needed in the first place.
20+
21+
Let's consider how we might write that same code as a `render()` function, as that makes the VNodes much more obvious:
22+
23+
```js
24+
import { h } from 'vue'
25+
26+
export default {
27+
render() {
28+
const nodes = []
29+
30+
if (this.x) {
31+
nodes.push(h('div', 'A'))
32+
}
33+
34+
nodes.push(h('div', 'B'))
35+
36+
return nodes
37+
},
38+
// ...
39+
}
40+
```
41+
42+
If `x` is `true` this code will create two `<div>` VNodes:
43+
44+
```
45+
#
46+
+- <div>
47+
| +- 'A'
48+
|
49+
+- <div>
50+
+- 'B'
51+
```
52+
53+
If `x` is `false` we'll just get one `<div>`:
54+
55+
```
56+
#
57+
+- <div>
58+
+- 'B'
59+
```
60+
61+
If the value of `x` changes and the component re-renders, we want the VDOM patching algorithm to pair up the `<div>` nodes that have child `'B'`, leaving them unchanged, and just insert or remove the other `<div>`. But it isn't so obvious to the algorithm that this is what we want. It doesn't check the children, it just checks the type and `key`. There isn't a `key`, so it just pairs up the first two `<div>` nodes. This leads to the `'A'` in the first tree being paired up with the `'B'` in the second tree.
62+
63+
We can give the patching algorithm the hint it needs using a `key`:
64+
65+
```js
66+
import { h } from 'vue'
67+
68+
export default {
69+
render() {
70+
const nodes = []
71+
72+
if (this.x) {
73+
nodes.push(h('div', { key: 'A' }, 'A'))
74+
}
75+
76+
nodes.push(h('div', { key: 'B' }, 'B'))
77+
78+
return nodes
79+
},
80+
// ...
81+
}
82+
```
83+
84+
Now nodes can be paired up using the `key` values, rather than relying on their positions.
85+
86+
So what is the trickery the template compiler uses to avoid this problem?
87+
88+
It attacks the problem in two ways. Firstly, it automatically adds a `key` to the `<div v-if>`. It'll do the same for `v-else-if` and `v-else` too, each getting a different `key` value, ensuring that different branches of the same conditional don't get paired up. For cases where there is no `v-else`, like in our previous example, it'll then use the second trick: rendering a comment node when the `v-if` is `false`. This extra VNode ensures that the number of sibling VNodes always stays fixed, irrespective of whether the `v-if` condition is `true` or `false`. This makes pairing them up much easier.
89+
90+
But what about `v-for`? Won't that also change the number of siblings?
91+
92+
Yes and no. Again, brace yourself for trickery.
93+
94+
Let's consider a specific example:
95+
96+
```vue
97+
<template>
98+
<img v-for="item in upper" :src="item">
99+
<img src="separator.png">
100+
<img v-for="item in lower" :src="item">
101+
</template>
102+
```
103+
104+
To human eyes it might appear obvious that the `<img src="separator">` should always be paired up with itself, but a naive implementation of the template compiler wouldn't necessarily give us that. If the `render` function just churned out a load of `<img>` nodes then it wouldn't be possible to tell which node that is.
105+
106+
The trick here doesn't use a `key`, instead it uses *fragments*. Fragments are special VNodes that don't render anything themselves, they just hold child nodes. The tree of VNodes will end up looking something like this:
107+
108+
```
109+
#
110+
+- #fragment
111+
| +- <img>
112+
| +- <img>
113+
| ...
114+
|
115+
+- <img src="separator.png">
116+
|
117+
+- #fragment
118+
| +- <img>
119+
| +- <img>
120+
| ...
121+
```
122+
123+
So we end up with 3 siblings, a fragment for the first `v-for`, an element node for `<img src="separator.png">`, and another fragment for the second `v-for`. The number of children within the fragments can vary, and we would need to add `key` attributes to ensure those image nodes get paired up correctly, but that pain is confined to the fragments. The `<img src="separator.png">` is always the second of three nodes, so the pairing process won't struggle to pair it up correctly, even without any `key` attributes.
124+
125+
Happy? Not really? Good news! It gets worse.
126+
127+
A single `v-for` can lead to two levels of fragments. This arises when using `v-for` on a `<template>` tag:
128+
129+
```vue
130+
<template>
131+
<template v-for="item in list">
132+
<img :src="item">
133+
<hr>
134+
</template>
135+
</template>
136+
```
137+
138+
The VNode tree will look something like this:
139+
140+
```
141+
#
142+
+- #fragment
143+
+- #fragment
144+
| +- <img>
145+
| +- <hr>
146+
|
147+
+- #fragment
148+
| +- <img>
149+
| +- <hr>
150+
...
151+
```
152+
153+
Each iteration gets its own fragment. Why? Again, its to give the pairing process some hints based on the template structure. Each of the inner fragments forms a fixed-length unit, making pairing up those nodes relatively simple, so long as we can pair up the fragment nodes correctly. As with any other `v-for`, pairing up those nodes correctly may require a `key`. Here the inner fragment nodes give us a place to store that `key`. For example, if we have a `key` in our template like this:
154+
155+
```vue
156+
<template>
157+
<template v-for="item in list" :key="item">
158+
<img :src="item">
159+
<hr>
160+
</template>
161+
</template>
162+
```
163+
164+
The `key` on the `<template>` tag will become a prop of the fragment nodes:
165+
166+
```
167+
#
168+
+- #fragment
169+
+- #fragment - key="a.png"
170+
| +- <img>
171+
| +- <hr>
172+
|
173+
+- #fragment - key="b.png"
174+
| +- <img>
175+
| +- <hr>
176+
...
177+
```
178+
179+
Vue 2 didn't have fragments, so the `key` couldn't be placed on a `<template>` tag. Instead, we had to put a different `key` on each direct child of the `<template v-for>`, which was pretty annoying.
180+
181+
Comment nodes and fragments are a pain if you're trying work with the VNodes returned by a slot. Making that part of the process less painful is the problem `vue-vnode-utils` aims to solve.

0 commit comments

Comments
 (0)