Модуль компиляции Vue состоит из 4 каталогов:
compiler-core
compiler-dom // 浏览器
compiler-sfc // 单文件组件
compiler-ssr // 服务端渲染
Модуль ядра компилятора является основным модулем для компиляции Vue и не зависит от платформы. Остальные три адаптированы под разные платформы на основе компилятора-ядра.
Компиляция Vue делится на три этапа: синтаксический анализ, преобразование и создание кода.
На этапе синтаксического анализа строка шаблона преобразуется в синтаксическое абстрактное дерево AST. Этап преобразования выполняет некоторую обработку преобразования AST. Этап codegen генерирует соответствующую строку функции рендеринга на основе AST.
Parse
Когда Vue анализирует строки шаблона, его можно разделить на два случая:<
строки, начинающиеся с и не начинающиеся с<
строка для начала.
Не<
Строка в начале имеет два случая: это текстовый узел или{{ exp }}
Интерполяционное выражение.
И с<
Начало строки делится на следующие ситуации:
- начальный тег элемента
<div>
- конечный тег элемента
</div>
- Узел комментариев
<!-- 123 -->
- Заявление о документации
<!DOCTYPE html>
В псевдокоде приблизительный процесс выглядит следующим образом:
while (s.length) {
if (startsWith(s, '{{')) {
// 如果以 '{{' 开头
node = parseInterpolation(context, mode)
} else if (s[0] === '<') {
// 以 < 标签开头
if (s[1] === '!') {
if (startsWith(s, '<!--')) {
// 注释
node = parseComment(context)
} else if (startsWith(s, '<!DOCTYPE')) {
// 文档声明,当成注释处理
node = parseBogusComment(context)
}
} else if (s[1] === '/') {
// 结束标签
parseTag(context, TagType.End, parent)
} else if (/[a-z]/i.test(s[1])) {
// 开始标签
node = parseElement(context, ancestors)
}
} else {
// 普通文本节点
node = parseText(context, mode)
}
}
Соответствующие функции в исходном коде:
-
parseChildren()
,главный вход. -
parseInterpolation()
, который анализирует интерполяционные выражения двойного цветка. -
parseComment()
, разбор комментариев. -
parseBogusComment()
, который анализирует объявление документа. -
parseTag()
, который анализирует теги. -
parseElement()
, который анализирует узел элемента, который внутренне выполняетparseTag()
. -
parseText()
, который анализирует обычный текст. -
parseAttribute()
, разбор свойств.
Каждый раз, когда анализируется узел метки, текста, комментария и т. д., Vue генерирует соответствующий узел AST иусекает проанализированную строку.
Строка усекается с помощьюadvanceBy(context, numberOfCharacters)
функция, context — объект контекста строки, numberOfCharacters — количество символов, которые нужно обрезать.
Давайте смоделируем операцию усечения на простом примере:
<div name="test">
<p></p>
</div>
Разобрать сначала<div
, затем выполнитеadvanceBy(context, 4)
выполнить операцию усечения (внутреннее выполнениеs = s.slice(4)
),стали:
name="test">
<p></p>
</div>
Затем проанализируйте атрибут и усеките его, чтобы он стал:
<p></p>
</div>
Точно так же следующие усечения:
></p>
</div>
</div>
<!-- 所有字符串已经解析完 -->
узел AST
Все определения узлов AST находятся в файлеcompile-core/ast.ts, вот определение узла элемента:
export interface BaseElementNode extends Node {
type: NodeTypes.ELEMENT // 类型
ns: Namespace // 命名空间 默认为 HTML,即 0
tag: string // 标签名
tagType: ElementTypes // 元素类型
isSelfClosing: boolean // 是否是自闭合标签 例如 <br/> <hr/>
props: Array<AttributeNode | DirectiveNode> // props 属性,包含 HTML 属性和指令
children: TemplateChildNode[] // 字节点
}
Были рассмотрены некоторые простые моменты, давайте рассмотрим более сложный пример, чтобы подробно объяснить процесс обработки синтаксического анализа.
<div name="test">
<!-- 这是注释 -->
<p>{{ test }}</p>
一个文本节点
<div>good job!</div>
</div>
Строка шаблона выше предполагает s и первый символ s[0]<
Вначале это означает, что это может быть только один из четырех только что упомянутых случаев.
Затем вам нужно снова посмотреть, что представляют собой символы s[1]:
- если
!
, вызывается нативный метод строкиstartsWith()
см. да'<!--'
начать с'<!DOCTYPE'
начало. Хотя соответствующие функции обработки этих двух различны, в конце концов они оба разрешаются как узлы комментариев. - если
/
, он обрабатывается в соответствии с конечным тегом. - если не
/
, он обрабатывается в соответствии с начальным тегом.
Из нашего примера это<div>
Вкладка «Пуск».
Здесь следует упомянуть еще одну вещь: Vue будет использовать стек стека для сохранения проанализированных тегов элементов. Когда он встречает начальную метку, он помещает метку в стек, а когда встречает конечную метку, он извлекает предыдущую метку из стека. Его роль заключается в сохранении тега элемента, который был проанализирован, но еще не проанализирован. У этого стека есть еще одна функция: при парсинге до определенной точки байта передатьstack[stack.length - 1]
Может получить свой родительский элемент.
В нашем примере он входит в стек и выходит из него следующим образом:
1. [div] // div 入栈
2. [div, p] // p 入栈
3. [div] // p 出栈
4. [div, div] // div 入栈
5. [div] // div 出栈
6. [] // 最后一个 div 出栈,模板字符串已解析完,这时栈为空
Тогда продолжайте анализировать наш пример выше, и мы уже знаем, что этоdiv
Тег уже проанализирован и будет проанализирован следующим<div
Строка усекается, а затем анализируется на предмет ее свойств.
Свойства Vue имеют два случая:
- Обычные атрибуты HTML
- Vue-директивы
Разные узлы генерируются в соответствии с разными атрибутами.Тип узла общего атрибута HTML равен 6, а тип узла инструкций Vue – 7.
Все значения типа узла следующие:
ROOT, // 根节点 0
ELEMENT, // 元素节点 1
TEXT, // 文本节点 2
COMMENT, // 注释节点 3
SIMPLE_EXPRESSION, // 表达式 4
INTERPOLATION, // 双花插值 {{ }} 5
ATTRIBUTE, // 属性 6
DIRECTIVE, // 指令 7
После анализа атрибутаdiv
Начальный тег анализируется,<div name="test">
Эта строка строки была усечена. Теперь оставшаяся строка выглядит следующим образом:
<!-- 这是注释 -->
<p>{{ test }}</p>
一个文本节点
<div>good job!</div>
</div>
Правила синтаксического анализа текста комментариев и обычных текстовых узлов очень просты, и они напрямую усекаются для создания узлов. Текстовый вызов аннотацииparseComment()
Обработка функций, вызов текстового узлаparseText()
иметь дело с.
Логика обработки строк интерполяции с двойной тратой немного сложнее, например, в примере{{ test }}
:
- Сначала извлеките содержимое в двойные фигурные скобки, то есть
test
, затем выполните на немtrim()
, чтобы удалить пробелы. - Затем будут сгенерированы два узла, один узел
INTERPOLATION
, тип равен 5, что указывает на то, что это интерполяция с двойной тратой. - Второй узел – это его содержимое, т.е.
test
, это создастSIMPLE_EXPRESSION
Узел с типом 4.
return {
type: NodeTypes.INTERPOLATION, // 双花插值类型
content: {
type: NodeTypes.SIMPLE_EXPRESSION,
isStatic: false, // 非静态节点
isConstant: false,
content,
loc: getSelection(context, innerStart, innerEnd)
},
loc: getSelection(context, start)
}
В остальном логика разбора строки аналогична вышеописанной, поэтому объяснять не буду, AST, разобранный в последнем примере, выглядит следующим образом:
Из AST мы также можем видеть, что некоторые узлы имеют некоторые другие атрибуты:
- ns, пространство имен, обычно HTML, значение равно 0.
- loc, это информация о позиции, указывающая позицию этого узла в исходной строке HTML, включая строку, столбец, смещение и другую информацию.
-
{{ test }}
Анализируемый узел будет иметь атрибут isStatic со значением false, что указывает на то, что это динамический узел. Если это статический узел, он будет сгенерирован только один раз, и тот же самый узел будет повторно использоваться на последующих этапах без сравнения различий.
Также есть свойство tagType, которое имеет 4 значения:
export const enum ElementTypes {
ELEMENT, // 0 元素节点
COMPONENT, // 1 组件
SLOT, // 2 插槽
TEMPLATE // 3 模板
}
Он в основном используется для различения вышеуказанных четырех типов узлов.
Transform
На этапе преобразования Vue выполнит некоторые операции по преобразованию AST, в основном добавляя различные параметры опций в соответствии с разными узлами AST, которые будут использоваться на этапе создания кода. Вот некоторые из наиболее важных параметров:
cacheHandlers
Если значение cacheHandlers равно true, это означает, что кеширование функции события включено. Например@click="foo"
Компилируется по умолчанию как{ onClick: foo }
, если эта опция включена, она компилируется в
{ onClick: _cache[0] || (_cache[0] = e => _ctx.foo(e)) }
hoistStatic
hoistStatic — это идентификатор, указывающий, следует ли включить продвижение статического узла. Если true, статические узлы будут повышены доrender()
генерируется вне функции и называется_hoisted_x
Переменная.
Например一个文本节点
Сгенерированный кодconst _hoisted_2 = /*#__PURE__*/_createTextVNode(" 一个文本节点 ")
.
Следующие две картинки, перваяhoistStatic = false
, с последующимhoistStatic = true
. каждый можетВеб-сайтПопробуй сам.
prefixIdentifiers
Этот параметр используется для генерации кода. Например{{ foo }}
Код, сгенерированный в модульном режиме,_ctx.foo
, а в функциональном режимеwith (this) { ... }
. Поскольку в модульном режиме по умолчанию используется строгий режим, оператор with нельзя использовать.
PatchFlags
Когда преобразование преобразует узел AST, он будет помечен параметром patchflag, который в основном используется для процесса сравнения различий. Когда DOM-узел имеет этот флаг и он больше 0, это означает, что его нужно обновить, иначе он будет пропущен.
Давайте посмотрим на диапазон значений patchflag:
export const enum PatchFlags {
// 动态文本节点
TEXT = 1,
// 动态 class
CLASS = 1 << 1, // 2
// 动态 style
STYLE = 1 << 2, // 4
// 动态属性,但不包含类名和样式
// 如果是组件,则可以包含类名和样式
PROPS = 1 << 3, // 8
// 具有动态 key 属性,当 key 改变时,需要进行完整的 diff 比较。
FULL_PROPS = 1 << 4, // 16
// 带有监听事件的节点
HYDRATE_EVENTS = 1 << 5, // 32
// 一个不会改变子节点顺序的 fragment
STABLE_FRAGMENT = 1 << 6, // 64
// 带有 key 属性的 fragment 或部分子字节有 key
KEYED_FRAGMENT = 1 << 7, // 128
// 子节点没有 key 的 fragment
UNKEYED_FRAGMENT = 1 << 8, // 256
// 一个节点只会进行非 props 比较
NEED_PATCH = 1 << 9, // 512
// 动态 slot
DYNAMIC_SLOTS = 1 << 10, // 1024
// 静态节点
HOISTED = -1,
// 指示在 diff 过程应该要退出优化模式
BAIL = -2
}
Как видно из приведенного выше кода, patchflag использует 11-битное растровое изображение для представления различных значений, каждое из которых имеет разное значение. Vue использует разные методы исправления в соответствии с разными флагами исправления во время процесса сравнения.
Следующее изображение представляет собой AST после преобразования:
Вы можете видеть, что codegenNode, helpers и hoists были заполнены соответствующими значениями. codegenNode — это данные, используемые для генерации кода, hoists хранит статические узлы, а помощники хранят имя функции (фактически Symbol), которая создает VNode.
Прежде чем официально начать трансформацию, вам нужно создать transformContext, то есть трансформировать контекст. Данные и методы, связанные с этими тремя свойствами, следующие:
helpers: new Set(),
hoists: [],
// methods
helper(name) {
context.helpers.add(name)
return name
},
helperString(name) {
return `_${helperNameMap[context.helper(name)]}`
},
hoist(exp) {
context.hoists.push(exp)
const identifier = createSimpleExpression(
`_hoisted_${context.hoists.length}`,
false,
exp.loc,
true
)
identifier.hoisted = exp
return identifier
},
Давайте посмотрим, на что похож конкретный процесс преобразования, используя<p>{{ test }}</p>
Например.
Этот узел соответствуетtransformElement()
функция преобразования, так какp
Никакие динамические свойства не привязаны, никакие директивы не привязаны, поэтому основное внимание уделяется не им, а{{ test }}
начальство.{{ test }}
является интерполяционным выражением двойного цветка, поэтому установите для его patchFlag значение 1 (динамический текстовый узел), и соответствующий код выполнения будетpatchFlag |= 1
. затем выполнитьcreateVNodeCall()
функция, и ее возвращаемое значение является значением codegenNode этого узла.
node.codegenNode = createVNodeCall(
context,
vnodeTag,
vnodeProps,
vnodeChildren,
vnodePatchFlag,
vnodeDynamicProps,
vnodeDirectives,
!!shouldUseBlock,
false /* disableTracking */,
node.loc
)
createVNodeCall()
Добавлен на основе этого узлаcreateVNode
Символ Symbol, который размещается в хелперах. По сути, это вспомогательная функция, которую нужно ввести на этапе генерации кода.
// createVNodeCall() 内部执行过程,已删除多余的代码
context.helper(CREATE_VNODE)
return {
type: NodeTypes.VNODE_CALL,
tag,
props,
children,
patchFlag,
dynamicProps,
directives,
isBlock,
disableTracking,
loc
}
hoists
Добавление узла к подъемникам в основном зависит от того, является ли он статическим узлом и необходимо ли установить для hoistStatic значение true.
<div name="test"> // 属性静态节点
<!-- 这是注释 -->
<p>{{ test }}</p>
一个文本节点 // 静态节点
<div>good job!</div> // 静态节点
</div>
Как видите, выше есть три статических узла, поэтому массив hoists имеет 3 значения. И независимо от того, насколько глубоко вложены статические узлы, они будут повышены до подъемов.
изменение типа
Как видно из рисунка выше, тип самого внешнего div изначально был 1, а тип в codegenNode, сгенерированном преобразованием, стал 13.
Это 13 тип, соответствующий генерации кодаVNODE_CALL
. а также:
// codegen
VNODE_CALL, // 13
JS_CALL_EXPRESSION, // 14
JS_OBJECT_EXPRESSION, // 15
JS_PROPERTY, // 16
JS_ARRAY_EXPRESSION, // 17
JS_FUNCTION_EXPRESSION, // 18
JS_CONDITIONAL_EXPRESSION, // 19
JS_CACHE_EXPRESSION, // 20
Только что упомянутый пример{{ test }}
, его codegenNode вызываетсяcreateVNodeCall()
Сгенерировано:
return {
type: NodeTypes.VNODE_CALL,
tag,
props,
children,
patchFlag,
dynamicProps,
directives,
isBlock,
disableTracking,
loc
}
Как видно из приведенного выше кода, для типа установлено значение NodeTypes.VNODE_CALL, равное 13.
Каждые разные узлы обрабатываются разными функциями преобразования.Из-за ограниченного места проверьте это самостоятельно.
Codegen
На этапе генерации кода наконец-то генерируется строка. Мы удаляем двойные цитаты из строки, чтобы увидеть, какой конкретный контент:
const _Vue = Vue
const { createVNode: _createVNode, createCommentVNode: _createCommentVNode, createTextVNode: _createTextVNode } = _Vue
const _hoisted_1 = { name: "test" }
const _hoisted_2 = /*#__PURE__*/_createTextVNode(" 一个文本节点 ")
const _hoisted_3 = /*#__PURE__*/_createVNode("div", null, "good job!", -1 /* HOISTED */)
return function render(_ctx, _cache) {
with (_ctx) {
const { createCommentVNode: _createCommentVNode, toDisplayString: _toDisplayString, createVNode: _createVNode, createTextVNode: _createTextVNode, openBlock: _openBlock, createBlock: _createBlock } = _Vue
return (_openBlock(), _createBlock("div", _hoisted_1, [
_createCommentVNode(" 这是注释 "),
_createVNode("p", null, _toDisplayString(test), 1 /* TEXT */),
_hoisted_2,
_hoisted_3
]))
}
}
режим генерации кода
Вы можете видеть, что приведенный выше код, наконец, возвращаетrender()
Функция заключается в создании соответствующего VNode.
Фактически существует два режима генерации кода: модульный и функциональный, режим которого определяется идентификатором prefixIdentifiers.
Особенности функционального режима: использованиеconst { helpers... } = Vue
способ ввести вспомогательные функции, т. е.createVode()
createCommentVNode()
эти функции. экспорт с использованиемreturn
вернуть весьrender()
функция.
Характеристики режима модуля: использовать модули es6 для импорта и экспорта функций, то есть использовать импорт и экспорт.
статический узел
Кроме того, есть три переменные, которые используют_hoisted_
Имя, за которым следует число, представляющее первые несколько статических переменных.
Еще раз взгляните на строку шаблона HTML на этапе синтаксического анализа:
<div name="test">
<!-- 这是注释 -->
<p>{{ test }}</p>
一个文本节点
<div>good job!</div>
</div>
В этом примере есть только один динамический узел, т.е.{{ test }}
, остальные все статические узлы. Из сгенерированного кода также видно, что существует однозначное соответствие между сгенерированным узлом и кодом в шаблоне. Роль статических узлов состоит в том, чтобы сгенерировать их только один раз и напрямую использовать повторно в будущем.
Внимательные пользователи сети могли обнаружить_hoisted_2
а также_hoisted_3
переменная имеет/*#__PURE__*/
Примечания.
Цель этой аннотации — указать, что эта функция является чистой функцией без побочных эффектов и в основном используется для встряхивания деревьев. Инструменты сжатия будут удалять неиспользуемый код напрямую (встряхивая) при упаковке.
Давайте посмотрим на создание динамических узлов{{ test }}
код:_createVNode("p", null, _toDisplayString(test), 1 /* TEXT */)
.
в_toDisplayString(test)
Внутренняя реализация:
return val == null
? ''
: isObject(val)
? JSON.stringify(val, replacer, 2)
: String(val)
Код очень простой, он преобразуется в строковый вывод.
а также_createVNode("p", null, _toDisplayString(test), 1 /* TEXT */)
Последний параметр 1 — это флаг исправления, добавленный преобразованием.
помощники вспомогательной функции
На двух этапах преобразования и кодегена мы можем видеть тень хелперов.Для чего используются хелперы?
// Name mapping for runtime helpers that need to be imported from 'vue' in
// generated code. Make sure these are correctly exported in the runtime!
// Using `any` here because TS doesn't allow symbols as index type.
export const helperNameMap: any = {
[FRAGMENT]: `Fragment`,
[TELEPORT]: `Teleport`,
[SUSPENSE]: `Suspense`,
[KEEP_ALIVE]: `KeepAlive`,
[BASE_TRANSITION]: `BaseTransition`,
[OPEN_BLOCK]: `openBlock`,
[CREATE_BLOCK]: `createBlock`,
[CREATE_VNODE]: `createVNode`,
[CREATE_COMMENT]: `createCommentVNode`,
[CREATE_TEXT]: `createTextVNode`,
[CREATE_STATIC]: `createStaticVNode`,
[RESOLVE_COMPONENT]: `resolveComponent`,
[RESOLVE_DYNAMIC_COMPONENT]: `resolveDynamicComponent`,
[RESOLVE_DIRECTIVE]: `resolveDirective`,
[WITH_DIRECTIVES]: `withDirectives`,
[RENDER_LIST]: `renderList`,
[RENDER_SLOT]: `renderSlot`,
[CREATE_SLOTS]: `createSlots`,
[TO_DISPLAY_STRING]: `toDisplayString`,
[MERGE_PROPS]: `mergeProps`,
[TO_HANDLERS]: `toHandlers`,
[CAMELIZE]: `camelize`,
[CAPITALIZE]: `capitalize`,
[SET_BLOCK_TRACKING]: `setBlockTracking`,
[PUSH_SCOPE_ID]: `pushScopeId`,
[POP_SCOPE_ID]: `popScopeId`,
[WITH_SCOPE_ID]: `withScopeId`,
[WITH_CTX]: `withCtx`
}
export function registerRuntimeHelpers(helpers: any) {
Object.getOwnPropertySymbols(helpers).forEach(s => {
helperNameMap[s] = helpers[s]
})
}
На самом деле вспомогательные функции — это некоторые функции, введенные из Vue во время генерации кода, чтобы позволить программе нормально выполняться, как видно из кода, сгенерированного выше. А helperNameMap — это имя карты по умолчанию, которое является именем функции, импортируемой из Vue.
Кроме того, мы также можем видеть функцию регистрацииregisterRuntimeHelpers(helpers: any()
, для чего это используется?
Мы знаем, что модуль компиляции «compiler-core» не зависит от платформы, а «compile-dom» — это модуль компиляции, связанный с браузером. Для нормального запуска программы Vue в браузере необходимо импортировать связанные с браузером данные и функции Vue.registerRuntimeHelpers(helpers: any()
Он используется для этого, как видно из файла runtimeHelpers.ts компилятора:
registerRuntimeHelpers({
[V_MODEL_RADIO]: `vModelRadio`,
[V_MODEL_CHECKBOX]: `vModelCheckbox`,
[V_MODEL_TEXT]: `vModelText`,
[V_MODEL_SELECT]: `vModelSelect`,
[V_MODEL_DYNAMIC]: `vModelDynamic`,
[V_ON_WITH_MODIFIERS]: `withModifiers`,
[V_ON_WITH_KEYS]: `withKeys`,
[V_SHOW]: `vShow`,
[TRANSITION]: `Transition`,
[TRANSITION_GROUP]: `TransitionGroup`
})
он работаетregisterRuntimeHelpers(helpers: any()
, который внедряет некоторые функции, связанные с браузером, в таблицу сопоставления.
Как используются помощники??
На этапе синтаксического анализа соответствующие типы генерируются при анализе различных узлов.
На этапе преобразования создается помощник, представляющий собой заданную структуру данных. Всякий раз, когда он преобразует AST, он добавляет различные вспомогательные функции в зависимости от типа узла AST.
Например, если предположить, что сейчас он преобразует узел комментария, он выполнитcontext.helper(CREATE_COMMENT)
Внутреннее достижение эквивалентноhelpers.add('createCommentVNode')
. Затем на этапе codegen пройдитесь по помощникам и импортируйте функции, необходимые программе из Vue, Код реализован следующим образом:
// 这是 module 模式
`import { ${ast.helpers
.map(s => `${helperNameMap[s]} as _${helperNameMap[s]}`)
.join(', ')} } from ${JSON.stringify(runtimeModuleName)}\n`
Как сгенерировать код?
Из файла codegen.ts вы можете увидеть множество функций генерации кода:
generate() // 代码生成入口文件
genFunctionExpression() // 生成函数表达式
genNode() // 生成 Vnode 节点
...
Генерация кода заключается в вызове различных функций генерации кода в соответствии с разными узлами AST и, наконец, в объединении строк кода для вывода полной строки кода.
Старые правила, давайте рассмотрим пример:
const _hoisted_1 = { name: "test" }
const _hoisted_2 = /*#__PURE__*/_createTextVNode(" 一个文本节点 ")
const _hoisted_3 = /*#__PURE__*/_createVNode("div", null, "good job!", -1 /* HOISTED */)
Посмотрите, как генерируется этот код, сначала выполнитеgenHoists(ast.hoists, context)
, с подъемами массива статических узлов, сгенерированными преобразованием, в качестве первого аргумента.genHoists()
Внутренняя реализация:
hoists.forEach((exp, i) => {
if (exp) {
push(`const _hoisted_${i + 1} = `);
genNode(exp, context);
newline();
}
})
Как видно из приведенного выше кода, пройдитесь по массиву hoists и вызовитеgenNode(exp, context)
.genNode()
Выполнение различных функций в соответствии с различными типами.
const _hoisted_1 = { name: "test" }
в этой строке кодаconst _hoisted_1 =
Зависит отgenHoists()
генерировать,{ name: "test" }
Зависит отgenObjectExpression()
генерировать.
Точно так же оставшиеся две строки кода генерируются таким же образом, но функция, которая в конечном итоге вызывается, отличается.