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

    • traditionalDiffProblem with the algorithm
    • BlockCooperate withPatchFlagsDo targeted update
    • The node is unstable –Block Tree
    • v-ifAs the element ofBlock
    • v-forAs the element ofBlock
    • unstableFragment
    • The stability of theFragment
      • v-forThe expression of is constant
      • Multiple root elements
      • Socket outlet
      • <template v-for>
  • Static ascension

    • Promote the static node tree
    • A situation in which elements are not promoted
    • Element with dynamickeyThe binding
    • userefThe elements of the
    • Elements that use custom instructions
    • Improve the staticPROPS
  • prestringing

  • Cache Event handler

  • v-once

  • Handwriting high performance rendering function

    • A couple of little things to remember
    • Block TreeIs flexible
    • Use it correctlyPatchFlags
    • NEED_PATCH
    • The use ofBlockMust be used
      • Branch judgment useBlock
      • A list of useBlock
      • Using dynamickeyThe element of should beBlock
    • useSlot hint
    • Use correctly for componentsDYNAMIC_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 dynamictextContent(Such as in the template abovepThe label)
  • The number 2: indicates that the element is dynamicclassThe 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

tag, and v-else is a

tag, so there is a problem here. The underlying problem is that dynamicChildren diff ignores the vDOM tree hierarchy, and the following template has the same problem:
<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

tag, this can cause problems because of the instability of the front and back DOM trees. So we thought, how do we stabilize the structure of the DOM tree?

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

tag is a Block, then the corresponding Block Tree should be:
// 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

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

element and its descendants are promoted because they are static throughout the tree. If we replace the ABC in the code above with {{ABC}}, the whole tree will not be promoted. Take a look at the following code:

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

  • userefThe 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:

  • whenuseP1To true,refP1.valuereferencepThe element
  • whenuseP1Is false,refP2.valuereferencepThe 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:

  1. aBlockIt’s a special oneVNodeIt can be understood that it is just more than ordinaryVNodeOne moredynamicChildrenattribute
  2. createBlock()Functions andcreateVNode()The calling signature of the function is almost identical, in factcreateBlock()Inside the function is encapsulationcreateVNode()Again, this provesBlockisVNode.
  3. In the callcreateBlock()createBlockBefore 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 dynamicclassBind using
  • PatchFlags.STYLE– When there is dynamicstyleFor 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 exceptclassstyleOther 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 movementnamepropsFor 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,PatchFlagsAs a result ofbug
  • Reduce the mental burden at the same time, although lostprops diffPerformance advantages, but still can be enjoyedBlock TreeThe 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 listsFragmentAnd as aBlockThe role of
  • ifFragmentchildrenIs not specifiedkey, then should beFragmentIn the playPatchFlags.UNKEYED_FRAGMENT. Accordingly, if specifiedkeyYou should havePatchFlags.KEYED_FRAGMENT
  • Notice the callopenBlock(true), the parameter is passedtrueThis represents thisBlockNot collectdynamicChildrenBecause no matter it isKEYEDorUNKEYEDFragment, in Diff itschildrenWill 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!