напишите в начале
Студенты, которые писали Vue, должны были испытать это на себе..vue
Насколько удобен этот однофайловый компонент. Но мы также знаем, что нижний слой Vue визуализируется через виртуальный DOM, затем.vue
Как именно шаблон файла преобразуется в виртуальный DOM? Этот кусок всегда был для меня черным ящиком, и я не изучал его глубоко, и я собираюсь выяснить это сегодня.
Скоро выйдет Vue 3, изначально я хотел напрямую посмотреть на компиляцию шаблонов Vue 3, но когда я открыл исходный код Vue 3, то обнаружил, что даже не знаю, как Vue 2 компилирует шаблоны. Лу Синь говорил нам с юных лет, что мы не можем съесть толстяка за один укус, тогда я могу только оглянуться назад на исходный код компиляции шаблона Vue 2. Что касается Vue 3, я подожду, пока он не будет официально выпущен. .
версия Вью
Когда многие люди используют Vue, они используют код шаблона, сгенерированный непосредственно vue-cli, но они не знают, что Vue на самом деле предоставляет две версии сборки.
-
vue.js
: Полная версия, включая возможность компилировать шаблоны; -
vue.runtime.js
: версия среды выполнения не предоставляет возможности компиляции шаблонов и должна быть скомпилирована заранее через vue-loader.
Проще говоря, если вы используете vue-loader, вы можете использоватьvue.runtime.min.js
, передать процесс компиляции шаблона в vue-loader, если вы передаете его прямо в браузереscript
Метки введены в Vue и должны использоватьсяvue.min.js
, который компилирует шаблон во время выполнения.
скомпилировать запись
Зная версию Vue, давайте взглянем на входной файл полной версии Vue (src/platforms/web/entry-runtime-with-compiler.js
).
// 省略了部分代码,只保留了关键部分
import { compileToFunctions } from './compiler/index'
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (el) {
const options = this.$options
// 如果没有 render 方法,则进行 template 编译
if (!options.render) {
let template = options.template
if (template) {
// 调用 compileToFunctions,编译 template,得到 render 方法
const { render, staticRenderFns } = compileToFunctions(template, {
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this)
// 这里的 render 方法就是生成生成虚拟 DOM 的方法
options.render = render
}
}
return mount.call(this, el, hydrating)
}
посмотри снова./compiler/index
документcompileToFunctions
Откуда метод.
import { baseOptions } from './options'
import { createCompiler } from 'compiler/index'
// 通过 createCompiler 方法生成编译函数
const { compile, compileToFunctions } = createCompiler(baseOptions)
export { compile, compileToFunctions }
Дальнейшая основная логика заключается вcompiler
В модуле эта часть немного запутана, потому что эта статья не для анализа исходного кода, поэтому весь исходный код выкладываться не будет. Вы только посмотрите на логику этого абзаца.
export function createCompiler(baseOptions) {
const baseCompile = (template, options) => {
// 解析 html,转化为 ast
const ast = parse(template.trim(), options)
// 优化 ast,标记静态节点
optimize(ast, options)
// 将 ast 转化为可执行代码
const code = generate(ast, options)
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
}
const compile = (template, options) => {
const tips = []
const errors = []
// 收集编译过程中的错误信息
options.warn = (msg, tip) => {
(tip ? tips : errors).push(msg)
}
// 编译
const compiled = baseCompile(template, options)
compiled.errors = errors
compiled.tips = tips
return compiled
}
const createCompileToFunctionFn = () => {
// 编译缓存
const cache = Object.create(null)
return (template, options, vm) => {
// 已编译模板直接走缓存
if (cache[template]) {
return cache[template]
}
const compiled = compile(template, options)
return (cache[key] = compiled)
}
}
return {
compile,
compileToFunctions: createCompileToFunctionFn(compile)
}
}
основной процесс
Вы можете видеть, что основная логика компиляции в основном находится вbaseCompile
Метод в основном делится на три этапа:
- Компиляция шаблона, преобразующая код шаблона в AST;
- Оптимизировать AST для облегчения последующих обновлений виртуального DOM;
- Генерировать код, конвертировать AST в исполняемый код;
const baseCompile = (template, options) => {
// 解析 html,转化为 ast
const ast = parse(template.trim(), options)
// 优化 ast,标记静态节点
optimize(ast, options)
// 将 ast 转化为可执行代码
const code = generate(ast, options)
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
}
parse
AST
Сначала ознакомьтесь с методом parse. Основная функция этого метода заключается в анализе HTML и преобразовании его в AST (абстрактное синтаксическое дерево). Учащиеся, знакомые с ESLint и Babel, должны быть знакомы с AST. Сначала мы можем увидеть, как выглядит AST. вроде после разбора.
Вот обычный шаблон Vue:
new Vue({
el: '#app',
template: `
<div>
<h2 v-if="message">{{message}}</h2>
<button @click="showName">showName</button>
</div>
`,
data: {
name: 'shenfq',
message: 'Hello Vue!'
},
methods: {
showName() {
alert(this.name)
}
}
})
AST после синтаксического анализа:
AST представляет собой объект с древовидной структурой, каждый слой представляет собой узел, первый слойdiv
(tag: "div"
).div
Все дочерние узлы находятся в дочернем свойстве, котороеh2
метки, пустые строки,button
Этикетка. Мы также можем заметить, что есть атрибут, используемый для обозначения типа узла: тип, здесьdiv
Тип 1, что означает, что это узел элемента.Существует три типа типа:
- узел элемента;
- выражение;
- текст;
существуетh2
а такжеbutton
Пустые строки между метками — это текстовые узлы типа 3, аh2
Под меткой находится узел выражения.
Разобрать HTML
Общая логика синтаксического анализа более сложная, мы можем сначала упростить код и увидеть процесс синтаксического анализа.
import { parseHTML } from './html-parser'
export function parse(template, options) {
let root
parseHTML(template, {
// some options...
start() {}, // 解析到标签位置开始的回调
end() {}, // 解析到标签位置结束的回调
chars() {}, // 解析到文本时的回调
comment() {} // 解析到注释时的回调
})
return root
}
Вы можете видеть, что синтаксический анализ в основном работает через parseHTML, который сам взят из библиотеки с открытым исходным кодом:htmlparser.js, но после некоторых изменений, внесенных командой Vue, связанные с этим проблемы были исправлены.
Давайте посмотрим на логику parseHTML.
export function parseHTML(html, options) {
let index = 0
let last,lastTag
const stack = []
while(html) {
last = html
let textEnd = html.indexOf('<')
// "<" 字符在当前 html 字符串开始位置
if (textEnd === 0) {
// 1、匹配到注释: <!-- -->
if (/^<!\--/.test(html)) {
const commentEnd = html.indexOf('-->')
if (commentEnd >= 0) {
// 调用 options.comment 回调,传入注释内容
options.comment(html.substring(4, commentEnd))
// 裁切掉注释部分
advance(commentEnd + 3)
continue
}
}
// 2、匹配到条件注释: <![if !IE]> <![endif]>
if (/^<!\[/.test(html)) {
// ... 逻辑与匹配到注释类似
}
// 3、匹配到 Doctype: <!DOCTYPE html>
const doctypeMatch = html.match(/^<!DOCTYPE [^>]+>/i)
if (doctypeMatch) {
// ... 逻辑与匹配到注释类似
}
// 4、匹配到结束标签: </div>
const endTagMatch = html.match(endTag)
if (endTagMatch) {}
// 5、匹配到开始标签: <div>
const startTagMatch = parseStartTag()
if (startTagMatch) {}
}
// "<" 字符在当前 html 字符串中间位置
let text, rest, next
if (textEnd > 0) {
// 提取中间字符
rest = html.slice(textEnd)
// 这一部分当成文本处理
text = html.substring(0, textEnd)
advance(textEnd)
}
// "<" 字符在当前 html 字符串中不存在
if (textEnd < 0) {
text = html
html = ''
}
// 如果存在 text 文本
// 调用 options.chars 回调,传入 text 文本
if (options.chars && text) {
// 字符相关回调
options.chars(text)
}
}
// 向前推进,裁切 html
function advance(n) {
index += n
html = html.substring(n)
}
}
Приведенный выше код представляет собой упрощенный parseHTML,while
В цикле каждый раз перехватывается фрагмент html-текста, а затем обрабатывается путем регулярной оценки типа текста, что похоже на конечный автомат, обычно используемый в принципе компиляции. каждый раз, когда вы получаете"<"
текст до и после символа,"<"
Символы перед символами обрабатываются как текст,"<"
Посредством регулярного суждения после персонажа можно рассчитать ограниченное количество состояний.
Другая логическая обработка не сложна, в основном начальный тег и конечный тег Давайте сначала посмотрим на закономерность, связанную с начальным тегом и конечным тегом.
const ncname = '[a-zA-Z_][\\w\\-\\.]*'
const qnameCapture = `((?:${ncname}\\:)?${ncname})`
const startTagOpen = new RegExp(`^<${qnameCapture}`)
Это регулярное выражение выглядит длинным, но разобраться в нем несложно. Вот рекомендацияОбычный визуализатор. Давайте посмотрим на startTagOpen в инструменте:
Смущает здесь то, почему существует tagName:
Это XMLПространства имен, который сейчас редко используется, мы можем его игнорировать напрямую, так что давайте упростим этот регуляр:
const ncname = '[a-zA-Z_][\\w\\-\\.]*'
const startTagOpen = new RegExp(`^<${ncname}`)
const startTagClose = /^\s*(\/?)>/
const endTag = new RegExp(`^<\\/${ncname}[^>]*>`)
В дополнение к вышеупомянутой регуляризации начала и конца тегов, есть также регуляризация для извлечения атрибутов тега, которая действительно вонючая и длинная.
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
Помещение регулярного выражения в инструмент делает его понятным с первого взгляда, с=
Для разграничения лицевая сторона — это имя свойства, а обратная сторона — значение свойства.
После уточнения регулярности для нас удобнее посмотреть на код позади.
while(html) {
last = html
let textEnd = html.indexOf('<')
// "<" 字符在当前 html 字符串开始位置
if (textEnd === 0) {
// some code ...
// 4、匹配到标签结束位置: </div>
const endTagMatch = html.match(endTag)
if (endTagMatch) {
const curIndex = index
advance(endTagMatch[0].length)
parseEndTag(endTagMatch[1], curIndex, index)
continue
}
// 5、匹配到标签开始位置: <div>
const startTagMatch = parseStartTag()
if (startTagMatch) {
handleStartTag(startTagMatch)
continue
}
}
}
// 向前推进,裁切 html
function advance(n) {
index += n
html = html.substring(n)
}
// 判断是否标签开始位置,如果是,则提取标签名以及相关属性
function parseStartTag () {
// 提取 <xxx
const start = html.match(startTagOpen)
if (start) {
const [fullStr, tag] = start
const match = {
attrs: [],
start: index,
tagName: tag,
}
advance(fullStr.length)
let end, attr
// 递归提取属性,直到出现 ">" 或 "/>" 字符
while (
!(end = html.match(startTagClose)) &&
(attr = html.match(attribute))
) {
advance(attr[0].length)
match.attrs.push(attr)
}
if (end) {
// 如果是 "/>" 表示单标签
match.unarySlash = end[1]
advance(end[0].length)
match.end = index
return match
}
}
}
// 处理开始标签
function handleStartTag (match) {
const tagName = match.tagName
const unary = match.unarySlash
const len = match.attrs.length
const attrs = new Array(len)
for (let i = 0; i < l; i++) {
const args = match.attrs[i]
// 这里的 3、4、5 分别对应三种不同复制属性的方式
// 3: attr="xxx" 双引号
// 4: attr='xxx' 单引号
// 5: attr=xxx 省略引号
const value = args[3] || args[4] || args[5] || ''
attrs[i] = {
name: args[1],
value
}
}
if (!unary) {
// 非单标签,入栈
stack.push({
tag: tagName,
lowerCasedTag:
tagName.toLowerCase(),
attrs: attrs
})
lastTag = tagName
}
if (options.start) {
// 开始标签的回调
options.start(tagName, attrs, unary, match.start, match.end)
}
}
// 处理闭合标签
function parseEndTag (tagName, start, end) {
let pos, lowerCasedTagName
if (start == null) start = index
if (end == null) end = index
if (tagName) {
lowerCasedTagName = tagName.toLowerCase()
}
// 在栈内查找相同类型的未闭合标签
if (tagName) {
for (pos = stack.length - 1; pos >= 0; pos--) {
if (stack[pos].lowerCasedTag === lowerCasedTagName) {
break
}
}
} else {
pos = 0
}
if (pos >= 0) {
// 关闭该标签内的未闭合标签,更新堆栈
for (let i = stack.length - 1; i >= pos; i--) {
if (options.end) {
// end 回调
options.end(stack[i].tag, start, end)
}
}
// 堆栈中删除已关闭标签
stack.length = pos
lastTag = pos && stack[pos - 1].tag
}
}
Когда метка анализируется, если этикетка не является одним тегом, метка помещается в стеке. Когда метка закрыта, то же самое название найдет с верхней части стека до тех пор, пока не найден та же метка имени. Закройте все этикетки над меткой контракта. Давайте возьмем пример:
<div>
<h2>test</h2>
<p>
<p>
</div>
После разбора открывающих тегов div и h2 в стеке осталось два элемента. После того, как h2 закрывается, h2 извлекается из стека. Затем анализируются два незакрытых тега p, и в этот момент в стеке есть три элемента (div, p, p). Если в это время будет проанализирован закрывающий тег div, в дополнение к закрытию div, два незакрытых тега p в div также будут закрыты, и стек будет очищен.
Для облегчения понимания специально была записана движущаяся картинка, а именно:
После выяснения логики parseHTML возвращаемся к месту вызова parseHTML, при вызове этого метода будет передано всего четыре коллбэка, соответствующие началу и концу тега, тексту и комментариям.
parseHTML(template, {
// some options...
// 解析到标签位置开始的回调
start(tag, attrs, unary) {},
// 解析到标签位置结束的回调
end(tag) {},
// 解析到文本时的回调
chars(text: string) {},
// 解析到注释时的回调
comment(text: string) {}
})
Обработка начальных тегов
Во-первых, при анализе начального тега будет создан узел AST, затем будут обработаны атрибуты тега и, наконец, узел AST будет помещен в древовидную структуру.
function makeAttrsMap(attrs) {
const map = {}
for (let i = 0, l = attrs.length; i < l; i++) {
const { name, value } = attrs[i]
map[name] = value
}
return map
}
function createASTElement(tag, attrs, parent) {
const attrsList = attrs
const attrsMap = makeAttrsMap(attrsList)
return {
type: 1, // 节点类型
tag, // 节点名称
attrsMap, // 节点属性映射
attrsList, // 节点属性数组
parent, // 父节点
children: [], // 子节点
}
}
const stack = []
let root // 根节点
let currentParent // 暂存当前的父节点
parseHTML(template, {
// some options...
// 解析到标签位置开始的回调
start(tag, attrs, unary) {
// 创建 AST 节点
let element = createASTElement(tag, attrs, currentParent)
// 处理指令: v-for v-if v-once
processFor(element)
processIf(element)
processOnce(element)
processElement(element, options)
// 处理 AST 树
// 根节点不存在,则设置该元素为根节点
if (!root) {
root = element
checkRootConstraints(root)
}
// 存在父节点
if (currentParent) {
// 将该元素推入父节点的子节点中
currentParent.children.push(element)
element.parent = currentParent
}
if (!unary) {
// 非单标签需要入栈,且切换当前父元素的位置
currentParent = element
stack.push(element)
}
}
})
Обработка конечных тегов
Логика конца тега относительно проста, просто удалите последний незакрытый тег в стеке и закройте его.
parseHTML(template, {
// some options...
// 解析到标签位置结束的回调
end() {
const element = stack[stack.length - 1]
const lastNode = element.children[element.children.length - 1]
// 处理尾部空格的情况
if (lastNode && lastNode.type === 3 && lastNode.text === ' ') {
element.children.pop()
}
// 出栈,重置当前的父节点
stack.length -= 1
currentParent = stack[stack.length - 1]
}
})
текст процесса
После обработки этикетки текст внутри этикетки также необходимо обработать. Существует два типа обработки текста: один — текст с выражениями, а другой — чистый статический текст.
parseHTML(template, {
// some options...
// 解析到文本时的回调
chars(text) {
if (!currentParent) {
// 文本节点外如果没有父节点则不处理
return
}
const children = currentParent.children
text = text.trim()
if (text) {
// parseText 用来解析表达式
// delimiters 表示表达式标识符,默认为 ['{{', '}}']
const res = parseText(text, delimiters))
if (res) {
// 表达式
children.push({
type: 2,
expression: res.expression,
tokens: res.tokens,
text
})
} else {
// 静态文本
children.push({
type: 3,
text
})
}
}
}
})
Давайте посмотрим, как parseText анализирует выражения.
// 构造匹配表达式的正则
const buildRegex = delimiters => {
const open = delimiters[0]
const close = delimiters[1]
return new RegExp(open + '((?:.|\\n)+?)' + close, 'g')
}
function parseText (text, delimiters){
// delimiters 默认为 {{ }}
const tagRE = buildRegex(delimiters || ['{{', '}}'])
// 未匹配到表达式,直接返回
if (!tagRE.test(text)) {
return
}
const tokens = []
const rawTokens = []
let lastIndex = tagRE.lastIndex = 0
let match, index, tokenValue
while ((match = tagRE.exec(text))) {
// 表达式开始的位置
index = match.index
// 提取表达式开始位置前面的静态字符,放入 token 中
if (index > lastIndex) {
rawTokens.push(tokenValue = text.slice(lastIndex, index))
tokens.push(JSON.stringify(tokenValue))
}
// 提取表达式内部的内容,使用 _s() 方法包裹
const exp = match[1].trim()
tokens.push(`_s(${exp})`)
rawTokens.push({ '@binding': exp })
lastIndex = index + match[0].length
}
// 表达式后面还有其他静态字符,放入 token 中
if (lastIndex < text.length) {
rawTokens.push(tokenValue = text.slice(lastIndex))
tokens.push(JSON.stringify(tokenValue))
}
return {
expression: tokens.join('+'),
tokens: rawTokens
}
}
Сначала извлеките выражение через обычный абзац:
Глядя на код, может быть немного сложно, давайте сразу перейдем к примеру, вот текст, содержащий выражение.
<div>是否登录:{{isLogin ? '是' : '否'}}</div>
optimize
С помощью некоторой обработки столбцов, описанной выше, мы получаем AST шаблона Vue. Поскольку Vue — адаптивный дизайн, после получения AST необходимо выполнить ряд оптимизаций, чтобы гарантировать, что статические данные не перейдут на фазу обновления виртуального DOM для оптимизации производительности.
export function optimize (root, options) {
if (!root) return
// 标记静态节点
markStatic(root)
}
Проще говоря, это установка для свойства static всех статических узлов значения true.
function isStatic (node) {
if (node.type === 2) { // 表达式,返回 false
return false
}
if (node.type === 3) { // 静态文本,返回 true
return true
}
// 此处省略了部分条件
return !!(
!node.hasBindings && // 没有动态绑定
!node.if && !node.for && // 没有 v-if/v-for
!isBuiltInTag(node.tag) && // 不是内置组件 slot/component
!isDirectChildOfTemplateFor(node) && // 不在 template for 循环内
Object.keys(node).every(isStaticKey) // 非静态节点
)
}
function markStatic (node) {
node.static = isStatic(node)
if (node.type === 1) {
// 如果是元素节点,需要遍历所有子节点
for (let i = 0, l = node.children.length; i < l; i++) {
const child = node.children[i]
markStatic(child)
if (!child.static) {
// 如果有一个子节点不是静态节点,则该节点也必须是动态的
node.static = false
}
}
}
}
generate
Когда у вас есть оптимизированный AST, вам нужно преобразовать AST в метод рендеринга. Все еще используя предыдущий шаблон, давайте посмотрим, как выглядит сгенерированный код:
<div>
<h2 v-if="message">{{message}}</h2>
<button @click="showName">showName</button>
</div>
{
render: "with(this){return _c('div',[(message)?_c('h2',[_v(_s(message))]):_e(),_v(" "),_c('button',{on:{"click":showName}},[_v("showName")])])}"
}
Разверните сгенерированный код:
with (this) {
return _c(
'div',
[
(message) ? _c('h2', [_v(_s(message))]) : _e(),
_v(' '),
_c('button', { on: { click: showName } }, [_v('showName')])
])
;
}
Должно быть, это очень сбивает с толку, увидев здесь кучу подчеркиваний._c
Соответствует виртуальному DOMcreateElement
метод. Другие методы подчеркивания находятся вcore/instance/render-helpers
В каждом методе есть определения, что каждый метод делает и не расширяет.
Конкретный метод преобразования представляет собой простое сращивание символов.Ниже приведена часть, которая упрощает логику, поэтому я не буду вдаваться в подробности.
export function generate(ast, options) {
const state = new CodegenState(options)
const code = ast ? genElement(ast, state) : '_c("div")'
return {
render: `with(this){return ${code}}`,
staticRenderFns: state.staticRenderFns
}
}
export function genElement (el, state) {
let code
const data = genData(el, state)
const children = genChildren(el, state, true)
code = `_c('${el.tag}'${
data ? `,${data}` : '' // data
}${
children ? `,${children}` : '' // children
})`
return code
}
Суммировать
Проясняется весь процесс компиляции шаблона Vue, и основное внимание уделяется части синтаксического анализа HTML для создания AST. В этой статье лишь кратко описан основной процесс, в котором опущены многие детали, такие как: обработка шаблона/слота, обработка инструкций и т. д. Если вы хотите узнать подробности, вы можете непосредственно прочитать исходный код. Я надеюсь, что вы все что-то получите после прочтения этой статьи.