Этот материал - часть серии Читая исходный код Vue.
Мы разберём:
- Парсер
- Оптимизатор
- Генератор
Эта статья главное внимание уделяет части компиляции.
// `createCompilerCreator` позволяет создавать компиляторы, которые используют альтернативные
// parser/optimizer/codegen, например для серрверного рендеринга (SSR).
// Тут мы просто экспортирует компилятор по-умолчанию с параметрами по-умолчанию.
export const createCompiler = createCompilerCreator(function baseCompile(
template: string,
options: CompilerOptions,
): CompiledResult {
const ast = parse(template.trim(), options);
optimize(ast, options);
const code = generate(ast, options);
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns,
};
});
Согласно коду функции baseCompile
, шаблон сначала конвертируется в AST с помощью parse()
,после чего отправляется optimize()
и generate()
для создания render и staticRenderFns.
Парсер занимается забором шаблона и построением из него AST.AST - это Абстрактное синтаксическое дерево. До парсинга шаблон это просто строка. После парсинга - мы уже понимаем значение шаблона и преобразуем его в дерево, для последующего использования.
Открываем compiler/parser/index.js
, просматриваем код, фукнция parse()
определяет вспомогательные фукнции и вызывает parseHTML
для собственно парсинга.
Эта статья не ставит целью научить вас писать парсеры, так что опустим детали. Если вам правдо интересно - прочитайте весь файл.
Сейчас мы будем относится к парсеру как к чёрному ящику. Хотя мы не вдаёмся в детали, я все ещё хочу показать вам окончательное AST, сгенерированное парсером - это интересно и полезно для дальнейшего понимания.
Чтобы получить AST нам нужно немного изменить код Vue.
Если вы используете git для получения исходников Vue, ветка по-умолчанию - dev
, которая сгенерирует вам ошибку при попытке собрать Vue и использовать в своём проекте.
If you use Git to clone Vue, the default branch is dev
, which will generate this error if you build Vue files and use them in your project:
- [email protected]
- [email protected]
Это может привести к неправильной работе. Убедитесь, что используете одну и ту же версию для обоих пунктов.
Мы должны переключиться на ветку master
, перейти на последний релиз и собрать библиотеку:
git checkout master
git pull
git checkout v2.3.4
npm install
npm run build
Код в ветке
master
немного отличается от кода в веткеdev
, потому что вdev
самая последняя версия и не все изменения были вмержены вmaster
.
Добавьте одну строчку в baseCompiler()
:
...
const ast = parse(template.trim(), options)
console.log(ast) // <-- ДОБАВЬТЕ ЭТУ СТРОЧКУ
optimize(ast, options)
const code = generate(ast, options)
...
Вот маленькое демо для .vue
файла:
<template>
<div id="app">
{{ newName ? newName + 'true' : newName + 'false' }}
<span>This is static node</span>
</div>
</template>
<script>
export default {
name: 'app',
data() {
return {
name: 'foo',
};
},
computed: {
newName() {
return this.name + 'new!';
},
},
};
</script>
Если вы пишете тег <template>
внутри HTML файла, Vue скомпилирует его в рантайме (в процессе работы). Если вы помещаете его внутрь .vue
файла, то вовремя процесса сборки (ещё до размещения в интернете) будет использован vue-template-compiler
.
Запустите npm run build
для генерации изменённого проекта. Скопируйте package/vue-template-compiler/build.js
в на тестовый node_modules/vue-template-compiler
, и после используйте измененый vue-template-compiler для сборки проекта. Я в консоли получил вот это:
{
type:1,
tag:'div',
attrsList:[
{
name:'id',
value:'app'
}
],
attrsMap:{
id:'app'
},
parent:undefined,
children:[
{
type:2,
expression:'"\\n "+_s(newName ? newName + \'true\' : newName + \'false\')+"\\n "',
text:'\n {{ newName ? newName + \'true\' : newName + \'false\' }}\n '
},
{
type:1,
tag:'span',
attrsList:[
],
attrsMap:{
},
parent:[
Circular
],
children:[
Array
],
plain:true
}
],
plain:false,
attrs:[
{
name:'id',
value:'"app"'
}
]
}
Это AST, которое сгенерировал парсер. Его легко поймёт и человек и компьютер.
После парсинга Vue использует оптимизатор для извлечения статических частей. Почему? Потому что статические части не изменяются, так что мы можем их выделить и сделать наш процесс рендеринга легче.
Стандартный оптимизатор расположен в compiler/optimizer.js
. Он проходит по AST и находит наши статические части. Как и в случае с парсером - мы будем относиться к нему как к чёрному ящику и рассмотрим его результат.
...
const ast = parse(template.trim(), options)
console.log(ast) // <-- ДОБАВИЛИ СТРОЧКУ
optimize(ast, options)
console.log(ast) // <-- ДОБАВИЛИ СТРОЧКУ
const code = generate(ast, options)
...
Ниже приведён вывод второго вызова console.log()
:
{
type:1,
tag:'div',
attrsList:[
{
name:'id',
value:'app'
}
],
attrsMap:{
id:'app'
},
parent:undefined,
children:[
{
type:2,
expression:'"\\n "+_s(newName ? newName + \'true\' : newName + \'false\')+"\\n "',
text:'\n {{ newName ? newName + \'true\' : newName + \'false\' }}\n ',
static:false
},
{
type:1,
tag:'span',
attrsList:[
],
attrsMap:{
},
parent:[
Circular
],
children:[
Array
],
plain:true,
static:true,
staticInFor:false,
staticRoot:false
}
],
plain:false,
attrs:[
{
name:'id',
value:'"app"'
}
],
static:false,
staticRoot:false
}
Сравнив эти два AST, вы сможете сказать в чем разница. После оптимизаци, все узлы в AST помечены статическими флагами. А где они используются?
Ответ –– это
Генератор расположен в compiler/codegen/index.js
. И снова, давайте сосредоточимся на результате работы.
...
const ast = parse(template.trim(), options)
console.log(ast) // <-- ДОБАВИЛИ СТРОЧКУ
optimize(ast, options)
console.log(ast) // <-- ДОБАВИЛИ СТРОЧКУ
const code = generate(ast, options)
console.log(code) // <-- ДОБАВИЛИ СТРОЧКУ
...
{
render:'with(this){return _c(\'div\',{attrs:{"id":"app"}},[_v("\\n "+_s(newName ? newName + \'true\' : newName + \'false\')+"\\n "),_c(\'span\',[_v("This is static node")])])}',
staticRenderFns:[
]
}
Результат - строчка render
и массив staticRenderFns
. Кажется он не сгенерировал статически функции рендера для таких простых текстовых узлов.
render
- это строка, может быть слегка удивительно, но это разумно, если немного об этом подумать. Компилятор работает без окружение браузера, он просто принимает строчку и выдаёт скомпилированную строку.
Тут несколько важных моментов:
with(this)
. Повторный вызовrender
находится в строчкеvnode = render.call(vm._renderProxy, vm.$createElement)
, так что егоthis
ссылается на компонент. Именно поэтому мы можем в шаблоне обращаться к свойствам безthis
._c
и_v
. Что это? После поиска по проекту мы можем найти что это две функции изvm._c
иVue.prototype._v
, задаваемые во времяinitRender()
. Почему_c
находится вvm
? Прочитайте комментарии над этой строчкой.- Обратите внимание, что реализация
_c
и_v
динамически зависит от платформы. Но их имена внутриrender
всегда один и те же. Это своего рода асбтракци, чтобы сделать ваш код более общим
Компилятор также имеет функцию с названием compileToFunction()
, она просто преобразовывает строчку render
в функцию и возвращает её.
Теперь вы значете, что такое компилятор, как он компилирует шаблон в окончательную функцию ренедера.
В следующей статие мы вернёмся в браузер и посмотрем как Vue использует сгенерированные функции render()
и __patch__()
для обновления страницы.
Читайте следующую часть: Ренедеринг - Patch.
Найтите определение vm._c
, разберите оригинальную реализацию и скажите, что возвращается при выполнении функции.