前言
上一篇中我们主要讲的Vue2.0
响应式的实现,其主要是利用Object.defineProperty
的来对数据进行劫持。本节课接上一章,主要是讲解Vue
是如何进行模板编译的,模板编译相对与响应式的实现会复杂的很多,涉及到很多正则一级AST
语法树的实现。
我个人推荐的看源码的方式最好是通过视频文章自己能手写一边然后再去看,否则就会造成这样的结果。

真是卷...
本文章主要讲解的最基本的模板编译(标签,属性,mustache语法),并不包括事件、指令、过滤器等。
正文
思维导图
Vue在初始化的时候,会将我们传入的
template
模板解析为render函数,如果没有传入template
模板就会将el
对标的元素转为字符串,然后进行解析。
arduino
new Vue({
el: '#app',
data:{
name: '小明',
age: 18,
},
template: '<div>{{ name }}</div>', // template的优先级会比el高
})
编译入口
ini
// core/instance/init.js
function initMixin(Vue) {
Vue.prototype._init = function (options) {
const vm = this;
vm.$options = options; // $表示vue中的变量
initState(vm);
if(options.el) {
vm.$mount(options.el); // 实现页面挂载
}
}
Vue.prototype.$mount = function(el) {
const vm = this;
el = document.querySelector(el);
const opt = vm.$options;
if(!opt.render) {
let template;
if(opt.template) { // 如果传入的对象中有template就将template作为模板
template = opt.template;
} else { // 如果没有template就将el 转为模板
template = el.outerHTML;
}
if(template) {
// 对模板进行编译
const render = compileToFunction(template); // 最终转化为render函数
opt.render = render;
}
}
}
}
接着上一章中的通过initState
函数将数据转为响应式,接下来就是进行页面挂载$mount
,将模板转为render函数,其主要核心方法就是compileToFunction
。
模板转为AST语法树
ini
// compiler/parse.js
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/; // 匹配属性
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*`;
const qnameCapture = `((?:${ncname}\\:)?${ncname})`;
const startTagOpen = new RegExp(`^<${qnameCapture}`); // 他匹配到标签名 <div
const startTagClose = /^\s*(\/?)>/; // 匹配单个标签 <br/>
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`); // 他匹配到结束标签 </XXXX>
export function parseHTML(html) {
const ELEMENT_TYPE = 1; // 元素节点
const TEXT_TYPE = 3; // 文本节点
const stack = []; // 栈结构
let currentParent; // 当前父亲节点
let root; // 根节点
function createASTElement(tag, attrs) { // 创建节点,包括标签名、节点类型、子节点、属性,父节点
return {
tag: tag,
type: ELEMENT_TYPE,
children: [],
attrs,
parent: null
}
}
function start(tag, attrs) { // 开始标签入栈
let node = createASTElement(tag, attrs);
if (!root) {
root = node;
}
if (currentParent) {
node.parent = currentParent;
currentParent.children.push(node);
}
stack.push(node);
currentParent = node;
}
function chars(text) { // 文本标签入栈
text = text.replace(/\s/g, '');
text && currentParent.children.push({
type: TEXT_TYPE,
text,
parent: currentParent
})
}
function end(tag) { // 出栈
let node = stack.pop();
currentParent = stack[stack.length - 1];
}
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 attr; // 属性
let end; // 结束标签
while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) { // 如果没有匹配到结束标签就一直匹配,同时匹配中间的dom属性
advance(attr[0].length);
let name = attr[1];
let value = attr[3] || attr[4] || attr[5];
match.attrs.push({
name: name,
value: value
})
}
if (end) {
advance(end[0].length);
}
return match;
}
return false; // 不是开始标签
}
while (html) { // 最后循环结束条件就是模板被拆分完
let 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;
}
}
if (textEnd > 0) {
let text = html.substring(0, textEnd); // 文本内容
if (text) {
advance(text.length);
chars(text);
}
}
}
return root; // 最后返回根节点
}
使用不同的正则表达式对html
进行匹配,遇到开始标签就存入栈
,在匹配到结束标签之前一直循环匹配dom
属性,依次循环。同时每次匹配到一个就利用advance
对html
字符串进行截取,知道字符串被截取完毕结束循环。
AST转成code代码
ini
// compiler/codegen/index.js
const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g; // 匹配双括号内容
function gen(node) {
if(node.type === 1) { // 如果是元素节点
return codegen(node); // 进行递归处理
} else {
// 文本节点
let text = node.text;
if(!defaultTagRE.test(text)) { // 文本节点中没有{{}}直接将传入原文本
return `_v(${JSON.stringify(text)})`;
} else { // 文本节点中存在{{}}
let tokens = [];
let match;
defaultTagRE.lastIndex = 0;
let lastIndex = 0;
while(match = defaultTagRE.exec(text)) {
let index = match.index; // index代表匹配到的位置
if(index > lastIndex) { // 匹配到{{,将{{之前的文本存入tokens
tokens.push(JSON.stringify(text.slice(lastIndex, index)));
}
tokens.push(`_s(${match[1].trim()})`); // 将{{ }}里面的变量存入tokens
lastIndex = index + match[0].length; // 修改指针位置
}
if(lastIndex < text.length) { // 将剩余的文本存入tokens
tokens.push(JSON.stringify(text.slice(lastIndex, index)));
}
return `_v(${tokens.join('+')})`;
}
}
}
function genChildren(children) {
return children.map(child => {
return gen(child);
}).join(",");
}
function genProps(attrs) { // 对属性进行处理
let str = '';
for (let i = 0; i < attrs.length; i++) {
let attr = attrs[i];
if (attr.name === 'style') { // 如果属性是样式style 这里需要特殊处理下
let obj = {};
attr.value.split(';').forEach(item => {
let [key, value] = item.split(':');
obj[key] = value;
})
attr.value = obj;
}
str += `${attr.name}:${JSON.stringify(attr.value)},`;
}
return `{${str.slice(0, -1)}}`;
}
export function codegen(ast) {
let children = genChildren(ast.children);
let code = `_c('${ast.tag}', ${ast.attrs.length > 0 ? genProps(ast.attrs) : 'null'}, ${ast.children.length ? `${children}` : ''})`;
return code;
}
将ast
语法树转为code代码,最后生成
css
<div id="app">
<p style="color: red;">
我的姓名是: {{ name }}
我的年龄是: {{ age }}
</p>
</div>
// 最终结果
// _c('div', {id:"app"}, _c('p', {style:{"color":" red"}}, _v("我的姓名是:"+_s(name)+"我的年龄是:"+_s(age))))
生成render函数
javascript
// compiler/index.js
export function compileToFunction(template) {
let ast = parseHTML(template); // 将template转为ast树
let code = codegen(ast); // 将ast树转为code代码
code = `with(this) {return ${code}}`;
let render = new Function(code); // 根据代码生成render函数
return render;
}
new Function()
利用with
来改变render
函数的作用域。
小结
至此Vue2.0
的模板编译原理已经完结 大家可以试着自己动手写一遍核心代码哈,本文主要实现基础的元素的编译,其中不乏出错的地方,望请见谅!
如果觉得本文有帮助 记得点赞三连哦 十分感谢!