[vue2 源代码] 模板编译
文章目录
- 一、mount 基本流程
- 二、执行 $mount 方法
- 三、模版编译
- 1、入口代码
- 2、parse
- 2.1 parseHTML
- 2.2 parseText
- 3、generate
- genElement 函数
- 4、createCompileToFunctionFn
- 4、mountComponent
一、mount 基本流程
在执行 _init (new Vue时) 的方法中,调用了 vm.$mount(vm.$options.el)
后的挂载流程:
- 通过 parse 将模版编译成抽象语法树 ast
- 将 ast 转成 render 函数
- 执行 render 生成 vnode
- 通过 mountComponent,执行 patch 将 vnode 变成真实 dom
二、执行 $mount 方法
源码位置: src/platforms/web/runtime-with-compiler.ts
// 保留了 Vue 原型上原始的 $mount 方法的引用
const mount = Vue.prototype.$mount
// 定义了一个新的 $mount 方法
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && query(el)
const options = this.$options
// resolve template/el and convert to render function
if (!options.render) {
let template = options.template
if (template) {
// ...
} else if (el) {
// @ts-expect-error
template = getOuterHTML(el)
}
if (template) {
// compileToFunctions 方法会将 template 编译成 render 函数
const { render, staticRenderFns } = compileToFunctions(
template,
{
// ...
},
this
)
options.render = render
}
}
// 调用原始 $mount
return mount.call(this, el, hydrating)
}
三、模版编译
1、入口代码
源码路径: src/compiler/index.ts
export const createCompiler = createCompilerCreator(function baseCompile(
template: string,
options: CompilerOptions
): CompiledResult {
// 解析模板字符串生成 AST
const ast = parse(template.trim(), options)
// 对AST进行优化
// ...
// 使用 generate 函数将 AST 转换为渲染函数的代码字符串。
// 这一步是将结构化的 AST 转换为实际可执行的 JavaScript 代码
const code = generate(ast, options)
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
})
2、parse
parse函数的作用:用于将模板字符串转换为抽象语法树(AST)
源码路径: src/compiler/parser/index.ts
基本结构:
export function parse(template: string, options: ComponentOptions) {
// console.log("模版解析 parse");
const stack: any[] = [];
let root; // 最终生成的 AST
let currentParent;
parseHTML(template, {
start(tag, attrs) {
// 当遇到标签起始处的处理,创建 AST 元素节点
let element: ASTElement = createASTElement(tag, attrs, currentParent);
if (!root) {
root = element;
}
currentParent = element;
processRawAttrs(element);
// 进栈
stack.push(element);
},
// 匹配到结束标签后的处理
end() {
// 当遇到标签结束处的处理
// 弹出栈,更新当前处理的父级节点
const element = stack[stack.length - 1];
stack.length -= 1;
currentParent = stack[stack.length - 1];
if (currentParent) {
currentParent.children.push(element);
}
},
chars(text: string) {
// 文本内容处理
const children = currentParent.children;
text = text.trim();
if (text) {
let child: ASTNode;
let res;
// parseText 的实现在下面)(2.2)
if (text !== " " && (res = parseText(text))) {
// 解析文本,这里是带有 {{}} 的情况
// console.log(res);
child = {
type: 2,
expression: res.expression,
tokens: res.tokens,
text,
};
} else {
// 文本节点
child = {
type: 3,
text,
};
}
if (child) {
children.push(child);
}
}
},
comment(text: string) {
// 注释的处理
}
});
return root;
}
2.1 parseHTML
parseHTML的工作原理基于正则表达式,逐步读取HTML字符串,并且根据标签的开始、结束、文本内容等来构建AST
源码路径:src/compiler/parser/html-parser.ts
下面是我自己手写的 parseHTML ,不考虑注释,自闭合标签等。
import { ASTAttr } from "src/types/compiler";
interface HTMLParserOptions {
start?: Function;
end?: Function;
chars?: Function;
comment?: (content: string) => void;
}
const attribute =
/^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/;
const startTagOpen = /^<([a-zA-Z_]+[0-9]*)/;
const startTagClose = /^\s*(\/?)>/;
const endTag = /^<\/([a-zA-Z_]+[0-9]*)>/;
export function parseHTML(html: string, options: HTMLParserOptions) {
const stack: any[] = [];
let index = 0; // 指针
let last; // 剩余部分
while (html) {
last = html;
let textEnd = html.indexOf("<");
if (textEnd == 0) {
// Comment:
// ...
// Doctype:
// ...
// 结束标签
const endTagMatch = html.match(endTag);
if (endTagMatch) {
advance(endTagMatch[0].length);
parseEndTag(endTagMatch[1]);
continue;
}
// 开始标签
const startTagMatch = parseStartTag();
if (startTagMatch) {
handleStartTag(startTagMatch);
continue;
}
}
let rest, text;
if (textEnd >= 0) {
// 标签内有文本
rest = html.slice(textEnd);
text = html.substring(0, textEnd);
}
// 处理标签内的文本
if (text) {
advance(text.length);
}
// 调用文本的处理
if (options.chars && text) {
options.chars(text);
}
if (html === last) {
index++;
html = html.substring(1);
}
}
function advance(n) {
index += n;
html = html.substring(n);
}
// 解析开始标签
function parseStartTag() {
const start = html.match(startTagOpen);
if (start) {
const match: any = {
tagName: start[1],
attrs: [],
start: index,
};
advance(start[0].length);
// 处理开始标签的属性
let attr, end;
while (
!(end = html.match(startTagClose)) &&
(attr = html.match(attribute))
) {
// 当不为 ">" 且匹配到属性时
attr.start = index;
advance(attr[0].length);
attr.end = index;
match.attrs.push(attr);
}
// 开始标签的 >
if (end) {
advance(end[0].length);
match.end = index;
return match;
}
}
}
// 处理开始标签
function handleStartTag(match) {
const tagName: string = match.tagName;
// 处理属性
const len = match.attrs.length;
const attrs: ASTAttr[] = new Array(len);
for (let i = 0; i < len; i++) {
const args = match.attrs[i];
const value = args[3] || args[4] || args[5] || "";
attrs[i] = {
name: args[1],
value: value,
};
}
// 开始标签进栈
stack.push({
tag: tagName,
lowerCasedTag: tagName.toLocaleLowerCase(),
attrs: attrs,
start: match.start,
end: match.end,
});
if (options.start) {
options.start(tagName, attrs, match.start, match.end);
}
}
// 解析结束标签
function parseEndTag(tagName: string) {
const lastStack = stack[stack.length - 1];
if (tagName && tagName.toLocaleLowerCase() === lastStack.lowerCasedTag) {
if (options.end) {
options.end(lastStack.tag);
}
stack.length = stack.length - 1;
}
}
}
2.2 parseText
parseText 函数是模板编译过程的一部分,用于解析文本节点中的插值表达式
源码路径:src/compiler/parser/text-parser.ts
// 解析给定文本text中的动态绑定表达式,并返回一个包含解析结果的对象
export function parseText(text: string): TextParseResult | void {
const tagRE = defaultTagRE;
const tokens: string[] = [];
const rawTokens: any[] = [];
// 定义一个 lastIndex 变量,用于记录上一次匹配的位置
let lastIndex = (tagRE.lastIndex = 0);
let match, index, tokenValue;
while ((match = tagRE.exec(text))) {
// 这里是匹配 {{ }}
index = match.index;
// 文本(这里是 {{}} 前面的文本)
if (index > lastIndex) {
rawTokens.push((tokenValue = text.slice(lastIndex, index)));
tokens.push(JSON.stringify(tokenValue));
}
debugger
// {{}} 中的内容
const exp = match[1].trim();
tokens.push(`_s(${exp})`);
rawTokens.push({ "@binding": exp });
lastIndex = index + match[0].length;
}
// 判断 lastIndex 变量是否小于文本长度,小于则代表 {{}} 后面还有文本
if (lastIndex < text.length) {
rawTokens.push((tokenValue = text.slice(lastIndex)));
tokens.push(JSON.stringify(tokenValue));
}
// return 生成示例:
// 比如:<div>msg: {{ message }}</div>
// 返回:
// {
// expression: "\"msg:\"+_s(message)",
// tokens: [
// "msg:",
// {
// "@binding": "message"
// }
// ]
// }
return {
expression: tokens.join("+"),
tokens: rawTokens,
};
}
3、generate
generate 函数的主要作用是基于给定的AST生成相应的JavaScript代码(渲染函数)
这个渲染函数将会返回一个虚拟节点(VNode)树,表示组件的DOM结构
源码路径:src/compiler/codegen/index.ts
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
}
}
code 生成的示例:
_c('div',{attrs:{"id":"app"}},[(show)?_c('h3',{staticClass:"active"},[_v("message: "+_s(message))]):_e()])
genElement 函数
主要职责是将抽象语法树(AST)的元素(Element)节点转换成字符串形式的渲染函数代码。该过程涉及到递归地处理元素的所有属性、指令和子节点,以确保能生成准确反映模板结构和逻辑的渲染函数代码
核心工作内容:
1、处理元素的属性和指令
genElement需要将元素上的所有属性(包括静态属性和动态绑定的属性)和指令(如v-if、v-for、v-model等)转换成 JavaScript 代码。对于指令,这通常意味着生成特定的代码来实现指令定义的行为。
2、处理子节点
对于每个元素节点,genElement还需要考虑其子节点。这包括:
- 递归地对子元素调用genElement,生成子元素的渲染函数代码。
- 将文本节点转换成_v(创建文本 VNode 的函数)调用。
- 将表达式节点转换成_s(toString 包装器)调用,以确保任何绑定的表达式都可以正确地转换成字符串。
3、生成渲染函数代码
最终,genElement需要生成类似于_c(‘div’, {…}, […])这样的函数调用代码。_c是创建元素 VNode 的函数,第一个参数是标签名,第二个参数是一个包含该元素所有属性和指令的数据对象,第三个参数是该元素的子节点数组。
4、处理插槽和组件
genElement还需要特别处理插槽和组件。
对于插槽,它需要生成_t(渲染插槽的函数)调用,并为插槽内容生成适当的代码。
对于组件,它需要根据组件定义生成_c(或特定于组件的创建函数,如果设置了functional标志)调用,并处理传递给组件的任何属性或事件监听器。
4、createCompileToFunctionFn
主要是将模板字符串编译成渲染函数,并且缓存了这个过程的结果以提高性能。
返回的compileToFunctions
函数的主要作用是将 Vue 模板字符串转换成最终的渲染函数
以下是简化的createCompileToFunctionFn函数的示意性解释:
function createCompileToFunctionFn(compile) {
const cache = Object.create(null);
return function compileToFunctions(template, options, vm) {
// 使用 options 和模板生成一个缓存的 key
const key = options ? (options.delimiters ? String(options.delimiters) + template : template) : template;
// 检查缓存中是否已经存在编译后的结果
if (cache[key]) {
return cache[key];
}
// 调用编译函数,将模板编译成 AST、优化后的 AST 和字符串形式的渲染函数
const compiled = compile(template, options);
// 将字符串形式的渲染函数转换成 JavaScript 函数
const res = {};
// 生成最终的渲染函数
res.render = new Function(compiled.render);
// 处理静态渲染函数,只有当使用了 v-once 指令时,这部分才不为空
const staticRenderFns = compiled.staticRenderFns.map(code => new Function(code));
res.staticRenderFns = staticRenderFns;
// 缓存结果并返回
cache[key] = res;
return res;
};
}
4、mountComponent
挂载组件的核心函数,它负责将一个 Vue 组件实例挂载到 DOM 上,并启动响应式更新机制,以便组件的状态改变时能自动更新对应的 DOM 表现
源码位置:src/core/instance/lifecycle.ts
核心代码:
function mountComponent(vm, el) {
// 设置 vm.$el 以引用真实 DOM 元素
vm.$el = el;
// 如果没有定义 render 函数,尝试编译模板生成一个
if (!vm.$options.render) {
compileToRenderFunction(vm);
}
// 调用 beforeMount 生命周期钩子
callHook(vm, 'beforeMount');
// 创建观察者:在数据变化时重新渲染组件
const updateComponent = () => {
vm._update(vm._render(), hydrating);
};
// 创建组件级观察者,传递 updateComponent 作为更新函数
new Watcher(vm, updateComponent, noop, {
before() {
callHook(vm, 'beforeUpdate');
}
}, true /* 表示这是一个组件观察者 */);
// 挂载完成,调用 mounted 生命周期钩子
if (vm.$vnode == null) {
vm._isMounted = true;
callHook(vm, 'mounted');
}
return vm;
}