一、模版编译过程
- vue 模版编译的主要作用是将模版(template)编译为渲染函数(render)
- vue 模版编译整体逻辑主要分为三步
- 将模板变成ast语法树 —— 解析器
- 对ast进行静态节点标记,主要用来做虚拟DOM的渲染优化 —— 优化器
- 使用ast生成渲染函数 —— 代码生成器
- ast语法树(抽象语法书)是用来描述语法的,描述语法本身,描述成一个树结构
二、代码解析
在 vue 中有三种方式来创建 HTML ,分别是 template、render、jsx,在编译入口会对这三种情况分别做判断
2-1 模板编译入口
// init.js
Vue.prototype._init = function (options) {
const vm = this;
// 把用户的选项放到 vm 上,这样在其他方法中都可以获取到 options 了
// 为了后续扩展的方法,所有实例对象都可以获取 $options 选项
// options 中是用户传入的数据 el ,data......
vm.$options = options;
// 初始化数据 state
initState(vm);
// 要将数据挂载到页面上
if (vm.$options.el) {
vm.$mount(vm.$options.el);
}
}
// new Vue({el}) 等价于 new Vue().$mount
Vue.prototype.$mount = function (el) {
const vm = this;
const opts = vm.$options;
el = document.querySelector(el); // 获取真实的元素
vm.$el = el; // 页面真实元素
// 如果不存在render属性
if (!opts.render) {
let template = opts.template;
// 如果不存在render和template,但是存在el属性
if (!template) {
// 直接将模板赋值到 el 所在的外层html结构(就是el本身 并不是父元素)
template = el.outerHTML;
}
// 最终需要把tempalte模板转化成render函数
let render = compileToFunction(template)
opts.render = render;
}
// 把 render 渲染到 el 上
mountComponent(vm)
}
2-2 解析标签和内容,生成ast语法树
// parser.js
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*`; // 匹配标签名的 aa-xxx
const qnameCapture = `((?:${ncname}\\:)?${ncname})`; // aa:aa-xxx
const startTagOpen = new RegExp(`^<${qnameCapture}`); // 此正则可以匹配到标签名 匹配到结果的第一个(索引第一个) [1]
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`); // 匹配标签结尾的 </div> [1]
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/; // 匹配属性的
// [1]属性的key [3] || [4] ||[5] 属性的值 a=1 a='1' a=""
const startTagClose = /^\s*(\/?)>/; // 匹配标签结束的 /> >
const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g; // {{ xxx }}
// vue3的编译原理比vue2里好很多,没有这么多正则了
// 生成 ast 模版语法树
export function parserHTML(html) {
// 可以不停的截取模板,直到把模板全部解析完毕
let stack = []; // 栈
let root = null; // 树根
function createASTElement(tag, attrs, parent = null) {
return {
tag,
type: 1, // 元素
children: [],
parent,
attrs
}
}
function start(tag, attrs) { // [div,p]
// 遇到开始标签 就取栈中的最后一个作为父节点
let parent = stack[stack.length - 1];
let element = createASTElement(tag, attrs, parent);
if (root == null) { // 说明当前节点就是根节点
root = element
}
if (parent) {
element.parent = parent; // 跟新p的parent属性 指向parent
parent.children.push(element);
}
stack.push(element)
}
function end(tagName) {
let endTag = stack.pop();
if (endTag.tag != tagName) {
console.log('标签出错')
}
}
function text(chars) {
let parent = stack[stack.length - 1];
chars = chars.replace(/\s/g, "");
if (chars) {
parent.children.push({
type: 2,
text: chars
})
}
}
// 删除解析后的字符
function advance(len) {
html = html.substring(len);
}
// 解析到开始标签后,进入该方法
function parseStartTag() {
const start = html.match(startTagOpen); // 4.30 继续
if (start) {
const match = {
tagName: start[1],
attrs: []
}
advance(start[0].length);
let end;
let attr;
while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) { // 1要有属性 2,不能为开始的结束标签 <div>
match.attrs.push({ name: attr[1], value: attr[3] || attr[4] || attr[5] });
advance(attr[0].length);
} // <div id="app" a=1 b=2 >
if (end) {
advance(end[0].length);
}
return match;
}
return false;
}
// 解析入口
while (html) {
// 解析标签和文本
let index = html.indexOf('<');
if (index == 0) {
// 解析开始标签 并且把属性也解析出来 </div>
const startTagMatch = parseStartTag()
if (startTagMatch) { // 开始标签
start(startTagMatch.tagName, startTagMatch.attrs);
continue;
}
let endTagMatch;
if (endTagMatch = html.match(endTag)) { // 结束标签
end(endTagMatch[1]);
advance(endTagMatch[0].length);
continue;
}
}
// 文本
if (index > 0) { // 文本
let chars = html.substring(0, index) //<div></div>
text(chars);
advance(chars.length)
}
}
return root;
}
解析的模版
<div> <p>{{name}}</p> </div>
解析后生成的 ast 树
{
tag: "div"
type: 1,
staticRoot: false,
static: false,
plain: true,
parent: undefined,
attrsList: [],
attrsMap: {},
children: [
{
tag: "p"
type: 1,
staticRoot: false,
static: false,
plain: true,
parent: {tag: "div", ...},
attrsList: [],
attrsMap: {},
children: [{
type: 2,
text: "{{name}}",
static: false,
expression: "_s(name)"
}]
}
]
}
2-3 生成render函数中的代码字符串
// generate.js
const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g; // {{ xxx }}
// 本质上是拼接字符串
function genProps(attrs) {
// {key:value,key:value,}
let str = '';
for (let i = 0; i < attrs.length; i++) {
let attr = attrs[i];
// 将样式转成对象
if (attr.name === 'style') { // {name:id,value:'app'}
let styles = {}
attr.value.replace(/([^;:]+):([^;:]+)/g, function () {
styles[arguments[1]] = arguments[2];
})
attr.value = styles
}
str += `${attr.name}:${JSON.stringify(attr.value)},`
}
return `{${str.slice(0, -1)}}`
}
function gen(el) {
if (el.type == 1) {
return generate(el); // 如果是元素就递归的生成
} else {
// 如果元素为文本
let text = el.text;
// 如果是普通文本
if (!defaultTagRE.test(text)) return `_v('${text}')`;
// 如果包含 {{name}},说明有表达式,需要做一个表达式和普通值的拼接 ['aaaa',_s(name),'bbb'].join('+)
// _v('aaaa'+_s(name) + 'bbb')
let lastIndex = defaultTagRE.lastIndex = 0;
let tokens = []; // <div> aaa{{bbb}} aaa </div>
let match;
// ,每次匹配的时候 lastIndex 会自动向后移动
while (match = defaultTagRE.exec(text)) { // 如果正则 + g 配合exec 就会有一个问题 lastIndex的问题
let 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('+')})`; // webpack 源码 css-loader 图片处理
}
}
function genChildren(el) {
let children = el.children;
if (children) {
return children.map(item => gen(item)).join(',')
}
return false;
}
// _c(div,{},c1,c2,c3,c4)
export function generate(ast) {
let children = genChildren(ast)
let code = `_c('${ast.tag}',${ast.attrs.length ? genProps(ast.attrs) : 'undefined'
}${children ? `,${children}` : ''
})`
return code;
}
ast语法树生成的render函数代码字符串
'with(this){return _c('div',{attr:{"id":"el"}},[_v("hello"+ _s(name))])}'
//格式化之后
with(this) {
return _c(
"div",
{attrs:{"id":"el"}},
[_v("Hello"+_s(name))]
)
}
render 渲染函数之所以可以生成vnode,是因为代码字符串中包含了很多函数调用。_c是createElement 的别名,渲染函数其实是执行了 createElement ,而 createElement 可以创建一个 vnode ,每处理一个AST节点,就会生成一个对应类型的代码字符串,进行嵌套
- 元素AST节点 —— _c(, , )
- 文本AST节点 —— _v(“Hello”+_s(name))
- 注释节点 —— _e(text)
2-4 将代码字符串转化为render函数
import { generate } from "./generate";
import { parserHTML } from "./parser";
// 编译入口
export function compileToFunction(template) {
// 1.将模板变成ast语法树
let ast = parserHTML(template);
// 代码优化 标记静态节点 (暂时不讲)
// 2.代码生成(生成render函数)
let code = generate(ast);
let render = new Function(`with(this){return ${code}}`);
console.log(render.toString())
}