Детали оптимизации компилятора Vue3, как писать высокопроизводительные функции рендеринга

Vue.js

Добро пожаловать в мою личную учетную запись WeChat «HcySunYang». Давайте программировать в удовольствие вместе!

Vue3изCompilerа такжеruntimeРаботая в тесном сотрудничестве, чтобы в полной мере использовать информацию времени компиляции, производительность была значительно улучшена. Цель этой статьи говорит вамVue3изCompilerКакие оптимизации были сделаны, и некоторые детали оптимизации, которые вы, возможно, захотите узнать, на этой основе мы попытаемся обобщить набор методов для высокопроизводительных функций рендеринга в режиме рукописной оптимизации.Эти знания также могут быть использованы для реализацииVue3изjsx babelплагин, пустьjsxтакже могут пользоваться преимуществами оптимизированного режима во время выполнения, здесь необходимо уточнить, что даже в неоптимизированном режиме теоретическиVue3изDiffпроизводительность лучше, чемVue2из. Кроме того, в эту статью не включеныSSRСвязанные оптимизации я надеюсь обобщить в следующей статье.

Длина большая и потребовалось много усилий, чтобы разобраться.Vue3Учащимся, которые мало знают об этом, может быть трудно читать, поэтому вы можете сначала собрать его, а потом использовать.

TOC

  • Дерево блоков и PatchFlags

    • ТрадицияDiffпроблема алгоритма
    • BlockСотрудничатьPatchFlagsДелайте целевые обновления
    • Узел нестабилен -Block Tree
    • v-ifэлемент какBlock
    • v-forэлемент какBlock
    • нестабильныйFragment
    • стабильныйFragment
      • v-forвыражение постоянно
      • несколько корневых элементов
      • щелевой выход
      • <template v-for>
  • статический подъемник

    • повысить статическое дерево узлов
    • Случай, когда элемент не будет поднят
    • элементы с динамикойkeyсвязывать
    • использоватьrefЭлементы
    • Элементы, использующие пользовательские директивы
    • повысить статическийPROPS
  • предварительно натягивать

  • Cache Event handler

  • v-once

  • Рукописная функция высокопроизводительного рендеринга

    • Несколько моментов, которые нужно помнить
    • Block Treeгибкий
    • использовать правильноPatchFlags
    • NEED_PATCH
    • использованиеBlockгде вы должны использовать
      • Использование отраслевого сужденияBlock
      • Использование спискаBlock
      • использовать динамическийkeyЭлемент должен бытьBlock
    • использоватьSlot hint
    • Правильное использование компонентовDYNAMIC_SLOTS
    • использовать$stable hint

Дерево блоков и PatchFlags

Block Treeа такжеPatchFlagsдаVue3Воспользуйтесь информацией о компиляции иDiffпоэтапные оптимизации. Ю Да не раз публично говорил об идеях, цель наших подробных подробностей — лучше понять и попробовать написать высокопроизводительно вручную.VNode.

Проблемы с традиционными алгоритмами сравнения

"Традицияvdom" изDiffАлгоритм всегда основан наvdomИерархия дерева проходится слой за слоем (если выdiffЯ не понимаю алгоритма, вы можете прочитать то, что я написал раньше"Визуализатор"В этой статье представлены три традиционныхDiffметод), например, как показано в следующем шаблоне:

<div>
    <p>bar</p>
</div>

для традиционныхdiffАлгоритмически этоdiffэтот абзацvnode(шаблон составленvnode) будет испытывать:

  • Атрибуты тегов Div + дочерние элементы

  • Атрибуты метки (класс) + дочерние элементы

  • текстовый узел: полоса

Но, очевидно, это, очевидно, статическийvdom, который вряд ли изменится на этапе обновления компонента. если вы можетеdiffэтап, чтобы пропустить статический контент, это позволит избежать бесполезногоvdomОбход дерева и сравнение, это должно быть самым ранним источником идей по оптимизации ----Пропускайте статический контент и сравнивайте только динамический контент.

Блок сотрудничает с PatchFlags для достижения целевого обновления

Давайте начнемBlockговорить сноваBlock Tree. Теперь, когда идея есть, мы хотим сравнить только нестатический контент, такой как:

<div>
    <p>foo</p>
    <p>{{ bar }}</p>
</div>

Только в этом шаблоне<p>{{ bar }}</p>Текстовый узел является динамическим, поэтому необходимо целенаправленно обновлять только текстовый узел, что особенно очевидно в сценариях с большим объемом статического содержимого и небольшим объемом динамического содержимого. Но вопрос в том, как это сделать? нам нужно получить весьvdomВозможности динамических узлов в дереве могут быть не такими сложными, как вы думаете, давайте посмотрим на традицию, соответствующую этому шаблону.vdomКак выглядит дерево:

const vnode = {
    tag: 'div',
    children: [
        { tag: 'p', children: 'foo' },
        { tag: 'p', children: ctx.bar },  // 这是动态节点
    ]
}

в традиционномvdomдерева, мы не получаем никакой полезной информации во время выполнения, ноVue3изcompilerУмение анализировать шаблоны и извлекать полезную информацию, что в итоге проявляется вvdomна дереве. Например, он может четко знать: какие узлы являются динамическими узлами и почему он динамический (привязан к динамическим узлам).class? по-прежнему привязывать динамическиstyle? Или другие динамические свойства? ), короче говоря, компилятор может извлечь нужную нам информацию, и с помощью этой информации мы можем создатьvnodeВ процессе маркировки динамических узлов: то есть легендарныхPatchFlags.

мы можем поставитьPatchFlagsПросто понимается как числовой знак, который присваивает этим числам разные значения, например:

  • Число 1: означает, что узел имеет динамическийtextContent(например, в шаблоне вышеpЭтикетка)
  • Число 2: означает, что элемент является динамическим.classсвязывать
  • Номер 3: представляет ххххх

