第二篇:Vue模板编译原理(从HTML字符串到Render函数)
一、模板编译核心流程
Vue模板编译分为三个阶段:
- 解析(parse):将HTML字符串解析成AST抽象语法树
- 优化(optimize):标记AST中的静态节点(可选)
- 生成(generate):将AST转换成Render函数字符串
二、HTML解析成AST
1. 核心正则表达式
// compiler/parse-html.js
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*`
const qnameCapture = `((?:${ncname}\\:)?${ncname})`
const startTagOpen = new RegExp(`^<${qnameCapture}`) // 匹配开始标签
const startTagClose = /^\s*(\/?)>/ // 匹配标签结束
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`) // 匹配结束标签
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/ // 匹配属性
const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g // 匹配{{}}插值
2. AST节点类型
const ELEMENT_TYPE = 1 // 元素节点
const TEXT_TYPE = 3 // 文本节点
// 创建AST元素节点
function createASTElement(tag, attrs) {
return {
type: ELEMENT_TYPE,
tag,
attrs,
children: [],
parent: null
}
}
3. 解析主函数
// compiler/parse-html.js
export function parseHTML(html) {
let root // 根节点
let currentParent // 当前父节点
const stack = [] // 标签栈
// 处理开始标签
function start(tagName, attrs) {
const element = createASTElement(tagName, attrs)
if (!root) root = element
currentParent = element
stack.push(element)
}
// 处理结束标签
function end(tagName) {
const element = stack.pop()
currentParent = stack[stack.length - 1]
if (currentParent) {
element.parent = currentParent
currentParent.children.push(element)
}
}
// 处理文本
function chars(text) {
text = text.trim()
if (text) {
currentParent.children.push({
type: TEXT_TYPE,
text
})
}
}
// 截取字符串
function advance(n) {
html = html.substring(n)
}
// 解析开始标签
function parseStartTag() {
const start = html.match(startTagOpen)
if (start) {
const match = {
tagName: start[1],
attrs: []
}
advance(start[0].length)
// 解析属性
let end, attr
while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) {
advance(attr[0].length)
match.attrs.push({
name: attr[1],
value: attr[3] || attr[4] || attr[5] || true
})
}
if (end) {
advance(end[0].length)
}
return match
}
return false
}
// 主循环解析
while (html) {
const textEnd = html.indexOf('<')
if (textEnd === 0) {
// 解析开始标签
const startTagMatch = parseStartTag()
if (startTagMatch) {
start(startTagMatch.tagName, startTagMatch.attrs)
continue
}
// 解析结束标签
const endTagMatch = html.match(endTag)
if (endTagMatch) {
advance(endTagMatch[0].length)
end(endTagMatch[1])
continue
}
}
// 解析文本
let text
if (textEnd > 0) {
text = html.substring(0, textEnd)
}
if (text) {
advance(text.length)
chars(text)
}
}
return root
}
三、AST生成Render函数
1. 处理插值表达式
// compiler/generate.js
const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g
function genText(text) {
if (!defaultTagRE.test(text)) {
return `_v(${JSON.stringify(text)})`
}
let tokens = []
let lastIndex = 0
defaultTagRE.lastIndex = 0
let match
while (match = defaultTagRE.exec(text)) {
const index = match.index
if (index > lastIndex) {
tokens.push(JSON.stringify(text.slice(lastIndex, index)))
}
tokens.push(`_s(${match[1].trim()})`)
lastIndex = index + match[0].length
}
if (lastIndex < text.length) {
tokens.push(JSON.stringify(text.slice(lastIndex)))
}
return `_v(${tokens.join('+')})`
}
2. 处理子节点
function genChildren(el) {
const children = el.children
if (children && children.length) {
return children.map(child => gen(child)).join(',')
}
return false
}
function gen(node) {
if (node.type === 1) {
return generate(node)
} else {
return genText(node.text)
}
}
3. 处理属性
function genProps(attrs) {
let str = ''
for (let i = 0; i < attrs.length; i++) {
const attr = attrs[i]
if (attr.name === 'style') {
const styleObj = {}
attr.value.split(';').forEach(item => {
if (item) {
const [key, value] = item.split(':')
styleObj[key.trim()] = value.trim()
}
})
attr.value = styleObj
}
str += `${attr.name}:${JSON.stringify(attr.value)},`
}
return `{${str.slice(0, -1)}}`
}
4. 生成Render函数
export function generate(el) {
const children = genChildren(el)
let code = `_c('${el.tag}',${
el.attrs.length ? genProps(el.attrs) : 'undefined'
}${
children ? `,${children}` : ''
})`
return code
}
5. 编译入口
// compiler/index.js
import { parseHTML } from './parse-html'
import { generate } from './generate'
export function compileToFunction(template) {
// 1. 解析HTML成AST
const ast = parseHTML(template)
// 2. 生成Render函数字符串
const code = generate(ast)
// 3. 创建Render函数
const render = new Function(`with(this){return ${code}}`)
return render
}
四、Render函数执行
1. 虚拟DOM创建
// vdom/create-element.js
export function createElement(vm, tag, data = {}, ...children) {
return vnode(tag, data, data.key, children)
}
export function createTextNode(vm, text) {
return vnode(undefined, undefined, undefined, undefined, text)
}
function vnode(tag, data, key, children, text) {
return {
tag,
data,
key,
children,
text
}
}
2. 挂载Render方法
// render.js
import { createElement, createTextNode } from './vdom/create-element'
export function renderMixin(Vue) {
Vue.prototype._c = function(...args) {
return createElement(this, ...args)
}
Vue.prototype._v = function(text) {
return createTextNode(this, text)
}
Vue.prototype._s = function(val) {
return val == null ? '' : (typeof val === 'object' ? JSON.stringify(val) : val)
}
Vue.prototype._render = function() {
const vm = this
const { render } = vm.$options
const vnode = render.call(vm)
return vnode
}
}
五、编译流程总结
- 模板输入:
<div>{{name}}</div> - AST输出:生成抽象语法树
- Render函数:
_c('div',undefined,_v(_s(name))) - 虚拟DOM:执行Render生成VNode
- 真实DOM:将VNode渲染成真实DOM
下一篇我们将讲解Vue的Diff算法实现,继续深入Vue核心原理!
