思维导图
前言
上一篇文章里面主要写了Vue的数据响应式原理是怎么实现的,涉及对象的数据劫持
、数组的变异方法的重写
、以及data下面数据的便捷取值
。Vue 源码初探(一)响应式原理。
这一篇我们主要讲Vue的模板编译原理
,首先我们来回顾一下,
var vm = new Vue({
el: '#app',
data: {
message: 'Hello Vue!',
arr: [1, 2, 3]
},
template: '<div id="app">{{message}}</div>'
})
console.log(vm.$options.render)
我们来看一下打印出来的结果,这块引用的是官方的Vue代码。
这么来说我们写在options
里面的template
被Vue
转换成了这样的一个render
函数了,并且还可以通过这个函数,把页面渲染出来。现在我们回头来想一想,我们在template
里面的写的v-if
、{{}}
、v-for
、等等一系列的指令,都是原生html所不具备的,正式为了让原生html
的功能更加强大,Vue
内部需要对template
里面的代码进行对应的处理,所以才会有了模板编译的这个过程。
贴上这张图呢,是要表达本篇文章的主要目的,搞清楚Vue
内部是怎样把template
字符串变成一个render函数
的。
正文
1、模板编译函数入口
import { compileToFunction } from "./compiler"
import { initState } from "./state"
export function initMixin (Vue) {
Vue.prototype._init = function (options) {
//这里的 this 其实就是外面new 出来的实例
const vm = this
//把用户的选项放到vm 上,这样在其他的方法中都可以获取到了
vm.$options = options //为了后续方法,都可以获取到$options选项
//options中包含了很多的选项 el data props
initState(vm)
//模板编译入口
if(vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
Vue.prototype.$mount = function(el){
//在构造函数的原型方法内部获取当前实例、或者属性,还可以给属性动态添加参数。
const vm = this
const options = vm.$options
el = document.querySelector(el) // 获取真实元素
vm.$el = el
//用户没有传递render函数
if(!options.render){
//用户没有传递template字符串
let template = options.template
if(!template && el) {
template = el.outerHTML
}
//获取用户传递的el元素的html当做template 进行编译成render函数
let render = compileToFunction(template)
//把render函数放在 options上面放一份。
options.render = render
}
}
}
因为生成render函数
,之前我们先要得到template解析生成的ast
代码。
2、模板编译核心入口
import { generate } from "./generate";
import { parserHTML } from "./parser";
export function compileToFunction(template) {
//将模板变成ast
let ast = parserHTML(template)
//代码优化,静态节点标记
//代码生成
let code = generate(ast)
let render = new Function(`with(this){return ${code}}`);
console.log(render.toString())
}
3、template转换成ast
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里好很多,没有这么多正则了
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;
}
小结:利用正则表达式,匹配开始标签和结束标签,并且用栈来记录匹配到的标签的ast结构,当遇到开始标签的时候将ast节点
推入栈中,取栈顶上面的最后一个元素作为要加入元素节点的父节点,当匹配到结束标签的时候,就把当前标签节点从栈中移除。并通过advance不断截取匹配字符串,直到字符串全部解析完毕。
整体来说这块的代码所做的事情就是将template
转化成一个js对象
<div id="app">
{{message}}
</div>
转化结果如下。
{
"tag": "div",
"type": 1, //元素节点
"children": [
{
"type": 2, //文本节点
"text": "{{message}}"
}
],
"parent": null,
"attrs": [
{
"name": "id",
"value": "app"
}
]
}
现在可以先到# Vue Template Explorer这个网站上面大概了解一下最终要生成的render函数
长什么样子。
4、通过ast生成render函数
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}')`; // 说明就是普通文本
// 说明有表达式 我需要 做一个表达式和普通值的拼接 ['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;
}
总结
其实这一节,只要记住几个重要的步骤就可以了,至于内部的细节,可以有时间了在看相应内部的细节。
- template 转换成ats。
- ast 编译成render函数。