Короче говоря, мы можем предположить эти значения, которые в конечном итоге отражаются вvnodeначальство:

const vnode = {
    tag: 'div',
    children: [
        { tag: 'p', children: 'foo' },
        { tag: 'p', children: ctx.bar, patchFlag: 1 /* 动态的 textContent */ },
    ]
}

С помощью этой информации мы можемvnodeДинамический узел извлекается на этапе создания узла Какой узел является динамическим узлом? с участиемpatchFlagУзел является динамическим узлом, мы извлекаем его и сохраняем в массиве, например:

const vnode = {
    tag: 'div',
    children: [
        { tag: 'p', children: 'foo' },
        { tag: 'p', children: ctx.bar, patchFlag: 1 /* 动态的 textContent */ },
    ],
    dynamicChildren: [
        { tag: 'p', children: ctx.bar, patchFlag: 1 /* 动态的 textContent */ },
    ]
}

dynamicChildrenЭто массив, который мы используем для хранения всех динамических узлов-потомков под узлом, обратите внимание на формулировку здесь: «дети», например:

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 */ },
    ]
}

как указано вышеvnodeпоказано,divУзел может собирать не только непосредственных динамических потомков, но и динамические узлы во всех дочерних узлах. ЗачемdivУзел такой мощный? Потому что у него особая роль:Block, да этоdivУзлы легендарныеBlock.ОдинBlockНа самом делеVNode, за исключением того, что он обладает особыми свойствами (одним из которых являетсяdynamicChildren).

Теперь, когда у нас есть все динамические узлы, они хранятся вdynamicChildren, так что вdiffВ процессе можно избежатьvdomДерево пройдено слой за слоем, но непосредственно найденоdynamicChildrenобновить. Помимо пропуска бесполезных обходов уровней, поскольку мыvnodeотмеченpatchFlag, так что в обновленииdynamicChildrenКогда узел в узле находится, он может точно знать, какие действия по обновлению необходимо применить к узлу, что в основном реализует целевое обновление.

Нестабильность узла - дерево блоков

ОдинBlockне могу помиритьсяBlock Tree, что означает, что вvdomВ дереве будет несколькоvnodeУзел действует какBlockроль, которая представляет собойBlock Tree. Так в чем же делоvnodeузел будет действовать какblockроль?

Взгляните на следующий шаблон:

<div>
  <section v-if="foo">
    <p>{{ a }}</p>
  </section>
  <div v-else>
    <p>{{ a }}</p>
  </div>
</div>

Предположим, что только крайнийdivЭтикеткаBlockроль, то когдаfooкогда правда,blockСобранные динамические узлы:

cosnt block = {
    tag: 'div',
    dynamicChildren: [
        { tag: 'p', children: ctx.a, patchFlag: 1 }
    ]
}


когдаfooложно,blockСодержание следующее:

cosnt block = {
    tag: 'div',
    dynamicChildren: [
        { tag: 'p', children: ctx.a, patchFlag: 1 }
    ]
}


можно найти независимо отfooправда или ложь,blockСодержание не изменилось, а это значит, что вdiffСтадия не делает никаких обновлений, но мы также видим:v-ifэто<section>Этикетка,v-elseэто<div>тег, так что вот проблема. Собственно, суть проблемы в том, чтоdynamicChildrenизdiffэто игнорироватьvdomНа уровне дерева такая же проблема возникает у следующих шаблонов:

<div>
  <section v-if="foo">
    <p>{{ a }}</p>
  </section>
  <section v-else> <!-- 即使这里是 section -->
       <div> <!-- 这个 div 标签在 diff 过程中被忽略 -->
            <p>{{ a }}</p>
        </div>
  </section >
</div>

хотяv-elseтакже является<section>этикетка, но из-за передней и заднейDOMНестабильность дерева также может вызвать проблемы. Потом думаем как сделатьDOMСтабильна ли структура дерева?

v-ifэлемент какBlock

Если позволить использоватьv-if/v-else-if/v-elseЭлементы других директив также используются в качествеBlockЧто случится? В качестве примера возьмем следующий шаблон:

<div>
  <section v-if="foo">
    <p>{{ a }}</p>
  </section>
  <section v-else> <!-- 即使这里是 section -->
       <div> <!-- 这个 div 标签在 diff 过程中被忽略 -->
            <p>{{ a }}</p>
        </div>
  </section >
</div>

Если мы позволим этим двумsectionтеги какblock, то он будет представлять собойblock tree:

Block(Div)
    - Block(Section v-if)
    - Block(Section v-else)

ОтецBlockВ дополнение к сбору дочерних динамических узлов также собираются дочерние динамические узлы.Block, значит дваBlock(section)будет использоваться какBlock(div)изdynamicChildren:

cosnt block = {
    tag: 'div',
    dynamicChildren: [
        { tag: 'section', { key: 0 }, dynamicChildren: [...]}, /* Block(Section v-if) */
        { tag: 'section', { key: 1 }, dynamicChildren: [...]}  /* Block(Section v-else) */
    ]
}


так когдаv-ifКогда условие истинно,dynamicChildrenсодержалась вBlock(section v-if), когда условие ложноdynamicChildrenсодержалась вBlock(section v-else), во время процесса Diff рендерер знает, что это два разныхBlock, поэтому производится полная замена, которая решаетDOMПроблемы, вызванные структурной нестабильностью. А этоBlock Tree.

Элементы v-for as Block

не толькоv-ifпозволитDOMструктурная нестабильность,v-forбудет, ноv-forСитуация несколько сложнее. Рассмотрим следующий шаблон:

<div>
    <p v-for="item in list">{{ item }}</p>
    <i>{{ foo }}</i>
    <i>{{ bar }}</i>
</div>

