Welcome to follow my personal wechat official account “HcySunYang”. Let’s coding for fun together!
Vue3’s Compiler works closely with the Runtime to take full advantage of compile-time information, resulting in significant performance improvements. The purpose of this article is to tell you what optimizations are made in Vue3’s Compiler, as well as some optimization details that you might want to know. Based on this, we try to summarize a set of high-performance rendering functions for handwriting optimization mode, which can also be used to implement a JSX Babel plug-in for Vue3. To allow JSX to enjoy the run-time benefits of optimized mode, it should be clarified that Vue3 theoretically has better Diff performance than Vue2 even in non-optimized mode. In addition, this paper does not include SSR related optimization, which is expected to be summarized in the next article.
The length is large, it takes a lot of energy to sort out, for the students who do not have too much understanding of Vue3, it may be difficult to read, may wish to collect first, and may be used in the future.
TOC
-
Tree and PatchFlags Block
- traditional
Diff
Problem with the algorithm Block
Cooperate withPatchFlags
Do targeted update- The node is unstable –
Block Tree
v-if
As the element ofBlock
v-for
As the element ofBlock
- unstable
Fragment
- The stability of the
Fragment
v-for
The expression of is constant- Multiple root elements
- Socket outlet
<template v-for>
- traditional
-
Static ascension
- Promote the static node tree
- A situation in which elements are not promoted
- Element with dynamic
key
The binding - use
ref
The elements of the - Elements that use custom instructions
- Improve the static
PROPS
-
prestringing
-
Cache Event handler
-
v-once
-
Handwriting high performance rendering function
- A couple of little things to remember
Block Tree
Is flexible- Use it correctly
PatchFlags
NEED_PATCH
- The use of
Block
Must be used- Branch judgment use
Block
- A list of use
Block
- Using dynamic
key
The element of should beBlock
- Branch judgment use
- use
Slot hint
- Use correctly for components
DYNAMIC_SLOTS
- use
$stable hint
Tree and PatchFlags Block
Block Tree and PatchFlags are optimizations made by Vue3 to take full advantage of compilation information and in the Diff phase. Uvu has talked about ideas more than once in public, and our goal in going into detail is to better understand and try to write high-performance VNodes by hand.
The problem of traditional Diff algorithm
The “traditional VDOM” Diff algorithm is always traversed layer by layer according to the hierarchical structure of the VDOM tree (if you are not familiar with the various traditional Diff algorithms, you can see my previous article “Renderer”, which summarized the three traditional Diff methods). For example, the following template shows:
<div>
<p>bar</p>
</div>
Copy the code
For a traditional diff algorithm, it diff the vnode(template compiled vnode) :
-
Attribute of the Div tag + children
-
Attribute of the tag (class) + children
-
Text node: bar
But obviously, this is a static VDOM, and it cannot change during the component update phase. If you can skip static content in the diff phase, you can avoid useless vDOM tree traversal and comparison, which is probably the original source of optimization ideas —- skip static content and only compare dynamic content.
Block and PatchFlags achieve targeted update
Let’s talk about blocks first and then talk about Block trees. Now that we have the idea, we just want to compare non-static content, for example:
<div>
<p>foo</p>
<p>{{ bar }}</p>
</div>
Copy the code
In this template, only the text node in
{{bar}}
is dynamic, so you only need to target updates to that text node, which is especially advantageous in scenarios with a lot of static content and a little dynamic content. The question is how? We need to get the ability of dynamic nodes in the entire VDOM tree. In fact, it may not be as complicated as you think. Let’s look at the traditional VDOM tree corresponding to this template.
Const vnode = {tag: 'div', children: [{tag: 'p', children: 'foo'}, {tag: 'p', children: ctx.bar}, // This is a dynamic node]}Copy the code
In a traditional VDOM tree, we don’t get any useful information at run time, but Vue3’s Compiler can analyze the template and extract useful information, which is finally reflected in the VDOM tree. For example, it clearly knows which nodes are dynamic and why. Or is it bound to dynamic style? Or some other dynamic property? In short, the compiler can extract the information we want, and with this information we can mark the dynamic nodes during vNode creation: PatchFlags.
We can simply understand PatchFlags as a digital mark and assign different meanings to these numbers, such as:
- The number 1 indicates that the node is dynamic
textContent
(Such as in the template abovep
The label) - The number 2: indicates that the element is dynamic
class
The binding - The number 3 indicates XXXXX
In summary, we can presuppose these meanings, which are finally reflected in vNode:
const vnode = { tag: 'div', children: [ { tag: 'p', children: 'foo' }, { tag: 'p', children: ctx.bar, patchFlag: 1 /* Dynamic textContent */},]}Copy the code
With this information, we can extract dynamic nodes during vNode creation. What kind of nodes are dynamic nodes? Nodes with patchFlag are dynamic nodes, which are extracted and stored in an array. For example:
const vnode = { tag: 'div', children: [ { tag: 'p', children: 'foo' }, { tag: 'p', children: ctx.bar, patchFlag: 1 /* Dynamic textContent */},], dynamicChildren: [{tag: 'p', children: ctx.bar, patchFlag: 1 /* Dynamic textContent */},]}Copy the code
DynamicChildren is an array that we use to store the dynamic nodes of all the children of a node.
const vnode = {
tag: 'div',
children: [
{ tag: 'section', children: [
{ tag: 'p', children: ctx.bar, patchFlag: 1 /* 动态的 textContent */ },
]},
],
dynamicChildren: [
{ tag: 'p', children: ctx.bar, patchFlag: 1 /* 动态的 textContent */ },
]
}
Copy the code
As shown in vNode above, the DIV node can collect not only the direct dynamic children, but also the dynamic nodes from all the children. Why are div nodes so awesome? Because it has a special role: Block, that’s right, that div node is the legendary Block. A Block is just a VNode, but it has special properties (one of which is dynamicChildren).
Now that we have all the dynamic nodes, they are stored in dynamicChildren, so we can avoid traversing the vDOM tree layer by layer during diff and go directly to dynamicChildren for updates. In addition to skipping useless hierarchy traversal, patchFlag has been added to VNode early, so when updating nodes in dynamicChildren, we can know exactly which update actions need to be applied to the node, which basically realizes targeted update.
Node instability – Block Tree
A Block cannot form a Block Tree, which means that in a VDOM Tree, there will be multiple VNodes acting as blocks to form a Block Tree. When does a VNode act as a block?
Take a look at this template:
<div>
<section v-if="foo">
<p>{{ a }}</p>
</section>
<div v-else>
<p>{{ a }}</p>
</div>
</div>
Copy the code
Assuming that as long as the outermost div tag is a Block role, the dynamic nodes that the Block collects when foo is true are:
cosnt block = {
tag: 'div',
dynamicChildren: [
{ tag: 'p', children: ctx.a, patchFlag: 1 }
]
}
Copy the code
When foo is false, the contents of the block are as follows:
cosnt block = {
tag: 'div',
dynamicChildren: [
{ tag: 'p', children: ctx.a, patchFlag: 1 }
]
}
Copy the code
We can see that the contents of the block remain the same whether foo is true or false, which means nothing is updated during the diff phase, but we can also see that v-if is a
<div> <section v-if="foo"> <p>{{ a }}</p> </section> <section v-else> <! -- even if this is section --> <div> <! - this div tags are ignored in the process of the diff - > < p > {{a}} < / p > < / div > < / section > < / div >Copy the code
Even if the V-else is a
v-if
As the element ofBlock
What if elements that use instructions such as V-if/V-else -if/ V-else are also blocks? Take the following template as an example:
<div> <section v-if="foo"> <p>{{ a }}</p> </section> <section v-else> <! -- even if this is section --> <div> <! - this div tags are ignored in the process of the diff - > < p > {{a}} < / p > < / div > < / section > < / div >Copy the code
If we make both sections as blocks, we form a block tree:
Block(Div)
- Block(Section v-if)
- Block(Section v-else)
Copy the code
The parent Block collects child blocks as well as dynamic nodes, so the two blocks (sections) are dynamicChildren of the Block(div) :
cosnt block = {
tag: 'div',
dynamicChildren: [
{ tag: 'section', { key: 0 }, dynamicChildren: [...]}, /* Block(Section v-if) */
{ tag: 'section', { key: 1 }, dynamicChildren: [...]} /* Block(Section v-else) */
]
}
Copy the code
So if the v-if condition is true, dynamicChildren contains Block(section V-if), and if the condition is false, dynamicChildren contains Block(Section V-else), and in Diff, The renderer knows that these are two different blocks, so it makes a complete substitution, which solves the DOM structure instability problem. And that’s the Block Tree.
The element of V-for is a Block
Not only does v-if destabilize the DOM structure, but v-for does too, but v-for is a little more complicated. Consider the following template:
<div>
<p v-for="item in list">{{ item }}</p>
<i>{{ foo }}</i>
<i>{{ bar }}</i>
</div>
Copy the code
If the list value is changed from [1,2] to [1], the outermost
// const prevBlock = {tag: 'div', dynamicChildren: [{tag: 'p', children: 1, 1 /* TEXT */}, {tag: 'p', children: 2, 1 /* TEXT */ }, { tag: 'i', children: ctx.foo, 1 /* TEXT */ }, { tag: 'i', children: Bar, 1 /* TEXT */},]} // const nextBlock = {tag: 'div', dynamicChildren: [{tag: 'p', children: item, 1 /* TEXT */ }, { tag: 'i', children: ctx.foo, 1 /* TEXT */ }, { tag: 'i', children: ctx.bar, 1 /* TEXT */ }, ] }Copy the code
PrevBlcok has four dynamic nodes, and nextBlock has three. How do you Diff at this point? Some students may say that it is wrong to use dynamicChildren for traditional Diff, because one of the preconditions of traditional Diff is Diff between nodes of the same level, but nodes within dynamicChildren are not necessarily of the same level, which we have mentioned before.
In fact, we just need the v-for element to be a Block as well. So no matter how v-for changes, it will always be a Block, which ensures structural stability. No matter how V -for changes, the Block Tree will look like:
Const block = {tag: 'div', dynamicChildren: [// This is a block with dynamicChildren {tag: Fragment, dynamicChildren: [/ *.. v - for the node.. * /]} {tag: 'I', children: CTX foo, TEXT / * * / 1}, {tag: 'I', the children: ctx.bar, 1 /* TEXT */ }, ] }Copy the code
Unstable fragments
We’ve just solved the problem of structurally stable v-for levels by using a Fragment that acts as a Block, but let’s look at the Fragment itself:
Tag: Fragment, dynamicChildren: [/*.. v-for node..*/]}Copy the code
For a template like this:
<p v-for="item in list">{{ item }}</p>
Copy the code
The Fragment Block looks like this before and after a list [1, 2] becomes [1] :
// const prevBlock = {tag: Fragment, dynamicChildren: [{tag: 'p', children: item, 1 /* TEXT */}, {tag: 'p', children: item, 2 /* TEXT */}]} // const prevBlock = {tag: Fragment, dynamicChildren: [{tag: 'p', children: item, 1 /* TEXT */ } ] }Copy the code
We find that the Fragment Block still faces structural instability. The so-called structural instability refers to the inconsistency in the number or order of dynamic nodes collected in the dynamicChildren of a Block before and after update. What if this inconsistency prevents us from directly targeting Diff? In fact, there is no way to solve this situation, we can only abandon the Diff of dynamicChildren and fall back to the traditional Diff: children of Diff Fragment instead of dynamicChildren.
Note that children of fragments can still be blocks:
const block = {
tag: Fragment,
children: [
{ tag: 'p', children: item, dynamicChildren: [/*...*/], 1 /* TEXT */ },
{ tag: 'p', children: item, dynamicChildren: [/*...*/], 1 /* TEXT */ }
]
}
Copy the code
Thus, the Diff mode of the Block Tree is restored for the
tag and its descendants.
The stability of the fragments
If there are unstable fragments, there are stable fragments. What are stable fragments?
- V minus for is constant
<p v-for="n in 10"></p> <! <p v-for="s in 'ABC '"></p>Copy the code
Since 10 and ‘ABC’ are constant, all of these two fragments do not change, so it is stable, and for stable fragments there is no need to fall back to traditional Diff, which has a performance advantage.
- Multiple root elements
Vue3 no longer restricts component templates to having one root node. For templates with multiple root nodes, for example:
<template>
<div></div>
<p></p>
<i></i>
</template>
Copy the code
As shown above, this is also a stable Fragment. Some students might wonder whether the following template is also a stable Fragment:
<template>
<div v-if="condition"></div>
<p></p>
<i></i>
</template>
Copy the code
This is also stable, because the element with the V-if directive exists as a Block itself, so the Block Tree structure of the template is always:
Block(Fragment)
- Block(div v-if)
- VNode(p)
- VNode(i)
Copy the code
The corresponding VNode should look like this:
const block = {
tag: Fragment,
dynamicChildren: [
{ tag: 'div', dynamicChildren: [...] },
{ tag: 'p' },
{ tag: 'i' },
],
PatchFlags.STABLE_FRAGMENT
}
Copy the code
In any case, its structure is stable. Note the patchFlags. STABLE_FRAGMENT flag here, which must exist otherwise it will fall back to the traditional Diff mode.
- Socket outlet
The following template is displayed:
<Comp>
<p v-if="ok"></p>
<i v-else></i>
</Comp>
Copy the code
Children in the component
will be the slot content. After compiling, the content that should be the Block role will naturally be the Block.
render(ctx) { return createVNode(Comp, null, { default: OpenBlock (), createBlock('p', {key: 0})) : (openBlock(), createBlock(' I ', {key: 0})) : 1}))]), _: 1Copy the code
Now that the structure is stable, comp.vue at the render exit:
<template>
<slot/>
</template>
Copy the code
Is equivalent to:
render() {
return (openBlock(), createBlock(Fragment, null,
this.$slots.default() || []
), PatchFlags.STABLE_FRAGMENT)
}
Copy the code
STABLE_FRAGMENT this is a STABLE_FRAGMENT, and _: 1 is a compiled slot hint, which we must use when writing optimized rendering functions to let the Runtime know that the slot is stable, otherwise it will exit non-optimized mode. There is also a $stable Hint, which I’ll cover in the next article.
<template v-for>
The following template is displayed:
<template>
<template v-for="item in list">
<p>{{ item.name }}</P>
<p>{{ item.age }}</P>
</template>
</template>
Copy the code
The Template element with v-for is itself an unstable Fragment because the list is not constant. In addition, since the
element itself does not render any real DOM, if it contains multiple element nodes, those element nodes will also exist as fragments, but this Fragment is stable because it does not change with the list.
The above content is about how Block Tree and PatchFlags achieve targeted update and some specific ideas and details.
Static ascension
Promote the static node tree
Vue3’s Compiler promotes static nodes, or static properties, if hoistStatic is enabled, which reduces the cost of creating vNodes, as shown in the following template:
<div>
<p>text</p>
</div>
Copy the code
The render function is equivalent to:
function render() {
return (openBlock(), createBlock('div', null, [
createVNode('p', null, 'text')
]))
}
Copy the code
Obviously, the P tag is static, it doesn’t change. However, the problem with the rendering function above is also obvious. If there is dynamic content in the component, when the rendering function is re-executed, even if the P tag is static, its corresponding VNode will be re-created. When static promotion is enabled, its rendering function looks like this:
const hoist1 = createVNode('p', null, 'text')
function render() {
return (openBlock(), createBlock('div', null, [
hoist1
]))
}
Copy the code
This reduces the performance overhead of VNode creation. It is important to understand that static promotion is done in trees, as shown in the following template:
<div>
<section>
<p>
<span>abc</span>
</p>
</section >
</div>
Copy the code
Except for the root div, which cannot be promoted as a block, the entire
<div>
<section>
{{ dynamicText }}
<p>
<span>abc</span>
</p>
</section >
</div>
Copy the code
Since section tags contain dynamic interpolation, subtrees with section as the root node are not promoted, but p tags and their descendants are static and can be promoted.
A situation in which elements are not promoted
- Element with dynamic
key
The binding
Other than the fact that all of the child nodes of an element must be static in order to be promoted, what are the circumstances that prevent promotion?
An element will not be promoted if it has a dynamic key binding, for example:
<div :key="foo"></div>
Copy the code
In fact, an element with any dynamic binding should not be promoted, so why is the key isolated? In fact, the key has a different meaning to VNode than ordinary props. If ordinary props is dynamic, it only needs to be represented in PatchFlags. For example:
<div>
<p :foo="bar"></p>
</div>
Copy the code
We can add PatchFlags to the P tag:
render(ctx) {
return (openBlock(), createBlock('div', null, [
createVNode('p', { foo: ctx }, null, PatchFlags.PROPS, ['foo'])
]))
}
Copy the code
This element needs to be updated as PROPS, and the name of the PROPS to be updated is foo.
H But key itself has the special meaning hi, which is the unique identifier of VNode(or element). Even if two elements are the same except for the key, they are still different elements and need to be completely replaced for different elements. PatchFlags is used for property patches on the same element, so the key is different from the other props.
Since the key value is dynamically variable, the element with a dynamic key should always participate in the diff and cannot simply be identified by PatchFlags. It is simple to make the element with the dynamic key also be a Block, using the following template as an example:
<div>
<div :key="foo"></div>
</div>
Copy the code
The corresponding render function should be:
render(ctx) {
return (openBlock(), createBlock('div', null, [
(openBlock(), createBlock('div', { key: ctx.foo }))
]))
}
Copy the code
Use blocks when using dynamic keys for hand-optimized rendering functions, as we’ll cover later.
- use
ref
The elements of the
If an element uses ref, regardless of whether the value is dynamically bound, then the element will not be promoted statically. This is because the value of ref needs to be set at each patch, as shown in the following template:
<div ref="domRef"></div>
Copy the code
At first glance, this looks like a completely static element. Yes, the element itself does not change, but because of the nature of ref, we have to reset the value of ref during each Diff. Why? Consider a scenario where ref is used:
<template>
<div>
<p ref="domRef"></p>
</div>
</template>
<script>
export default {
setup() {
const refP1 = ref(null)
const refP2 = ref(null)
const useP1 = ref(true)
return {
domRef: useP1 ? refP1 : refP2
}
}
}
</script>
Copy the code
As shown in the code above, the P tag uses a non-dynamic ref attribute with the string domRef, and we notice that setupContext(we call the object returned by the setup function setupContext) also contains the domRef attribute of the same name. It’s not an accident, they form a bond, and the end result is:
- when
useP1
To true,refP1.value
referencep
The element - when
useP1
Is false,refP2.value
referencep
The element
So, even though ref is static, obviously we have to update domRef during the update process because useP1 changes, so as long as an element uses ref, it won’t be promoted statically, The VNode corresponding to this element will also be collected into the dynamicChildren of the parent Block.
However, since p tag does not need to update props except ref, a special PatchFlag will be added to it in the real rendering function, which is called: patchFlags.need_patch:
render() {
return (openBlock(), createBlock('div', null, [
createVNode('p', { ref: 'domRef' }, null, PatchFlags.NEED_PATCH)
]))
}
Copy the code
- Elements that use custom instructions
In fact, an element will not be improved if it uses all of the nave commands provided by Vue except v-pre/ V-cloak, nor will it be improved if it uses custom commands, such as:
<p v-custom></p>
Copy the code
NEED_PATCH is marked on the VNode corresponding to this template, just as in the case of key. By the way, a custom directive is a runtime directive that, like a component’s life cycle, a VNode object has its own life cycle:
- beforeMount
- mounted
- beforeUpdate
- updated
- beforeUnmount
- unmounted
Write a custom directive:
const myDir: Directive = {
beforeMount(el, binds) {
console.log(el)
console.log(binds.value)
console.log(binds.oldValue)
console.log(binds.arg)
console.log(binds.modifiers)
console.log(binds.instance)
}
}
Copy the code
Use this directive:
Const App = {setup() {return () => {return H ('div', [// Call withcache function withcache (h(' H1 ', 'hahah')), [myDir, 10, 'arg', {foo: true}]])])}}}Copy the code
An element can bind multiple directives:
Const App = {setup() {return () => {return H ('div', [// Call withcache function withcache (h(' H1 ', 'hahah')), [myDir, 10, 'arg', {foo: true}], [myDir2, 10, 'arg', {foo: true }], [myDir3, 10, 'arg', { foo: true }] ]) ]) } } }Copy the code
Raising static PROPS
As mentioned earlier, static nodes are promoted on a tree basis. If a VNode has non-static children, then the VNode is not static and will not be promoted. The props of the VNode can be static. This allows us to increase the props of the VNode object. This also saves the overhead of creating VNode objects.
<div>
<p foo="bar" a=b>{{ text }}</p>
</div>
Copy the code
In this template, p tag has dynamic text content, so it cannot be promoted. However, all attributes of P tag are static, so its attributes can be promoted. After promotion, its rendering function is as follows:
const hoistProp = { foo: 'bar', a: 'b' }
render(ctx) {
return (openBlock(), createBlock('div', null, [
createVNode('p', hoistProp, ctx.text)
]))
}
Copy the code
Even dynamically bound property values are promoted if the value is constant:
<p :foo="10" :bar="'abc' + 'def'">{{ text }}</p>
Copy the code
‘ABC’ + ‘def’ are constants and can be promoted.
prestringing
Statically promoted VNodes or tree of nodes themselves are static, so can we prestring them? The following template is displayed:
<div> <p></p> <p></p> ... 20 p tags <p></p> </div>Copy the code
Assume that there are a large number of continuous static P tags in the above template. When hoist optimization is used, the result is as follows:
cosnt hoist1 = createVNode('p', null, null, PatchFlags.HOISTED) cosnt hoist2 = createVNode('p', null, null, PatchFlags.HOISTED) ... Cosnt hoist20 = createVNode('p', null, null, PatchFlags.HOISTED) render() {return (openBlock(), CreateBlock ('div', null, [hoist1, hoist2...20 variables, hoist20])}Copy the code
Prestringing serializes these Static nodes into strings and generates a Static VNode:
const hoistStatic = createStaticVNode('<p></p><p></p><p></p>... 20... <p></p>') render() { return (openBlock(), createBlock('div', null, [ hoistStatic ])) }Copy the code
This has several obvious advantages:
- The volume of generated code is reduced
- Reduce the overhead of creating vNodes
- Reduce memory footprint
Static nodes are created with innerHTML at run time, so not all static nodes are prestring-ready. Static nodes that are prestring-ready must meet the following conditions:
-
Non-table tags: Caption, Thead, TR, TH, TBody, TD, tfoot, COLGroup, COL
-
The attributes of the tag must be:
-
Standard HTML attribute: developer.mozilla.org/en-US/docs/…
-
Or data-/aria- class attributes
When a node meets these conditions, it means that the node can be pre-stringed. However, if there is only one node, it will not be stringed. The number of stringable nodes must be consecutive and reach a certain number:
- If the node has no attributes, then there must be 20 or more static nodes in a row, for example:
<div>
<p></p>
<p></p>
... 20 个 p 标签
<p></p>
</div>
Copy the code
Or 5 or more of these contiguous nodes are attribute bound nodes:
<div>
<p></p>
<p></p>
<p></p>
<p></p>
<p></p>
</div>
Copy the code
Although the number of nodes in this section does not reach 20, 5 nodes have attribute binding.
These nodes do not have to be siblings, but parent-child relationships are also ok, as long as the threshold values meet the conditions, for example:
<div>
<p>
<p>
<p>
<p>
<p></p>
</p>
</p>
</p>
</p>
</div>
Copy the code
Prestringing evaluates the value of an attribute at compile time, for example:
<div>
<p :id="'id-' + 1">
<p :id="'id-' + 2">
<p :id="'id-' + 3">
<p :id="'id-' + 4">
<p :id="'id-' + 5"></p>
</p>
</p>
</p>
</p>
</div>
Copy the code
After stringing with:
const hoistStatic = createStaticVNode('<p></p><p></p>..... < p > < / p > ')Copy the code
When the value of the ID attribute is visible.
Cache Event handler
The template for the following components looks like this:
<Comp @change="a + b" />
Copy the code
This template, if rendered by hand, is equivalent to:
render(ctx) {
return h(Comp, {
onChange: () => (ctx.a + ctx.b)
})
}
Copy the code
Obviously, every time the render function is executed, the PROPS of the Comp component are new objects, and the onChange function is a new one. This triggers an update of the Comp component.
When Vue3 Compiler enables prefixIdentifiers and cacheHandlers, this template is compiled to read:
render(ctx, cache) {
return h(Comp, {
onChange: cache[0] || (cache[0] = ($event) => (ctx.a + ctx.b))
})
}
Copy the code
This will not trigger an update to the Comp component even if the render function is called multiple times, because Vue will see the same reference to onChange when comparing props during the patch phase.
The render function cache object in the above code is an array injected internally by the Vue when the render function is called, like the following:
render.call(ctx, ctx, [])
Copy the code
In fact, we can write cache-capable code by hand without relying on compilation:
Const handleChange = () => {/*... */} return () => {return h(AnthorComp, {onChange: handleChange)}}}Copy the code
So it’s best not to write code like this:
const Comp = { setup() { return () => { return h(AnthorComp, { onChang(){/*... */} // Every time the render function is executed, it is a new function})}}}Copy the code
v-once
This is what Vue2 supports. V-once is a “very directive” instruction, because it is intended for the compiler. When the compiler encountered v-once, it would use the cache we just described to cache all or part of the result of the rendering function, such as the following template:
<div>
<div v-once>{{ foo }}</div>
</div>
Copy the code
Will compile to:
render(ctx, cache) {
return (openBlock(), createBlock('div', null, [
cache[1] || (cache[1] = h("div", null, ctx.foo, 1 /* TEXT */))
]))
}
Copy the code
This caches the vNode. Since the vnode is already cached, subsequent updates will read the cached contents without recreating the vNode object, and the vNode will not be required to participate in the Diff process, so you will usually see compiled code that looks more like this:
render(ctx, cache) { return (openBlock(), createBlock('div', null, [ cache[1] || ( setBlockTracking(-1), Cache [1] = h("div", null, ctx.foo, 1 /* TEXT */), setBlockTracking(1), // Restore cache[1] // whole expression value)]))}Copy the code
To explain this code a bit, we’ve already explained what a “Block Tree” is, and the openBlock() and createBlock() functions create a Block. SetBlockTracking (-1) is used to suspend the collection, so you’ll see it in v-once compiled code, so that v-once wrapped content is not collected into the parent Block and does not participate in the Diff.
So, the performance boost from V-Once comes from two aspects:
- 1. VNode creation cost
- 2. Useless Diff overhead
However, we can reduce the overhead of creating vNodes by caching vNodes instead of using templates:
Const content = h('div', 'XXXX ') return () => {return h('section', content) } } }Copy the code
But this does not avoid useless Diff overhead because we did not use the Block Tree optimization pattern.
It is important to mention that vnodes are reusable in Vue2.5.18+ and Vue3, for example, we can use the same VNode multiple times in different places:
Const Comp = {setup() {const content = h('div', 'XXXX ') return () => {// multiple render content return h('section', [content, content, content]) } } }Copy the code
Handwriting high performance rendering function
Next we will enter the main part, where we will try out the rendering function in handwriting optimization mode.
A few tips to keep in mind:
- a
Block
It’s a special oneVNode
It can be understood that it is just more than ordinaryVNode
One moredynamicChildren
attribute createBlock()
Functions andcreateVNode()
The calling signature of the function is almost identical, in factcreateBlock()
Inside the function is encapsulationcreateVNode()
Again, this provesBlock
isVNode
.- In the call
createBlock()
createBlock
Before you callopenBlock()
Function, which usually occur with the comma operator:
render() {
return (openBlock(), createBlock('div'))
}
Copy the code
Block trees are flexible:
In the previous introduction, the root node exists as a Block, but the root node does not have to be a Block. Blocks can be enabled on any node:
setup() {
return () => {
return h('div', [
(openBlock(), createBlock('p', null, [/*...*/]))
])
}
}
Copy the code
This is also possible because the renderer automatically goes into optimized mode during Diff if the VNode has the dynamicChildren attribute. But we usually have the root node acting as a Block.
Use PatchFlags correctly:
PatchFlags is used to mark the content of an element that needs to be updated. For example, when an element has a dynamic class binding, we need to use the patchFlags.class flag:
const App = { setup() { const refOk = ref(true) return () => { return (openBlock(), createBlock('div', null, [createVNode('p', {class: {foo: refok. value}}, 'hello', PatchFlags.class) // Use the class flag])}}}Copy the code
If the wrong tag is used, the update may fail. Here are the details of how to use the tag:
PatchFlags.CLASS
– When there is dynamicclass
Bind usingPatchFlags.STYLE
– When there is dynamicstyle
For binding, for example:
createVNode('p', { style: { color: refColor.value } }, 'hello', PatchFlags.STYLE)
Copy the code
PatchFlags.TEXT
– When a dynamic text node is used, for example:
createVNode('p', null, refText.value, PatchFlags.TEXT)
Copy the code
PatchFlags.PROPS
– When there is exceptclass
和style
Other than dynamically binding properties, for example:
createVNode('p', { foo: refVal.value }, 'hello', PatchFlags.PROPS, ['foo'])
Copy the code
The important thing to note here is that in addition to using patchFlags.props, a fifth parameter is provided, an array containing the names of the dynamic properties.
PatchFlags.FULL_PROPS
– When there is movementname
的props
For example:
createVNode('p', { [refKey.value]: 'val' }, 'hello', PatchFlags.FULL_PROPS)
Copy the code
In fact, using FULL_PROPS is equivalent to the same Diff for props as the traditional Diff. FULL_PROPS = FULL_PROPS = FULL_PROPS = FULL_PROPS
- To avoid misuse,
PatchFlags
As a result ofbug
- Reduce the mental burden at the same time, although lost
props diff
Performance advantages, but still can be enjoyedBlock Tree
The advantage of.
When there are many updates at the same time, need to be used PatchFlags a bitwise or operation, such as: PatchFlags. CLASS | PatchFlags. STYLE.
NEED_PATCH logo
Why do we take this symbol out separately? It’s special and needs our extra attention. When we use hooks such as ref or onVNodeXXX (including custom directives), we need to use this flag so that it can be collected by the parent Block, for reasons explained in the static promotion section:
const App = {
setup() {
const refDom = ref(null)
return () => {
return (openBlock(), createBlock('div', null,[
createVNode('p',
{
ref: refDom,
onVnodeBeforeMount() {/* ... */}
},
null,
PatchFlags.NEED_PATCH
)
]))
}
}
}
Copy the code
Blocks must be used where they should be used
At the beginning, we explained that some instructions can cause DOM structure instability and that blocks must be used to solve the problem. The same goes for the handwritten render function:
- Branch judgment uses blocks:
const App = { setup() { const refOk = ref(true) return () => { return (openBlock(), createBlock('div', null, [refOk. Using Block value / / here? (openBlock (), createBlock (' div '{key: 0}, / / * * /...)) : (openBlock(), createBlock('div', { key: 1 }, [/* ... */])) ])) } } }Copy the code
The reason for using blocks was explained earlier, but it is important to note that in addition to using blocks for branching, you need to specify a different key for the Block.
- Lists use blocks:
When we render a list, we often write code like this:
const App = { setup() { const obj = reactive({ list: [ { val: 1 }, { val: 2 } ] }) return () => { return (openBlock(), createBlock('div', null, Obj.list. map(item => {return createVNode('p', null, item.val, PatchFlags.text)})))}}}Copy the code
This works fine in non-optimized mode, but now that we’re using blocks, we’ve already explained why v-for needs to use blocks. Imagine if we were to modify the data by executing the following statement:
obj.list.splice(0, 1)
Copy the code
This causes the dynamic nodes collected in the Block to be inconsistent, and eventually Diff problems. The solution is to make the entire list a Block, in which case we need to use a Fragment:
const App = { setup() { const obj = reactive({ list: [ { val: 1 }, { val: }}) return () => {return (openBlock(), createBlock('div', null, [// create a Fragment, Obj.list. map(item => {return createVNode('p', Null, item.val, patchFlags. TEXT)}), // Remember to specify the correct PatchFlags patchflags. UNKEYED_FRAGMENT)]))}}}Copy the code
To sum up:
- We should always use lists
Fragment
And as aBlock
The role of - if
Fragment
的children
Is not specifiedkey
, then should beFragment
In the playPatchFlags.UNKEYED_FRAGMENT
. Accordingly, if specifiedkey
You should havePatchFlags.KEYED_FRAGMENT
- Notice the call
openBlock(true)
, the parameter is passedtrue
This represents thisBlock
Not collectdynamicChildren
Because no matter it isKEYED
orUNKEYED
的Fragment
, in Diff itschildren
Will fall back to the traditional Diff mode, so no collection is requireddynamicChildren
.
Another thing to note here is that in a Diff Fragment, since the traditional Diff is retracted, we want to restore the optimized mode as soon as possible, while ensuring that the subsequent collection is controlled, so we usually make every child of the Fragment a Block:
const App = { setup() { const obj = reactive({ list: [ { val: 1 }, { val: 2 } ] }) return () => { return (openBlock(), createBlock('div', null, [ (openBlock(true), createBlock(Fragment, null, Obj.list.map (item => {return (openBlock(), createBlock('p', null, item.val, PatchFlags.text))}), PatchFlags.UNKEYED_FRAGMENT )) ])) } } }Copy the code
Finally, for stable fragments, if you are sure that the list will never change, for example if you are sure that obj.list will never change, then you should use: PatchFlags.STABLE_FRAGMENT flag, and openBlcok() to remove parameters, represents the collection of dynamicChildren:
const App = { setup() { const obj = reactive({ list: [ { val: 1 }, { val: 2}]}) return () => {return (openBlock(), createBlock('div', null, CreateBlock (Fragment, null, obj.list.map(item => {return createVNode('p', null, item.val, createVNode('p', null, item. Patchflags.text)}), // Stable fragment PatchFlags.stable_fragment))]))}}}Copy the code
As noted above.
- The element that uses a dynamic key should be a Block
As we saw in the static promotion section, when an element uses a dynamic key, even if the two elements are otherwise identical, they are two different elements that need to be replaced, and should exist as a Block in the Block Tree, so if an element uses a dynamic key, It should be a Block:
const App = { setup() { const refKey = ref('foo') return () => { return (openBlock(), createBlock('div', Null,[// this should be Block(openBlock(), createBlock('p', {key: refkey.value}))]))}}}Copy the code
This is actually a must, check out github.com/vuejs/vue-n… .
Using Slot hint
We mentioned slot Hints in the “Stable Fragment” section, and when we write slot contents for a component, we need to use slot hints to tell the Runtime that “we’ve been able to keep the slot contents structurally stable” :
render() { return (openBlock(), createBlock(Comp, null, { default: () => [ refVal.value ? (openBlock(), createBlock('p', ...)) ? (openBlock(), createBlock('div', ...)) ], // slot hint _: 1}}))Copy the code
Of course, don’t hint if you can’t guarantee that, or if you feel mentally burdened.
Using the $stable hint
The $stable Hint is different from the previous optimization strategy, which assumes that the renderer works in optimized mode, whereas the $stable Hint works in non-optimized mode, which is the rendering function we normally write. So what does it do? As shown in the following code (demonstrated using TSX) :
export const App = defineComponent({
name: 'App',
setup() {
const refVal = ref(true)
return () => {
refVal.value
return (
<Hello>
{
{ default: () => [<p>hello</p>] }
}
</Hello>
)
}
}
})
Copy the code
As shown in the code, read the refVal in rendering function. The value of the value, to establish the relationship between the dependent on collecting, when the value of the modified refVal trigger < Hello > component updates, but we found that the Hello component way not to change props, and slot of its content is static, The $stable Hint should therefore not be updated.
export const App = defineComponent({ name: 'App', setup() { const refVal = ref(true) return () => { refVal.value return ( <Hello> { { default: () = > / < p > hello < / p >], $stable: true}} < / hello > / / modified here)}}})Copy the code
Use DYNAMIC_SLOTS correctly for components
When we build slots dynamically, we need to specify PatchFlags.dynamic_slots for the component’s VNode, otherwise the update will fail. What is dynamic build slots? This is the name of slots that depends on the current scope variable. For example:
Render () {// Use the current component's scope const slots ={} Conditions determine the if (refVal value) {slots. The header () = = > [h (' p ', 'hello')]} / / case 2: ForEach (item => {slots[item.name] = () => [...]) }) // Slots [refname.value] = () => [... Return (openBlock(), createBlock('div', null, patchFlags. DYNAMIC_SLOTS createVNode(Comp, null, slots, PatchFlags.DYNAMIC_SLOTS) ])) }Copy the code
As noted above.
Don’t stop learning…
Welcome to follow my personal wechat official account “HcySunYang”. Let’s coding for fun together!