1. 前置知识
在 Vue 2 中,模板编译主要分为将模板字符串转换为抽象语法树(AST),再把 AST 转换为渲染函数,渲染函数返回虚拟 DOM(VNode),最后将虚拟 DOM 渲染为真实 DOM 这几个步骤。v-on 指令在这个流程里会被转换为 JavaScript 代码,从而为元素添加事件监听器。
2. 详细转换过程
步骤 1:模板解析为 AST
Vue 会把模板字符串解析成抽象语法树(AST)。对于模板 <button v-on:click="handleClick">Click me</button>,解析后的 AST 会包含元素节点、属性节点、事件节点等信息。以下是一个简化的 AST 表示:
{
"type":1,
"tag":"button",
"attrsList":[],
"attrsMap":{},
"events":{"click":"handleClick"},
"children":[{"type":3,"text":"Click me"}]
}
解析模板为 AST 的转换函数
function parseTemplate(template) {
template = template.trim();
let startIndex = template.indexOf('<') + 1;
let endIndex = template.indexOf(' ', startIndex);
if (endIndex === -1) {
endIndex = template.indexOf('>', startIndex);
}
const tag = template.slice(startIndex, endIndex);
const attrsList = [];
const attrsMap = {};
const events = {};
let attrStart = endIndex;
while (attrStart < template.length && template[attrStart] !== '>') {
// 查找下一个属性起始位置
attrStart = template.indexOf(' ', attrStart);
if (attrStart === -1 || attrStart >= template.indexOf('>', endIndex)) {
break;
}
// 找到属性名的结束位置(等号位置)
const attrEnd = template.indexOf('=', attrStart + 1);
if (attrEnd === -1) break;
const attrName = template.slice(attrStart + 1, attrEnd).trim();
if (!attrName) break;
// 找到属性值的起始和结束位置
const valueStart = template.indexOf('"', attrEnd) + 1;
const valueEnd = template.indexOf('"', valueStart);
// 如果没有找到引号,说明格式错误,退出循环
if (valueStart === 0 || valueEnd === -1) {
break;
}
const attrValue = template.slice(valueStart, valueEnd);
if (attrName.startsWith('v-on:')) {
const eventName = attrName.slice(5);
events[eventName] = attrValue;
} else {
attrsList.push({ name: attrName, value: attrValue });
attrsMap[attrName] = attrValue;
}
// 更新 attrStart 到属性值结束位置
attrStart = valueEnd;
}
const contentStart = template.indexOf('>', endIndex) + 1;
const contentEnd = template.lastIndexOf('</');
const textContent = template.slice(contentStart, contentEnd).trim();
const children = textContent ? [
{
type: 3,
text: textContent
}
] : [];
const ast = {
type: 1,
tag,
attrsList,
attrsMap,
events,
children
};
return ast;
}
const template = '<button v-on:click="handleClick">Click me</button>';
const ast = parseTemplate(template);
console.log(ast);
步骤 2:AST 转换为渲染函数
在这一步,Vue 会把 AST 转换为渲染函数。对于 v-on 指令,会将其转换为 JavaScript 代码来为元素添加事件监听器。以下是一个简化的渲染函数生成过程:
javascript
function generateRenderFunction(ast) {
const eventStr = Object.entries(ast.events).map(([eventName, handler]) => {
return `{ name: '${eventName}', handler: this.${handler} }`;
}).join(', ');
const childrenStr = ast.children.map(child => {
if (child.type === 3) {
return `_v('${child.text}')`;
}
// 这里省略了子元素节点的处理,仅处理文本节点
return '';
}).join(', ');
return `with(this) { return _c('${ast.tag}', { on: [${eventStr}] }, [${childrenStr}]) }`;
}
const ast = {
type: 1,
tag: 'button',
attrsList: [],
attrsMap: {},
events: {
click: 'handleClick'
},
children: [
{
type: 3,
text: 'Click me'
}
]
};
const renderFunction = generateRenderFunction(ast);
console.log(renderFunction);
上述代码中,generateRenderFunction 函数接收一个 AST 节点作为参数,遍历其事件列表,将事件信息转换为 JavaScript 代码。同时处理子节点,对于文本节点使用 _v 函数进行处理。最后返回一个渲染函数字符串:
with(this) { return _c('button', { on: [{ name: 'click', handler: this.handleClick }] }, [_v('Click me')]) }
步骤 3:执行渲染函数生成虚拟 DOM
当渲染函数被执行时,会根据当前组件的状态生成虚拟 DOM。以下是一个简化的虚拟 DOM 生成过程:
// 模拟 Vue 实例
const vm = {
handleClick() {
console.log('Button clicked!');
}
};
// 模拟 Vue 的 _c 和 _v 函数
function _c(tag, data, children) {
return {
tag,
data,
children
};
}
function _v(text) {
return {
type: 3,
text
};
}
// 执行渲染函数
const vnode = new Function(renderFunction).call(vm);
console.log(vnode);
在这个代码中,我们模拟了一个 Vue 实例 vm,包含 handleClick 方法。_c 函数用于创建虚拟 DOM 节点,_v 函数用于创建文本虚拟节点。通过 new Function(renderFunction).call(vm) 执行渲染函数,生成虚拟 DOM 节点:
{
"tag":"button",
"data":{"on":[{"name":"click"}]},
"children":[{"type":3,"text":"Click me"}]
}
步骤 4:虚拟 DOM 渲染为真实 DOM
最后,Vue 会比较新旧虚拟 DOM 的差异,只更新需要更新的真实 DOM 节点。对于 v-on 指令,会为按钮添加 handleClick 事件监听器。以下是一个简化的渲染过程:
function renderVNode(vnode) {
if (vnode.type === 3) {
return document.createTextNode(vnode.text);
}
const el = document.createElement(vnode.tag);
vnode.data.on.forEach(event => {
el.addEventListener(event.name, event.handler);
});
vnode.children.forEach(child => {
el.appendChild(renderVNode(child));
});
return el;
}
const realDom = renderVNode(vnode);
document.body.appendChild(realDom);
在这个代码中,renderVNode 函数接收一个虚拟 DOM 节点作为参数,对于文本节点创建文本节点,对于元素节点创建对应的真实 DOM 元素,并为其添加事件监听器,同时递归处理子节点。最后将真实 DOM 元素添加到页面中。
3. 转换后结果
最终渲染的 HTML 如下:
html
<button>Click me</button>
虽然 HTML 代码看起来没有变化,但实际上按钮已经绑定了 handleClick 方法,当点击按钮时会触发该方法。
注意事项
- 这个实现是一个非常简化的版本,仅适用于简单的模板字符串,对于复杂的模板(如嵌套标签、动态内容等)可能无法正常工作。
- 在实际的 Vue 源码中,模板解析要复杂得多,会处理各种情况,包括错误处理、指令解析等。