Предположим, что значение списка задано[1 ,2]становится​[1]​, согласно предыдущей идее, крайняя<div>маркировать какBlock, то соответствует до и после обновленияBlock Treeдолжно быть:

// 前
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: ctx.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 */ },
    ]
}


prevBlcokЕсть четыре динамических узла вnextBlockЕсть три динамических узла. Как поступитьDiff? Некоторые студенты могут сказать взятьdynamicChildrenвыполнять традициюDiff, что неверно, поскольку традиционныйDiffОдним из предварительных условий является то, что между узлами одного уровняDiff,ноdynamicChildrenУзлы внутри не обязательно находятся на одном уровне, о котором мы упоминали ранее.

На самом деле нам просто нужно позволитьv-forЭлемент также действует какBlockВот и все. так что неважноv-forкак он меняется, он всегда одинBlock, что гарантирует устойчивость конструкции, независимо отv-forКак это меняетсяBlock TreeЭто выглядит как:

const block = {
    tag: 'div',
    dynamicChildren: [
        // 这是一个 Block 哦,它有 dynamicChildren
        { tag: Fragment, dynamicChildren: [/*.. v-for 的节点 ..*/] }
        { tag: 'i', children: ctx.foo, 1 /* TEXT */ },
        { tag: 'i', children: ctx.bar, 1 /* TEXT */ },
    ]
}


Нестабильный фрагмент

только что мы использовалиFragmentи пусть действует какBlockроль решенаv-forСтруктура иерархии элементов стабильна, но давайте посмотрим на это.Fragmentсам:

{ tag: Fragment, dynamicChildren: [/*.. v-for 的节点 ..*/] }

Для такого шаблона:

<p v-for="item in list">{{ item }}</p>

в списке по[1, 2]стать​[1]до и после,FragmentэтоBlockЭто должно выглядеть так:

// 前
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 */ }
    ]
}

мы обнаруживаем,FragmentэтоBlockпо-прежнему сталкивается со структурной нестабильностью,Так называемая структурная нестабильность относится к результатам до и после обновления.blockизdynamicChildrenНесоответствия в количестве или порядке динамических узлов, собранных в. Это несоответствие не оставляет нам возможности напрямую нацеливатьсяDiff,Как это сделать? На самом деле нет никакого способа справиться с этой ситуацией, мы можем только отказаться отdynamicChildrenизDiffи вернуться к традиционнымDiff:которыйDiff FragmentизchildrenвместоdynamicChildren.

Но следует отметить, чтоFragmentдочерние узлы (children) еще может бытьBlock:

const block = {
    tag: Fragment,
    children: [
        { tag: 'p', children: item, dynamicChildren: [/*...*/], 1 /* TEXT */ },
        { tag: 'p', children: item, dynamicChildren: [/*...*/], 1 /* TEXT */ }
    ]
}


Таким образом, для<p>лейбл и его потомкиDiffвосстановитBlock TreeизDiffмодель.

Стабильный фрагмент

Так как существует нестабильностьFragment, то есть стабильнаяFragment, как это похожеFragmentЭто стабильно?

  • Выражение v-for постоянно
<p v-for="n in 10"></p>
<!-- 或者 -->
<p v-for="s in 'abc'"></p>

Потому что10и​'abc'константы, все дваFragmentне меняется, поэтому он стабилен, для стабильногоFragmentНе нужно возвращаться к традиционнымDiffДа, это будет иметь определенные преимущества в производительности.

  • несколько корневых элементов

Vue3Шаблоны, которые больше не ограничивают компоненты, должны иметь корневой узел, например для шаблонов с несколькими корневыми узлами:

<template>
    <div></div>
    <p></p>
    <i></i>
</template>

Как и выше, это также стабильный фрагмент.Некоторые учащиеся могут подумать, что следующий шаблон также является стабильным фрагментом:

<template>
    <div v-if="condition"></div>
    <p></p>
    <i></i>
</template>

Это на самом деле стабильно, потому что сv-ifЭлемент самой директивы какBlockсуществует, поэтому этот шаблонBlock TreeСтруктура всегда:

Block(Fragment)
    - Block(div v-if)
    - VNode(p)
    - VNode(i)

соответствуетVNodeдолжно быть похоже на:

const block = {
    tag: Fragment,
    dynamicChildren: [
        { tag: 'div', dynamicChildren: [...] },
        { tag: 'p' },
        { tag: 'i' },
    ],
    PatchFlags.STABLE_FRAGMENT
}


Несмотря на это, его структура стабильна. Следует отметить, что здесьPatchFlags.STABLE_FRAGMENT​, флаг должен существовать, иначе он вернется к традиционному​Diffрежим.

  • щелевой выход

Как показано в следующем шаблоне:

<Comp>
    <p v-if="ok"></p>
    <i v-else></i>
</Comp>

компоненты<Comp>внутриchildrenбудет использоваться как содержимое слота, после компиляции следует использовать какBlockСодержание персонажа, естественно, будетBlock, смог обеспечить стабильность структуры, например, приведенный выше код эквивалентен:

render(ctx) {
    return createVNode(Comp, null, {
        default: () => ([
            ctx.ok
                // 这里已经是 Block 了
                ? (openBlock(), createBlock('p', { key: 0 }))
                : (openBlock(), createBlock('i', { key: 1 }))
        ]),
        _: 1 // 注意这里哦
    })
}


Теперь, когда структура стабильна, на выходе рендерингаComp.vue:

<template>
    <slot/>
</template>

эквивалентно:

render() {
    return (openBlock(), createBlock(Fragment, null,
        this.$slots.default() || []
    ), PatchFlags.STABLE_FRAGMENT)
}


