Implementing Fragment
Issues with the current implementation
Let's try running the following code in a playground:
import { createApp, defineComponent } from 'chibivue'
const App = defineComponent({
template: `<header>header</header>
<main>main</main>
<footer>footer</footer>`,
})
const app = createApp(App)
app.mount('#app')
You may encounter an error like this:
Looking at the error message, it seems to be related to the Function constructor.
In other words, the code generation seems to be successful up to a certain point, so let's see what code is actually generated.
return function render(_ctx) {
with (_ctx) {
const { createVNode: _createVNode } = ChibiVue
return _createVNode("header", null, "header")"\n "_createVNode("main", null, "main")"\n "_createVNode("footer", null, "footer")
}
}
The code after the return
statement is incorrect. The current code generation implementation does not handle cases where the root is an array (i.e., not a single node).
We will fix this issue.
What code should be generated?
Even though we are making modifications, what kind of code should be generated?
In conclusion, the code should look like this:
return function render(_ctx) {
with (_ctx) {
const { createVNode: _createVNode, Fragment: _Fragment } = ChibiVue
return _createVNode(_Fragment, null, [
[
_createVNode('header', null, 'header'),
'\n ',
_createVNode('main', null, 'main'),
'\n ',
_createVNode('footer', null, 'footer'),
],
])
}
}
This Fragment
is a symbol defined in Vue.
In other words, Fragment is not represented as an AST like FragmentNode, but simply as a tag of ElementNode.
We will implement the processing for Fragment in the renderer, similar to Text.
Implementation
The Fragment symbol will be implemented in runtime-core/vnode.ts.
Let's add it as a new type in VNodeTypes.
export type VNodeTypes = Component | typeof Text | typeof Fragment | string
export const Fragment = Symbol()
Implement the renderer.
Add a branch for fragment in the patch function.
if (type === Text) {
processText(n1, n2, container, anchor)
} else if (shapeFlag & ShapeFlags.ELEMENT) {
processElement(n1, n2, container, anchor, parentComponent)
} else if (type === Fragment) {
// Here
processFragment(n1, n2, container, anchor, parentComponent)
} else if (shapeFlag & ShapeFlags.COMPONENT) {
processComponent(n1, n2, container, anchor, parentComponent)
} else {
// do nothing
}
Note that inserting or removing elements should generally be implemented with anchor as a marker.
As the name suggests, an anchor indicates the start and end positions of a fragment.
The starting element is represented by the existing el
property in VNode, but currently there is no property to represent the end. Let's add it.
export interface VNode<HostNode = any> {
// .
// .
// .
anchor: HostNode | null // fragment anchor // Added
// .
// .
}
Set the anchor during mount.
Pass the fragment's end as the anchor in mount/patch.
const processFragment = (
n1: VNode | null,
n2: VNode,
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
) => {
const fragmentStartAnchor = (n2.el = n1 ? n1.el : hostCreateText(''))!
const fragmentEndAnchor = (n2.anchor = n1 ? n1.anchor : hostCreateText(''))!
if (n1 == null) {
hostInsert(fragmentStartAnchor, container, anchor)
hostInsert(fragmentEndAnchor, container, anchor)
mountChildren(
n2.children as VNode[],
container,
fragmentEndAnchor,
parentComponent,
)
} else {
patchChildren(n1, n2, container, fragmentEndAnchor, parentComponent)
}
}
Be careful when the elements of the fragment change during updates.
const move = (
vnode: VNode,
container: RendererElement,
anchor: RendererElement | null,
) => {
const { type, children, el, shapeFlag } = vnode
// .
if (type === Fragment) {
hostInsert(el!, container, anchor)
for (let i = 0; i < (children as VNode[]).length; i++) {
move((children as VNode[])[i], container, anchor)
}
hostInsert(vnode.anchor!, container, anchor) // Insert the anchor
return
}
// .
// .
// .
}
During unmount, also rely on the anchor to remove elements.
const remove = (vnode: VNode) => {
const { el, type, anchor } = vnode
if (type === Fragment) {
removeFragment(el!, anchor!)
}
// .
// .
// .
}
const removeFragment = (cur: RendererNode, end: RendererNode) => {
let next
while (cur !== end) {
next = hostNextSibling(cur)! // ※ Add this to nodeOps!
hostRemove(cur)
cur = next
}
hostRemove(end)
}
Testing
The code we wrote earlier should work correctly.
import { Fragment, createApp, defineComponent, h, ref } from 'chibivue'
const App = defineComponent({
template: `<header>header</header>
<main>main</main>
<footer>footer</footer>`,
})
const app = createApp(App)
app.mount('#app')
Currently, we cannot use directives like v-for, so we cannot write a description that uses a fragment in the template and changes the number of elements.
Let's simulate the behavior by writing the compiled code and see how it works.
import { Fragment, createApp, defineComponent, h, ref } from 'chibivue'
// const App = defineComponent({
// template: `<header>header</header>
// <main>main</main>
// <footer>footer</footer>`,
// });
const App = defineComponent({
setup() {
const list = ref([0])
const update = () => {
list.value = [...list.value, list.value.length]
}
return () =>
h(Fragment, {}, [
h('button', { onClick: update }, 'update'),
...list.value.map(i => h('div', {}, i)),
])
},
})
const app = createApp(App)
app.mount('#app')
It seems to be working correctly!
Source code up to this point: GitHub