|
| 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