Это естественноSTABLE_FRAGMENT, всем обратить внимание на предыдущий код_: 1Это скомпилированныйslot hint, мы должны использовать этот флаг, когда вручную пишем функцию рендеринга оптимизированного режима, чтобы сделатьruntimeзнаниеslotстабилен, в противном случае он выйдет из неоптимизированного режима. Еще один$stableподсказка, которая будет объяснена в следующей статье.

<template v-for>

Как показано в следующем шаблоне:

<template>
    <template v-for="item in list">
        <p>{{ item.name }}</P>
        <p>{{ item.age }}</P>
    </template>
</template> 

для сv-forизtemplateСам элемент, он нестабиленFragment,потому чтоlistне является константой. Кроме того, потому что<template>Сам элемент не отображает никаких реальныхDOM, поэтому, если он содержит несколько узлов элементов, эти узлы элементов также будутFragmentсуществует, но этоFragmentстабилен, потому что не меняетсяlistменяется с изменением.

Выше почтиBlock TreeСотрудничатьPatchFlagsКак добиться целевого обновления и некоторые детали конкретных идей.

статический подъемник

повысить статическое дерево узлов

Vue3изCompilerесли включеноhoistStaticопция будет продвигать статические узлы или статические свойства, что может уменьшить созданиеVNodeпотребления, как показано в следующем шаблоне:

<div>
    <p>text</p>
</div>

Его функция рендеринга без продвижения эквивалентна:

function render() {
    return (openBlock(), createBlock('div', null, [
        createVNode('p', null, 'text')
    ]))
}


Это понятно,pМетка статична, она не меняется. Но проблема с вышеописанной функцией рендеринга тоже очевидна.Если в компоненте есть динамический контент, то при повторном выполнении функции рендеринга, даже еслиpМетка статическая, значит она соответствуетVNodeОн будет воссоздан. При включении статического обновления его функция рендеринга выглядит следующим образом:

const hoist1 = createVNode('p', null, 'text')

function render() {
    return (openBlock(), createBlock('div', null, [
        hoist1
    ]))
}


Это уменьшаетVNodeСтоимость созданной производительности. Важно понимать, что статическое продвижение выполняется в единицах деревьев, как показано в следующем шаблоне:

<div>
  <section>
    <p>
      <span>abc</span>
    </p>
  </section >
</div>

За исключением корневого узлаdivПоскольку блок не может быть поднят, весь<section>И элемент, и его потомки повышаются, потому что они статичны по всему дереву. Если мы поместим приведенный выше код вabcзаменить{{ abc }}, то все дерево не будет повышено. Посмотрите на следующий код:

<div>
  <section>
    {{ dynamicText }}
    <p>
      <span>abc</span>
    </p>
  </section >
</div>

из-заsectionТеги содержат динамическую интерполяцию, поэтому начните сsectionПоддерево, которое является корневым узлом, не будет повышено, ноpМетки и их узлы-потомки являются статическими и могут быть повышены.

Случай, когда элемент не будет поднят

  • элементы с динамикойkeyсвязывать

Помимо того факта, что все потомки элемента должны быть статическими для повышения, какие еще ситуации препятствуют повышению?

Если элемент имеет динамическийkeybind, то он не будет продвигаться, например:

<div :key="foo"></div>

На самом деле элемент с любой динамической привязкой не должен продвигаться, так почемуkeyЕго вынесут в одиночку? Фактическиkeyи обычныйpropsпо сравнению с этим дляVNodeСмысл другой, обычныйpropsЕсли он динамический, то нужно только проявить вPatchFlagsна нем, например:

<div>
    <p :foo="bar"></p>
</div>

мы можемpэтикетка наPatchFlags:

render(ctx) {
    return (openBlock(), createBlock('div', null, [
        createVNode('p', { foo: ctx }, null, PatchFlags.PROPS, ['foo'])
    ]))
}


обратите внимание, что созданиеVNode, пометил егоPatchFlags.PROPS, а это значит, что этот элемент нужно обновитьPROPS, и его необходимо обновитьPROPSимяfoo.

ч ноkeyсам по себе имеет особое значение привет, этоVNode(или элемент) уникальный идентификатор, даже если два элемента, кромеkeyВсе остальное то же самое, но эти два элемента все же разные элементы, и для разных элементов требуется полный процесс замены, иPatchFlagsдля патчей атрибутов на одном и том же элементе, поэтомуkeyотличается от другихpropsиз.

потому чтоkeyЗначение динамически изменяется, поэтому для динамическогоkeyэлемент, он всегда должен участвовать вdiffв и не может просто попастьPatchFlagsИдентификатор патча, что мне делать? Очень просто, пусть динамическийkeyЭлемент также действует какBlockТо есть возьмем в качестве примера следующий шаблон:

<div>
    <div :key="foo"></div>
</div>

Его соответствующая функция рендеринга должна быть:

render(ctx) {
    return (openBlock(), createBlock('div', null, [
        (openBlock(), createBlock('div', { key: ctx.foo }))
    ]))
}


Советы: При рукописном вводе функции рендеринга в оптимизированном режиме, если вы используете динамическийkey, не забудьте использоватьBlockО, и мы завершим это позже.

  • использоватьrefЭлементы

Если элемент используетref, независимо от того, является ли значение динамической привязки, то этот элемент не будет продвигаться статически, это потому, что каждый разpatchнужно установитьrefзначение, как показано в следующем шаблоне:

<div ref="domRef"></div>

На первый взгляд кажется, что это совершенно статичный элемент, да, сам элемент не изменится, но за счетrefхарактеристики, поэтому мы должныDiffсбросить в процессеrefценность, почему? Посмотрим, как использоватьrefСценарий:

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

Как показано в приведенном выше коде,pЭтикетка использует нединамическийrefсвойство, значение представляет собой строкуdomRef, и мы замечаемsetupContext(Мы кладемsetupОбъект, возвращаемый функцией, называетсяsetupContext) также содержит то же имяdomRefАтрибуты, это не случайно, они будут устанавливать соединение, и конечный результат:

  • когдаuseP1когда правда,refP1.valueЦитироватьpэлемент
  • когдаuseP1ложно,refP2.valueЦитироватьpэлемент

Следовательно, даже еслиrefявляется статическим, но, видимо, в процессе обновления из-заuseP1изменения, нам пришлось обновитьdomRef, так что пока используется один элементref, он не будет продвигаться статически, и этот элемент соответствуетVNodeтакже будет собираться родителюBlockизdynamicChildrenсередина.

Но из-заpДобавление тега необходимо обновитьref, нет необходимости обновлять другиеprops, поэтому в реальной функции рендеринга он будет помечен специальнымPatchFlag, называется:PatchFlags.NEED_PATCH:

render() {
    return (openBlock(), createBlock('div', null, [
        createVNode('p', { ref: 'domRef' }, null, PatchFlags.NEED_PATCH)
    ]))
}


  • Элементы, использующие пользовательские директивы

Фактически, если элемент используется, кромеv-pre/v-cloakВсе кромеVueПредоставленные изначально директивы не будут продвигаться, и использование пользовательских директив не будет продвигаться, например:

<p v-custom></p>

и использоватьkeyто же самое, он будет соответствовать этому шаблонуVNodeотметкаNEED_PATCHлоготип. Кстати, поговорим о том, как применять пользовательские инструкции при написании функций рендеринга от руки.VNodeОбъекты также имеют свой жизненный цикл:

  • beforeMount
  • mounted
  • beforeUpdate
  • updated
  • beforeUnmount
  • unmounted

Напишите пользовательскую директиву:

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


Используйте эту директиву:

const App = {
  setup() {
    return () => {
      return h('div', [
        // 调用 withDirectives 函数
        withDirectives(h('h1', 'hahah'), [
          // 四个参数分别是:指令、值、参数、修饰符
          [myDir, 10, 'arg', { foo: true }]
        ])
      ])
    }
  }
}


Элемент может связывать несколько директив:

const App = {
  setup() {
    return () => {
      return h('div', [
        // 调用 withDirectives 函数
        withDirectives(h('h1', 'hahah'), [
          // 四个参数分别是:指令、值、参数、修饰符
          [myDir, 10, 'arg', { foo: true }],
          [myDir2, 10, 'arg', { foo: true }],
          [myDir3, 10, 'arg', { foo: true }]
        ])
      ])
    }
  }
}

Увеличьте статические PROPS

Как упоминалось ранее, продвижение статических узлов осуществляется в единицах деревьев, еслиVNodeЕсть нестатические дочерние узлы, тогдаVNodeЭто не статично, это не продвигается. но этоVNodeизpropsможет быть статическим, что позволяет намpropsапгрейд, который также экономитVNodeНакладные расходы на создание объекта, объем памяти и т. д., например:

<div>
    <p foo="bar" a=b>{{ text }}</p>
</div>

в этом шаблонеpМетки имеют динамическое текстовое содержимое и поэтому не могут быть подняты, ноpВсе атрибуты тега являются статическими, поэтому его атрибуты можно продвигать.После продвижения его функция рендеринга выглядит следующим образом:

const hoistProp = { foo: 'bar', a: 'b' }

render(ctx) {
    return (openBlock(), createBlock('div', null, [
        createVNode('p', hoistProp, ctx.text)
    ]))
}


Даже если значение свойства динамически привязано, но если значениепостоянный, то он тоже будет повышен:

<p :foo="10" :bar="'abc' + 'def'">{{ text }}</p>

'abc' + 'def'является константой и может быть поднята.

предварительно натягивать

статически приподнятыйVNodeУзел или дерево узлов сами по себе являются статическими, поэтому можно ли их предварительно преобразовать в строки? Как показано в следующем шаблоне:

<div>
    <p></p>
    <p></p>
    ...20 个 p 标签
    <p></p>
</div>

Предполагая, что существует большое количество непрерывных статическихpярлык, когдаhoistПри оптимизации результаты следующие:

cosnt hoist1 = createVNode('p', null, null, PatchFlags.HOISTED)
cosnt hoist2 = createVNode('p', null, null, PatchFlags.HOISTED)
... 20 个 hoistx 变量
cosnt hoist20 = createVNode('p', null, null, PatchFlags.HOISTED)

render() {
    return (openBlock(), createBlock('div', null, [
        hoist1, hoist2, ...20 个变量, hoist20
    ]))
}


Предварительная обработка строк сериализует эти статические узлы в строки и генерируетStaticТипVNode:

const hoistStatic = createStaticVNode('<p></p><p></p><p></p>...20个...<p></p>')

render() {
    return (openBlock(), createBlock('div', null, [
       hoistStatic
    ]))
}


Это имеет несколько явных преимуществ:

  • Уменьшен размер сгенерированного кода
  • Уменьшите накладные расходы на создание VNodes
  • Уменьшить использование памяти

Статические узлы проходят черезinnerHTMLдля создания реальных узлов, поэтому не все статические узлы являются предварительно строковыми, а статический узел, который может быть предварительно строковым, должен соответствовать следующим условиям:

  • Нетабличные метки: caption, thead, tr, th, tbody, td, tfoot, colgroup, col.

  • Атрибуты тега должны быть:

  • Стандартные атрибуты HTML:developer.Mozilla.org/en-US/docs/…

  • или атрибуты класса data-/aria-

Когда узел удовлетворяет этим условиям, это означает, что узел может быть предварительно стрингифицирован, но если есть только один узел, он не будет стрингифицирован, Струнные узлы должны быть непрерывными и достигать определенного количества:

  • Если узел не имеет атрибутов, то должно быть продолжение20Существует только один или несколько статических узлов, например:
<div>
    <p></p>
    <p></p>
    ... 20 个 p 标签
    <p></p>
</div>

или в этих последовательных узлах есть5и другие узлы — это узлы с привязками атрибутов:

<div>
    <p></p>
    <p></p>
    <p></p>
    <p></p>
    <p></p>
</div>

Хотя количество узлов в этом разделе не достигает 20, оно удовлетворяет 5 узлам с привязкой атрибутов.

Эти узлы не обязательно являются братьями и сестрами, а также возможны отношения родитель-потомок, если пороговое значение удовлетворяет условиям, например:

<div>
    <p>
        <p>
            <p>
                <p>
                    <p></p>
                </p>
            </p>
        </p>
    </p>
</div>

Prestringization вычисляет значение свойства во время компиляции,Например:

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

После строки с:

const hoistStatic = createStaticVNode('<p></p><p></p>.....<p></p>')

видимыйidСтоимость имущества рассчитывается.

Cache Event handler

Шаблон для следующего компонента выглядит следующим образом:

<Comp @change="a + b" />

Этот шаблон эквивалентен:

render(ctx) {
    return h(Comp, {
        onChange: () => (ctx.a + ctx.b)
    })
}


Очевидно, каждый разrenderКогда функция выполняется,Compкомпонентpropsэто новые объекты,onChangeЭто также будет совершенно новая функция. Это вызовет триггерCompобновление компонентов.

Когда компилятор Vue3 включенprefixIdentifiersтак же какcacheHandlers, этот шаблон будет скомпилирован в:

render(ctx, cache) {
    return h(Comp, {
        onChange: cache[0] || (cache[0] = ($event) => (ctx.a + ctx.b))
    })
}


Таким образом, он не сработает, даже если функция рендеринга вызывается несколько раз.Compобновление компонента, потому чтоVueсуществуетpatchсравнение этаповpropsбудет найдено, когдаonChangeЦитаты не изменились.

в коде вышеrenderфункциональныйcacheобъектVueМассив, который вводится внутри при вызове функции рендеринга, например:

render.call(ctx, ctx, [])

На самом деле, мы можем писать вручную даже без компиляции.cacheКод способности:

const Comp = {
    setup() {
        // 在 setup 中定义 handler
        const handleChange = () => {/* ... */}
        return () => {
            return h(AnthorComp, {
                onChange: handleChange  // 引用不变
            })
        }
    }
}

Поэтому нам лучше не писать такой код:

const Comp = {
    setup() {
        return () => {
            return h(AnthorComp, {
                onChang(){/*...*/}  // 每次渲染函数执行,都是全新的函数
            })
        }
    }
}


v-once

ЭтоVue2Что касается поддерживаемых функций,v-onceЭто "очень инструктивная" инструкция, потому что она видна компилятору, когда компилятор встречаетv-once, будет использовать то, что мы только что сказалиcacheЧтобы кэшировать результаты выполнения всей или части функции рендеринга, например следующий шаблон:

<div>
    <div v-once>{{ foo }}</div>
</div>

будет скомпилирован в:

render(ctx, cache) {
    return (openBlock(), createBlock('div', null, [
        cache[1] || (cache[1] = h("div", null, ctx.foo, 1 /* TEXT */))
    ]))
}

Это кеширует этоvnode. теперь, когдаvnodeБыл кэширован, и последующие обновления будут считывать кэшированный контент без повторного созданияvnodeобъект, и этот vnode не нужен в процессе Diff, поэтому вы обычно видите скомпилированный код ближе к следующему:

render(ctx, cache) {
    return (openBlock(), createBlock('div', null, [
        cache[1] || (
            setBlockTracking(-1), // 阻止这段 VNode 被 Block 收集
            cache[1] = h("div", null, ctx.foo, 1 /* TEXT */),
            setBlockTracking(1), // 恢复
            cache[1] // 整个表达式的值
        )
    ]))
}

Чтобы немного объяснить этот код, мы уже объяснили, что такое «Дерево блоков», иopenBlock()а такжеcreateBlock()функция для созданияBlock. а такжеsetBlockTracking(-1)используется для приостановки действия сбора, поэтому вv-onceВы увидите это в коде, сгенерированном при компиляции, поэтому используйтеv-onceСодержимое пакета не будет собрано в родительскомBlock, не участвуйтеDiff.

так,v-onceПовышение производительности связано с двумя аспектами:

  • 1. Накладные расходы на создание VNode
  • 2. Бесполезные накладные расходы Diff

Но на самом деле через шаблон мы не компилируем, можно и кеш передатьVNodeуменьшитьVNodeНакладные расходы на создание:

const Comp = {
    setup() {
        // 缓存 content
        const content = h('div', 'xxxx')
        return () => {
            return h('section', content)
        }
    }
}


Но это позволяет избежать бесполезногоDiffнакладные расходы, так как мы не используемBlock Treeрежим оптимизации.

Здесь необходимо упомянуть: VNode повторно используется в Vue2.5.18+ и Vue3, например, мы можем использовать один и тот же узел VNode несколько раз в разных местах:

const Comp = {
    setup() {
        const content = h('div', 'xxxx')
        return () => {
            // 多次渲染 content
            return h('section', [content, content, content])
        }
    }
}

Рукописная высокопроизводительная функция рендеринга

Далее мы войдем в основную часть, где попробуем написать от руки функцию рендеринга оптимизированного режима.

Несколько небольших моментов, которые нужно помнить:

  1. ОдинBlockэто особыйVNode, можно понять, что это просто болееVNodeодин дополнительныйdynamicChildrenАтрибуты
  2. createBlock()функция иcreateVNode()Сигнатура вызова функции почти такая же, на самом делеcreateBlock()Внутри функции находится инкапсуляцияcreateVNode(), что еще раз доказывает, чтоBlockто естьVNode.
  3. вызовcreateBlock()СоздайтеBlockпозвонить доopenBlock()функция, обычно эти две функции появляются вместе с оператором запятой:
render() {
    return (openBlock(), createBlock('div'))
}


Дерево блоков является гибким:

В предыдущем введении корневой узел начинался сBlockРоль существует, но корневой узел не обязательно должен бытьBlock, мы можем открыть его в любом узлеBlock:

setup() {
    return () => {
        return h('div', [
            (openBlock(), createBlock('p', null, [/*...*/]))
        ])
    }
}


Это также возможно, потому что рендерерDiffв процессе, еслиVNodeс участиемdynamicChildrenсвойства, он автоматически перейдет в режим оптимизации.Но мы обычно позволяем корневому узлу действовать какBlockРоль.

Используйте PatchFlags правильно:

PatchFlagsИспользуется для пометки содержимого элемента, который необходимо обновить, например, когда элемент имеет динамическийclassПри привязке нам нужно использоватьPatchFlags.CLASSотметка:

const App = {
  setup() {
    const refOk = ref(true)
    
    return () => {
      return (openBlock(), createBlock('div', null, [
        createVNode('p', { class: { foo: refOk.value } }, 'hello', PatchFlags.CLASS) // 使用 CLASS 标记
      ]))
    }
  }
}

Если используется неправильный тег, обновление может завершиться ошибкой. Подробная информация об использовании тега приведена ниже:

  • PatchFlags.CLASS- когда есть динамикаclassиспользуется при привязке
  • PatchFlags.STYLE- когда есть динамикаstyleИспользуйте при привязке, например:
createVNode('p', { style: { color: refColor.value } }, 'hello', PatchFlags.STYLE)
  • PatchFlags.TEXT- Когда используются динамические текстовые узлы, например:
createVNode('p', null, refText.value, PatchFlags.TEXT)
  • PatchFlags.PROPS- когда естьclassа такжеstyleПри связывании свойств, отличных от динамических, например:
createVNode('p', { foo: refVal.value }, 'hello', PatchFlags.PROPS, ['foo'])

Здесь следует отметить, что помимо использованияPatchFlags.PROPSКроме того, укажите пятый параметр, массив, содержащий имена динамических свойств.

  • PatchFlags.FULL_PROPS- когда есть динамикаnameизpropsпри использовании, например:
createVNode('p', { [refKey.value]: 'val' }, 'hello', PatchFlags.FULL_PROPS)

на самом деле использоватьFULL_PROPSэквивалентноpropsизDiffс традициейDiffТакой же. На самом деле, если мы чувствуем, что умственное бремя тяжелое, мы можем использовать их все.FULL_PROPS, преимущества этого:

  • избегать неправильного использованияPatchFlagsвызванныйbug
  • При снижении умственной нагрузки, хотя потеряprops diffпреимущества производительности, но все равно наслаждайтесьBlock TreeПреимущества.

При наличии нескольких обновлений одновременно необходимоPatchFlagsпровестипобитовое ИЛИоперации, такие как:PatchFlags.CLASS | PatchFlags.STYLE.

Флаг NEED_PATCH

Зачем выносить этот знак отдельно, он особенный и требует нашего особого внимания. когда мы используемrefилиonVNodeXXXПри ожидании хуков (включая пользовательские директивы) необходимо использовать этот флаг, чтобы его мог использовать родитель.BlockКоллекция, подробные причины объясняются в разделе статического продвижения:

const App = {
  setup() {
    const refDom = ref(null)
    return () => {
      return (openBlock(), createBlock('div', null,[
        createVNode('p',
          {
            ref: refDom,
            onVnodeBeforeMount() {/* ... */}
          },
          null,
          PatchFlags.NEED_PATCH
        )
      ]))
    }
  }
}


Где необходимо использовать Block

В самом начале мы объяснили, что некоторые директивы могут привести к нестабильности структуры DOM, и для решения проблемы необходимо использовать блокировку. То же самое верно и для рукописных функций рендеринга:

  • Решение отделения использует блок:
const App = {
  setup() {
    const refOk = ref(true)
    return () => {
      return (openBlock(), createBlock('div', null, [
        refOk.value
          // 这里使用 Block
          ? (openBlock(), createBlock('div', { key: 0 }, [/* ... */]))
          : (openBlock(), createBlock('div', { key: 1 }, [/* ... */]))
      ]))
    }
  }
}


использовать здесьBlockПричина, по которой мы объяснили в предыдущей статье, но здесь необходимо подчеркнуть, заключается в том, что в дополнение к оценке ветвления, используйтеBlockКроме того, необходимо такжеBlockуказать разныеkeyПросто сделай это.

  • Списки используют Блок:

Когда мы отображаем списки, мы часто пишем такой код:

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

Это нормально в неоптимизированном режиме, но теперь мы используемBlock, я уже сказал почемуv-forНужно использоватьBlockПричина, представьте, когда мы выполняем следующий оператор для изменения данных:

obj.list.splice(0, 1)

Это приведет кBlockСобранные динамические узлы несовместимы и в конечном итогеDiffпоявляется проблема. Решение состоит в том, чтобы иметь весь список в видеBlock, то нам нужно использоватьFragment:

const App = {
  setup() {
    const obj = reactive({ list: [ { val: 1 }, { val: 2 } ] })

    return () => {
      return (openBlock(), createBlock('div', null, [
        // 创建一个 Fragment,并作为 Block 角色
        (openBlock(true), createBlock(Fragment, null,
          // 在这里渲染列表
          obj.list.map(item => {
            return createVNode('p', null, item.val, PatchFlags.TEXT)
          }),
          // 记得要指定正确的 PatchFlags
          PatchFlags.UNKEYED_FRAGMENT
        ))
      ]))
    }
  }
}


В заключение:

  • Для списков мы всегда должны использоватьFragment, и в качествеBlockхарактер
  • еслиFragmentизchildrenне указанkey, то должно бытьFragmentотметкаPatchFlags.UNKEYED_FRAGMENT. Соответственно, если указаноkeyдолжен ударитьPatchFlags.KEYED_FRAGMENT
  • обратите внимание, что вызовopenBlock(true)когда параметр передаетсяtrue, который представляет этоBlockне будет собиратьdynamicChildren, потому что либоKEYEDещеUNKEYEDизFragment, в Diff егоchildrenоткат к традиционному режиму Diff, поэтому нет необходимости собиратьdynamicChildren.

Еще один момент, который следует отметить здесь, это то, что в Diff Fragment, поскольку традиционный Diff откатывается, мы хотим как можно быстрее восстановить оптимизированный режим, и в то же время обеспечить управляемость последующих сборов, поэтому мы обычно делаемFragmentКаждый дочерний узел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
        ))
      ]))
    }
  }
}


Наконец, чтобы сказать вам стабильныйFragment, если вы можете быть уверены, что список никогда не изменится, например, вы можете быть увереныobj.listне изменилось, то вы должны использовать:PatchFlags.STABLE_FRAGMENTфлаг и вызовopenBlcok()Удалите параметр для представления коллекцииdynamicChildren:

const App = {
  setup() {
    const obj = reactive({ list: [ { val: 1 }, { val: 2 } ] })

    return () => {
      return (openBlock(), createBlock('div', null, [
        // 调用 openBlock() 不要传参
        (openBlock(), createBlock(Fragment, null,
          obj.list.map(item => {
            // 列表中的任何节点都不需要是 Block 角色
            return createVNode('p', null, item.val, PatchFlags.TEXT)
          }),
          // 稳定的片段
          PatchFlags.STABLE_FRAGMENT
        ))
      ]))
    }
  }
}


Как упоминалось в комментариях выше.

  • Элементы с динамическими ключами должны быть заблокированы

Как обсуждалось в разделе о статическом подъеме, когда элемент использует динамическоеkey, даже если другие аспекты двух элементов точно такие же, это два разных элемента, и их необходимо заменить.Block Treeдолжен быть вBlockроль существует, поэтому, если элемент использует динамическийkey, это должно бытьBlock:

const App = {
  setup() {
    const refKey = ref('foo')

    return () => {
      return (openBlock(), createBlock('div', null,[
        // 这里应该是 Block
        (openBlock(), createBlock('p', { key: refKey.value }))
      ]))
    }
  }
}


Это действительно необходимо, подробнее см.GitHub.com/v UE JS/v UE-вы….

Используйте подсказку слота

Мы упоминали в разделе «Стабильный фрагмент»slot hint, когда мы пишем содержимое слота для компонента, чтобы сообщить среде выполнения: «Мы смогли гарантировать структурную стабильность содержимого слота», нам нужно использоватьslot hint:

render() {
    return (openBlock(), createBlock(Comp, null, {
        default: () => [
            refVal.value
               ? (openBlock(), createBlock('p', ...)) 
               ? (openBlock(), createBlock('div', ...)) 
        ],
        // slot hint
        _: 1
    }))
}


Конечно, если вы не можете этого гарантировать или чувствуете тяжелую душевную нагрузку, то не пишите.hint.

Используйте подсказку $stable

$stable hintВ отличие от упомянутой ранее стратегии оптимизации, стратегия из предыдущей статьи предполагает, что средство визуализацииРежим оптимизацииработает под$stableИспользуется в неоптимизированном режиме, который является функцией рендеринга, которую мы обычно пишем. Итак, для чего это нужно? Как показано в следующем коде (с использованием демонстрации tsx):

export const App = defineComponent({
  name: 'App',
  setup() {
    const refVal = ref(true)

    return () => {
      refVal.value

      return (
        <Hello>
          {
            { default: () => [<p>hello</p>] }
          }
        </Hello>
      )
    }
  }
})


Как показано в приведенном выше коде, функция рендеринга читаетсяrefVal.valueЗначение установленного отношения коллекции зависимостей при измененииrefValзначение, триггеры<Hello>обновление компонента, но мы нашлиHelloкомпоненты никогдаpropsизмениться, а во-вторых, его содержимое слота статично, поэтому его не следует обновлять, тогда мы можем использовать$stable hint:

export const App = defineComponent({
  name: 'App',
  setup() {
    const refVal = ref(true)

    return () => {
      refVal.value

      return (
        <Hello>
          {
            { default: () => [<p>hello</p>], $stable: true } // 修改了这里
          }
        </Hello>
      )
    }
  }
})

Правильное использование DYNAMIC_SLOTS для компонентов

когда мы строим динамичноslots, это должен быть компонентVNodeуточнитьPatchFlags.DYNAMIC_SLOTS, иначе обновление не будет выполнено. что такое динамическая сборкаslotsШерстяная ткань? Обычно означает: зависит от текущегоscopeпеременная сборкаslots,Например:

render() {
    // 使用当前组件作用域的变量
    const slots ={}
    // 常见的场景
    // 情况一:条件判断
    if (refVal.value) {
        slots.header = () => [h('p', 'hello')]
    }
    // 情况二:循环
    refList.value.forEach(item => {
        slots[item.name] = () => [...]
    })
    // 情况三:动态 slot 名称,情况二包含情况三
    slots[refName.value] = () => [...]

    return (openBlock(), createBlock('div', null, [
        // 这里要使用 PatchFlags.DYNAMIC_SLOTS
        createVNode(Comp, null, slots, PatchFlags.DYNAMIC_SLOTS)
    ]))
}


Как упоминалось в комментариях выше.

Наверху, я не знаю, сколько студентов прибывает сюда, Не прекращайте учиться...

Добро пожаловать в мою личную учетную запись WeChat «HcySunYang». Давайте программировать в удовольствие вместе!