08 | 【阅读Vue2源码】Template被Vue编译成了什么?
前言
我们写Vue组件代码时,一般都是写模板,这些模板并不是标准的HTML代码,Vue执行组件代码时会把模板转化成真正的HTML代码,那么Vue是如何将模板转换成标准HTML呢?一起来分析一下。
示例代码
有一个工具网站可以将Vue的模板转成JS代码
v2.template-explorer.vuejs.org/
那么,先写一段简单的模板代码,看看转换成了啥
<div id="app">
Hello Vue
</div>
上面的模板代码被转换成了下面的这段JS代码
function render() {
with(this) {
return _c('div', {
attrs: {
"id": "app"
}
}, [_v("\n Hello Vue\n")])
}
}
从以上操作中可以得出结论:Vue将template转换成了JS的函数
源码分析
接下来进入源码分析
思维导图
下面是调用链路图,可以带着调用链路图一起看源码的调用过程
准备Demo代码
为了更好的分析源码,我们先准备一段小的代码片段作为分析对象
<section id="app">
<button @click="plus">+1</button>
<div class="count-cls" :class="['count-text']">count:{{ count }}</div>
<div :calss="['count-double']" v-if="count % 2 === 0">doubleCount:{{ count }}</div>
</section>
var app = new Vue({
name: 'SimpleDemo_Template',
data() {
return {
count: 0
}
},
methods: {
plus() {
this.count += 1;
}
}
})
app.$mount('#app')
这段代码中包含了
事件(@click)
动态属性(:class)
条件渲染(v-if)
双大括号取值({{count}})
基于以上特性,研究Vue将模板转化成HTML时做了什么事情。
调试源码步骤
- 初始化Vue的过程跳过,直接debugger到
app.$mount('#app')
,然后一步一步往下走
- 进入
$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) {
// 我们没有new Vue时提供template,而是$mount('#app'),所以进入这个if语句
template = getOuterHTML(el)
}
if (template) {
// 核心逻辑:调用compileToFunctions将template字符串编译成渲染函数render
const { render, staticRenderFns } = compileToFunctions(template, {...}, this)
options.render = render
options.staticRenderFns = staticRenderFns
}
}
return mount.call(this, el, hydrating)
}
这里的主要逻辑是,获取el
的字符串代码,赋值给template
然后调用compileToFunctions
将template
字符串编译成渲染函数render
- 进入
compileToFunctions
函数,看看它做了什么
// src/compiler/to-function.js
function compileToFunctions (
template: string,
options?: CompilerOptions,
vm?: Component
): CompiledFunctionResult {
// ...
// 核心逻辑:调用compile函数
// compile
const compiled = compile(template, options)
// turn code into functions
const res = {}
res.render = createFunction(compiled.render, fnGenErrors)
// ...
return (cache[key] = res)
}
里面调用compile
,返回编译后的东西compiled
- 进入
compile
函数,看看它做了什么
// src/compiler/create-compiler.js
function compile (
template: string,
options?: CompilerOptions
): CompiledResult {
// ...
const compiled = baseCompile(template.trim(), finalOptions)
return compiled
}
里面调用baseCompile
,返回编译后的东西compiled
- 进入
baseCompile
函数,看看它做了什么
function baseCompile (
template: string,
options: CompilerOptions
): CompiledResult {
const ast = parse(template.trim(), options)
if (options.optimize !== false) {
optimize(ast, options)
}
const code = generate(ast, options)
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
}
经历了一路的函数调用,终于进入到了核心代码里baseCompile()
,这里的逻辑也很明确,做了3件事
- 将
template
解析成ast
- 优化
ast
- 将
ast
生成代码
逻辑清晰明确,继续分析
parse
函数调用createASTElement
生成ast
,具体过程比较复杂,这里不做详细分析,我们只关注生成的ast
是什么样的
构建ast的函数源码:
// src/compiler/parser/index.js
function createASTElement (
tag: string,
attrs: Array<ASTAttr>,
parent: ASTElement | void
): ASTElement {
return {
type: 1,
tag,
attrsList: attrs,
attrsMap: makeAttrsMap(attrs),
rawAttrsMap: {},
parent,
children: []
}
}
通过开发者工具的调试器,可以看到ast
的结构,ast
其实就是一个JS对象,然后带有parent
和children
属性,使它可以构建成树形结构,就是抽象语法树。
代码展示:
ast = {
attrs: [
{
"name": "id",
"value": ""app"",
"start": 9,
"end": 17
}
],
attrsList: [
{
"name": "id",
"value": "app",
"start": 9,
"end": 17
}
],
attrsMap: {
"id": "app"
},
children: [...]
end: 232,
start: 0,
tag: "section",
// ...
}
- 有了
ast
对象,然后做个优化optimize
,优化过程略,主要关注如何生成代码,看看generate(ast, options)
做了啥 - 进入
generate
函数体
function generate (
ast: ASTElement | void,
options: CompilerOptions
): CodegenResult {
const state = new CodegenState(options)
// fix #11483, Root level <script> tags should not be rendered.
const code = ast ? (ast.tag === 'script' ? 'null' : genElement(ast, state)) : '_c("div")'
return {
render: `with(this){return ${code}}`,
staticRenderFns: state.staticRenderFns
}
}
进入generate
函数,第一眼看到with(this){return
是不是很眼熟?
没错,就是开始时我们看到的render
的代码,就是在这里写的render
函数的函数体代码
继续,接着分析是如何生成的函数体的代码字符串,分析里面的逻辑
- 判断有无ast,无ast,则拼接
'_c("div")'
字符串 - 有ast,判断是否是
script
标签,若是拼接'null'
字符串,若不是,调用genElement(ast, state))
函数生成函数体
接着看看genElement()
的实现
// src/compiler/codegen/index.js
function genElement (el: ASTElement, state: CodegenState): string {
if (el.parent) {
el.pre = el.pre || el.parent.pre
}
if (el.staticRoot && !el.staticProcessed) {
return genStatic(el, state)
} else if (el.once && !el.onceProcessed) {
return genOnce(el, state)
} else if (el.for && !el.forProcessed) {
return genFor(el, state)
} else if (el.if && !el.ifProcessed) {
return genIf(el, state)
} else if (el.tag === 'template' && !el.slotTarget && !state.pre) {
return genChildren(el, state) || 'void 0'
} else if (el.tag === 'slot') {
return genSlot(el, state)
} else {
// component or element
let code
if (el.component) {
code = genComponent(el.component, el, state)
} else {
let data
if (!el.plain || (el.pre && state.maybeComponent(el))) {
data = genData(el, state)
}
const children = el.inlineTemplate ? null : genChildren(el, state, true)
code = `_c('${el.tag}'${
data ? `,${data}` : '' // data
}${
children ? `,${children}` : '' // children
})`
}
// module transforms
for (let i = 0; i < state.transforms.length; i++) {
code = state.transforms[i](el, code)
}
return code
}
}
可以看到genElement
里面也调用了好多个函数,处理各种情况static、if、once、for、data、scoped等等,由于篇幅有限,不详细分析每个函数的实现过程,我们只关注genElement
执行完的返回结果
最后的render
的值为
格式化一下代码字符串,就是下面这段代码了
with(this) {
return _c('section', {
attrs: {
"id": "app"
}
}, [_c('button', {
on: {
"click": plus
}
}, [_v("+1")]), _c('div', {
staticClass: "count-cls",
class: ['count-text']
}, [_v("count:" + _s(count))]), (count % 2 === 0) ? _c('div', {
attrs: {
"calss": ['count-double']
}
}, [_v("doubleCount:" + _s(count))]) : _e()])
}
至此render
函数字符构建好了
这里面的
_c
、_v
、_s
、_e
分别是什么函数呢?
_c
对应的是createElement
,创建VNode元素
_v
对应的是createTextVNode
,创建文本VNode
_s
对应的是toString
,转成字符串
_e
对应的是createEmptyVNode
,创建空的VNode这些函数在Vue初始化时,已经通过
installRenderHelpers(Vue.prototype)
挂载到Vue.prototype
身上,所以在Vue的实例中可以直接访问这些函数
- 那么是怎么把字符串变成render函数呢?随着调用栈代码执行,最后回到
compileToFunctions
函数体重,执行到const compiled = compile(template, options)
得到返回值compiled
- 往下执行,调用
res.render = createFunction(compiled.render, fnGenErrors)
生成函数
function createFunction (code, errors) {
try {
return new Function(code)
} catch (err) {
errors.push({ err, code })
return noop
}
}
其实很简单,直接把构建好的render
函数的函数体字符串作为new Function
的参数,即可创建一个函数
到这里render函数就已经创建好了,通过一路的debugger调试追踪代码,我们了解了整个过程做了什么事情。如果看得很懵逼,可以结合我画的调用链路图,多看看几遍。
手写Mini模板解析器
了解了template转成render函数的过程,那么我们可以依葫芦画瓢,自己动手写一个简单版的编译器。
1. 构建基本的执行流程
开搞,先把架子搭起来,编写好模板
<section id="app">
<button @click="plus">+1</button>
<div class="count-cls" :class="['count-text']">count:{{ count }}</div>
<div :calss="['count-double']" v-if="count % 2 === 0">doubleCount:{{ count }}</div>
</section>
再写相关的处理函数
function compile(template = '') {
const ast = parse(template);
const code = generate(ast);
return code;
}
function parse(template = '') {
}
function generate(ast = {}) {
}
function createASTElement() {
}
function createFunction(code = '') {
}
// 获取元素和模板字符串
const el = document.getElementById('app');
const template = el.outerHTML;
// 执行编译
const compiled = compile(template);
console.log('alan->compiled', compiled)
这里先把结构搭好,调用过程为:获取template
-> compile(template)
-> parse(template)
-> generate(ast)
,接下来编写各函数的函数体。
2. 实现parse函数,生成AST
先编写createASTElement
,定义我们想要的ast的结构,然后解析模板时才有目标要解析成什么样子,这里可以参照Vue的ast
我们也按照这个基本的结构返回,attrsList
和rawAttrsMap
可以不要,暂时用不上
function createASTElement(tag, attrs, parent) {
return {
tag,
attrsMap: {},
parent,
children: []
}
}
这样createASTElement
函数就编写好了,接下来编写parse
函数
parse函数的实现
首先明确一下,针对template我们需要做什么操作
<section id="app">
<button @click="plus">+1</button>
<div class="count-cls" :class="['count-text']">count:{{ count }}</div>
<div :calss="['count-double']" v-if="count % 2 === 0">doubleCount:{{ count }}</div>
</section>
从上面的模板来看,我们需要做的事情有:
- 收集标签的属性的键值
- 解析
@click
指令 - 解析动态属性
:class
- 解析插值语法
{{count}}
- 解析
v-if
明确了目标之后,开始编写函数
首先是收集属性的键值,这个简单,可以通过dom元素的attributes
属性获取,例如,打印app元素的attributes
值,可以看到有name
和value
的属性,它的值就是我们需要的
所以,我们可以编写一个函数来获取元素的属性的键值
function getAttrs(el) {
const attributes = el.attributes;
const attrs = [];
const attrMap = {};
const events = {};
let ifStatment = {};
for (const key in attributes) {
if (Object.hasOwnProperty.call(attributes, key)) {
const item = object[key];
attrMap[item.name] = item.value;
attrs.push({
name: item.name,
value: item.value,
});
if (item.name.startsWith('@')) {
events[item.name.replace('@', '')] = { value: item.value }
}
if (item.name === 'v-if') {
ifStatment = { exp: item.value }
}
}
}
return { attrs, attrMap, events, ifStatment };
}
接下来需要遍历整个元素及其子元素来收集属性,我们再编写一个遍历元素的函数walkElement
那么有一个问题,我们接收的参数是template字符串,那么怎么把它变成元素呢,其实也很简单:创建一个临时的元素,再通过innerHTML赋值,就可以变成DOM
const tempDOM = document.createElement("div");
tempDOM.innerHTML = template;
const templateDOM = tempDOM.children[0];
再编写walkElement
,在walkElement中
,我们需要先创建一个空ast,然后再组装相关的属性
function walkElement(el, parent) {
const ast = createASTElement();
ast.parent = parent;
ast.tag = el.tagName.toLowerCase();
// 获取当前元素的所有属性
const { attrs, attrMap, events, ifStatment } = getAttrs(el);
ast.attrs = attrs;
ast.attrMap = attrMap;
ast.events = events;
if (ifStatment && Object.keys(ifStatment).length) { // 收集v-if
ast.if = ifStatment
}
const children = Array.from(el.children);
if (children.length) { // 如果有子元素,递归遍历收集所有子元素
children.forEach((child) => {
const childAST = walkElement(child, ast);
ast.children.push(childAST);
});
} else { // 没有子元素,那么就是文本内容,例如:<div>123</div>中的123
const childVNodes = [...el.childNodes];
if (childVNodes.length) {
const text = childVNodes[0].nodeValue
.trim()
.replace(" ", "")
.replace("\n", " ")
.trim(); // 去除空格和换行
// 创建空的ast,文本节点增加text属性
const textAst = createASTElement();
textAst.text = text;
textAst.expression = {
values: parseExpressionVar(el.innerText), // 解析插值{{}}中的值,如果有{{}}
};
ast.children.push(textAst);
}
}
return ast;
}
执行后得到ast
其中有一个解析插值{{}}
的函数,通过正则匹配得到插值的变量
function parseExpressionVar(str = "") {
const content = ".*?";
const reg = new RegExp(`{{(${content})}}`, "g");
const matchs = [...str.matchAll(reg)] || [];
const res = [];
if (matchs.length) {
matchs.forEach((item) => {
res.push({
raw: item[0],
name: String(item[1]).trim(),
index: item.index,
});
});
}
return res;
}
最终parse
函数就已经实现了,完整代码如下
function parse(template = '') {
// 获取元素所有属性
function getAttrs(el) {
const attributes = el.attributes;
const attrs = []; // 收集属性
const attrMap = {}; // 收集属性的map
const events = {}; // 收集事件@xxx
let ifStatment = {}; // 收集v-if
for (const key in attributes) {
if (Object.hasOwnProperty.call(attributes, key)) {
const item = attributes[key];
attrMap[item.name] = item.value;
attrs.push({
name: item.name,
value: item.value,
});
if (item.name.startsWith('@')) { // 处理事件
events[item.name.replace('@', '')] = { value: item.value }
}
if (item.name === 'v-if') { // 处理v-if
ifStatment = { exp: item.value }
}
}
}
return { attrs, attrMap, events, ifStatment };
}
// 解析插值
function parseExpressionVar(str = "") {
const content = ".*?";
const reg = new RegExp(`{{(${content})}}`, "g");
const matchs = [...str.matchAll(reg)] || [];
const res = [];
if (matchs.length) {
matchs.forEach((item) => {
res.push({
raw: item[0],
name: String(item[1]).trim(),
index: item.index,
});
});
}
return res;
}
// 遍历元素
function walkElement(el, parent) {
const ast = createASTElement();
ast.parent = parent;
ast.tag = el.tagName.toLowerCase();
// 获取当前元素的所有属性
const { attrs, attrMap, events, ifStatment } = getAttrs(el);
ast.attrs = attrs;
ast.attrMap = attrMap;
ast.events = events;
if (ifStatment && Object.keys(ifStatment).length) { // 收集v-if
ast.if = ifStatment
}
const children = Array.from(el.children);
if (children.length) { // 如果有子元素,递归遍历收集所有子元素
children.forEach((child) => {
const childAST = walkElement(child, ast);
ast.children.push(childAST);
});
} else { // 没有子元素,那么就是文本内容,例如:<div>123</div>中的123
const childVNodes = [...el.childNodes];
if (childVNodes.length) {
const text = childVNodes[0].nodeValue
.trim()
.replace(" ", "")
.replace("\n", " ")
.trim(); // 去除空格和换行
// 创建空的ast,文本节点增加text属性
const textAst = createASTElement();
textAst.text = text;
textAst.expression = {
values: parseExpressionVar(el.innerText), // 解析插值{{}}中的值,如果有{{}}
};
ast.children.push(textAst);
}
}
return ast;
}
const tempDOM = document.createElement("div");
tempDOM.innerHTML = template;
const templateDOM = tempDOM.children[0];
const ast = walkElement(templateDOM, null);
return ast;
}
3. AST生成render函数字符串
有了ast,那么我们就可以根据ast来构造出渲染函数的函数体字符串,接着完善genderate
函数
前置知识
new Funtion的使用
首先需要明确一下,我们要构造的是函数体,然后用new Function
的方式构造一个函数,看个小例子
const code = `console.log('hello');`;
const render = new Function(code);
render(); // hello
所以,我们需要构造函数体就可以了。
with语句的使用
还有,Vue的渲染函数中,还有一个with
语句,具体介绍和使用方法可以见developer.mozilla.org/zh-CN/docs/…
with 语句扩展一个语句的作用域链。——MDN
简单来讲,with语句中的变量可以使用变量名,它会在with()
中去找这个对象中的变量,例如
function testWith() {
const person = {
name: 'AlanLee',
job: 'frontend engineer'
}
with(person) {
console.log('name=', name); // 直接使用变量名name
console.log('job=', job);
}
}
testWith();
进入正题
开始进入正题,构造渲染函数的函数体字符串
通过前面的步骤,我们得到的ast是这样的
{
"tag": "section",
"attrsMap": {},
"children": [
{
"tag": "button",
"attrsMap": {},
"children": [
{
"attrsMap": {},
"children": [],
"text": "+1",
"expression": {
"values": []
}
}
],
"attrs": [
{
"name": "@click",
"value": "plus"
}
],
"attrMap": {
"@click": "plus"
},
"events": {
"click": {
"value": "plus"
}
}
},
{
"tag": "div",
"attrsMap": {},
"children": [
{
"attrsMap": {},
"children": [],
"text": "count:{{count }}",
"expression": {
"values": [
{
"raw": "{{ count }}",
"name": "count",
"index": 6
}
]
}
}
],
"attrs": [
{
"name": "class",
"value": "count-cls"
},
{
"name": ":class",
"value": "['count-text']"
}
],
"attrMap": {
"class": "count-cls",
":class": "['count-text']"
},
"events": {}
},
{
"tag": "div",
"attrsMap": {},
"children": [
{
"attrsMap": {},
"children": [],
"text": "doubleCount:{{count }}",
"expression": {
"values": [
{
"raw": "{{ count }}",
"name": "count",
"index": 12
}
]
}
}
],
"attrs": [
{
"name": ":calss",
"value": "['count-double']"
},
{
"name": "v-if",
"value": "count % 2 === 0"
}
],
"attrMap": {
":calss": "['count-double']",
"v-if": "count % 2 === 0"
},
"events": {},
"if": {
"exp": "count % 2 === 0"
}
}
],
"attrs": [
{
"name": "id",
"value": "app"
}
],
"attrMap": {
"id": "app"
},
"events": {}
}
拼接_c()
函数的字符串
根据这个ast,遍历ast对象,拼接字符串,构建出一个以_c(tag, data, children)
函数为主的字符串,_c就是createElement
函数(注意,这个不是document.createElement
,而是Vue中用于构建VNode的一个函数),主要有三个参数
tag
:标签名data
:创建元素需要的数据,如events、if、和其他属性等children
:_c()数组
其形式为:_c('div', {attrs: {id: 'app'}}, [_c('button', {text: '+1'}), ...])
我们需要定义一些工具函数,用来处理元素、子元素、data;接下来定义genElm
、genData
、genElmChildren
函数
genElm
先定义一个genElm
函数
-
处理v-if,就是拼接一个三元运算符,例如
count % 2 === 0 ? _c(xxx) : _e()
-
处理data,交给
genData
-
处理子元素children,交给
genElmChildren
-
处理文本节点,文本节点又分有插值语法的文本和静态的文本
- 有插值语法的文本,需要用正则匹配出来,并替换成
_s()
函数包裹插值的变量,交给replaceVarWithFn
处理 - 静态文本,直接
_v()
包裹
- 有插值语法的文本,需要用正则匹配出来,并替换成
// 构建_c()
const genElm = (ast) => {
let str = "";
if (ast['if'] && ast['if'].exp) { // 处理v-if
let elStr = ''
if (ast.tag) {
elStr += `_c('${ast.tag}', ${JSON.stringify(genData(ast))}, ${ast.children ? genElmChildren(ast.children) || "[]" : "[]"})`;
}
// v-if构造出来,就是拼接一个三元运算符,例如count % 2 === 0 ? _c(xxx) : _e()
str += `${ast['if'].exp} ? ${elStr} : _e()`
} else if (ast.tag) {
// 处理元素节点,data参数通过genData函数处理,children通过genElmChildren处理
str += `_c('${ast.tag}', ${JSON.stringify(genData(ast))}, ${ast.children ? genElmChildren(ast.children) || "[]" : "[]"})`;
} else if (ast.text) { // 处理文本节点
// 处理文本中插值语法,例如:将countVal:{{count}}解析生成'countVal:'+ _s(count)
if (ast.expression && ast.expression.values.length) {
// 解析插值语法
const replaceVarWithFn = (name, target = "") => {
const toReplace = `' + _s('${name}')`;
const content = ".*?";
const reg = new RegExp(`{{(${content})}}`, "g");
let newStr = "";
newStr = target.replaceAll(reg, (item) => {
const matchs = [...item.matchAll(reg)] || [];
let tempStr = "";
if (matchs.length) {
matchs.forEach((matItem) => {
const mated = matItem[1];
if (mated && mated.trim() === name) {
tempStr = item.replaceAll(reg, toReplace);
}
});
}
return tempStr;
});
return newStr;
};
let varName = "";
ast.expression.values.forEach((item) => {
varName += replaceVarWithFn(item.name, ast.text);
});
str += `_v('${varName})`;
} else {
// 静态文本
str += `_v('${ast.text}')`;
}
}
return str;
};
genData
genData,接收ast参数,主要处理事件和属性
// 构建data
const genData = (ast = {}) => {
const data = {}
// 处理事件
if (ast.events && Object.keys(ast.events).length) {
data.on = ast.events;
}
// 处理属性
if (ast.attrs && ast.attrs.length) {
data.attrs = {}
ast.attrs.forEach(item => {
const skip = item.name.startsWith('@') || item.name === 'v-if'; // 跳过@xxx和v-if
let key;
let value;
if (!skip) {
if (item.name.startsWith(':')) { // parse :class
key = item.name.replace(':', '');
if (data.attrs[key]) {
const oldVal = data.attrs[key]
const valList = JSON.parse(item.value.replaceAll(`'`, `"`) || '[]');
value = `${oldVal} ${valList.join(' ')}`
}
} else {
key = item.name;
value = item.value;
}
}
data.attrs[key] = value;
})
}
return data;
};
genElmChildren
genElmChildren,接收children,主要还是genElm,拼接成数组
// 构建子元素
const genElmChildren = (children = []) => {
let str = "[";
children.forEach((child, i) => {
str += genElm(child) + `${i == children.length - 1 ? "" : ", "}`;
});
return str + "]";
};
generate
的完整代码
// 将ast转化成render函数的函数体的字符串
function generate(ast = {}) {
// 构建子元素
const genElmChildren = (children = []) => {
let str = "[";
children.forEach((child, i) => {
str += genElm(child) + `${i == children.length - 1 ? "" : ", "}`;
});
return str + "]";
};
// 构建data
const genData = (ast = {}) => {
const data = {}
// 处理事件
if (ast.events && Object.keys(ast.events).length) {
data.on = ast.events;
}
// 处理属性
if (ast.attrs && ast.attrs.length) {
data.attrs = {}
ast.attrs.forEach(item => {
const skip = item.name.startsWith('@') || item.name === 'v-if'; // 跳过@xxx和v-if
let key;
let value;
if (!skip) {
if (item.name.startsWith(':')) { // parse :class
key = item.name.replace(':', '');
if (data.attrs[key]) {
const oldVal = data.attrs[key]
const valList = JSON.parse(item.value.replaceAll(`'`, `"`) || '[]');
value = `${oldVal} ${valList.join(' ')}`
}
} else {
key = item.name;
value = item.value;
}
}
data.attrs[key] = value;
})
}
return data;
};
// 构建_c()
const genElm = (ast) => {
let str = "";
if (ast['if'] && ast['if'].exp) { // 处理v-if
let elStr = ''
if (ast.tag) {
elStr += `_c('${ast.tag}', ${JSON.stringify(genData(ast))}, ${ast.children ? genElmChildren(ast.children) || "[]" : "[]"})`;
}
// v-if构造出来,就是拼接一个三元运算符,例如count % 2 === 0 ? _c(xxx) : _e()
str += `${ast['if'].exp} ? ${elStr} : _e()`
} else if (ast.tag) {
// 处理元素节点,data参数通过genData函数处理,children通过genElmChildren处理
str += `_c('${ast.tag}', ${JSON.stringify(genData(ast))}, ${ast.children ? genElmChildren(ast.children) || "[]" : "[]"})`;
} else if (ast.text) { // 处理文本节点
// 处理文本中插值语法,例如:将countVal:{{count}}解析生成'countVal:'+ _s(count)
if (ast.expression && ast.expression.values.length) {
const replaceVarWithFn = (name, target = "") => {
const toReplace = `' + _s('${name}')`;
const content = ".*?";
const reg = new RegExp(`{{(${content})}}`, "g");
let newStr = "";
newStr = target.replaceAll(reg, (item) => {
const matchs = [...item.matchAll(reg)] || [];
let tempStr = "";
if (matchs.length) {
matchs.forEach((matItem) => {
const mated = matItem[1];
if (mated && mated.trim() === name) {
tempStr = item.replaceAll(reg, toReplace);
}
});
}
return tempStr;
});
return newStr;
};
let varName = "";
ast.expression.values.forEach((item) => {
varName += replaceVarWithFn(item.name, ast.text);
});
str += `_v('${varName})`;
} else {
// 静态文本
str += `_v('${ast.text}')`;
}
}
return str;
};
let code = genElm(ast);
return code;
}
最后实现的效果
_c('section', {"attrs":{"id":"app"}}, [_c('button', {"on":{"click":{"value":"plus"}},"attrs":{}}, [_v('+1')]), _c('div', {"attrs":{"class":"count-cls count-text"}}, [_v('count:' + _s('count'))]), count % 2 === 0 ? _c('div', {"attrs":{}}, [_v('doubleCount:' + _s('count'))]) : _e()])
构建好了render
渲染函数的函数体,接下来我们只需要把它放进new Function
中构建一个函数就ok了
function compile(template = '') {
const ast = parse(template);
console.log('alan->ast', ast)
const code = generate(ast);
const render = createFunction(code);
return render;
}
function createFuntion(code) {
return new Function(`
with(this) {
return ${code};
}
`)
}
现在有render函数了,那么render函数又是怎样转化成真实的DOM呢?由于篇幅有限,请看下回分解。
总结
- Vue将template转换成了JS的函数(render)
- 通过
baseCompile
函数、将template
解析成ast
,然后优化ast
,最后根据ast
生成字符串代码 - 通过
new Function
的方式构建render
函数 - render函数是由
_c()
函数构成,用于创建VNode的函数 - 整个实现过程很复杂,尤其是解析html的时候是最复杂的
附录
- mini-compiler完整代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mini Compiler</title>
</head>
<body>
<section id="app">
<button @click="plus">+1</button>
<div class="count-cls" :class="['count-text']">count:{{ count }}</div>
<div :calss="['count-double']" v-if="count % 2 === 0">doubleCount:{{ count }}</div>
</section>
<script>
function compile(template = '') {
const ast = parse(template);
console.log('alan->ast', ast)
const code = generate(ast);
const render = createFunction(code);
return render;
}
function parse(template = '') {
// 获取元素所有属性
function getAttrs(el) {
const attributes = el.attributes;
const attrs = []; // 收集属性
const attrMap = {}; // 收集属性的map
const events = {}; // 收集事件@xxx
let ifStatment = {}; // 收集v-if
for (const key in attributes) {
if (Object.hasOwnProperty.call(attributes, key)) {
const item = attributes[key];
attrMap[item.name] = item.value;
attrs.push({
name: item.name,
value: item.value,
});
if (item.name.startsWith('@')) { // 处理事件
events[item.name.replace('@', '')] = { value: item.value }
}
if (item.name === 'v-if') { // 处理v-if
ifStatment = { exp: item.value }
}
}
}
return { attrs, attrMap, events, ifStatment };
}
// 解析插值
function parseExpressionVar(str = "") {
const content = ".*?";
const reg = new RegExp(`{{(${content})}}`, "g");
const matchs = [...str.matchAll(reg)] || [];
const res = [];
if (matchs.length) {
matchs.forEach((item) => {
res.push({
raw: item[0],
name: String(item[1]).trim(),
index: item.index,
});
});
}
return res;
}
// 遍历元素
function walkElement(el, parent) {
const ast = createASTElement();
// ast.parent = parent;
ast.tag = el.tagName.toLowerCase();
// 获取当前元素的所有属性
const { attrs, attrMap, events, ifStatment } = getAttrs(el);
ast.attrs = attrs;
ast.attrMap = attrMap;
ast.events = events;
if (ifStatment && Object.keys(ifStatment).length) { // 收集v-if
ast.if = ifStatment
}
const children = Array.from(el.children);
if (children.length) { // 如果有子元素,递归遍历收集所有子元素
children.forEach((child) => {
const childAST = walkElement(child, ast);
ast.children.push(childAST);
});
} else { // 没有子元素,那么就是文本内容,例如:<div>123</div>中的123
const childVNodes = [...el.childNodes];
if (childVNodes.length) {
const text = childVNodes[0].nodeValue
.trim()
.replace(" ", "")
.replace("\n", " ")
.trim(); // 去除空格和换行
// 创建空的ast,文本节点增加text属性
const textAst = createASTElement();
textAst.text = text;
textAst.expression = {
values: parseExpressionVar(el.innerText), // 解析插值{{}}中的值,如果有{{}}
};
ast.children.push(textAst);
}
}
return ast;
}
const tempDOM = document.createElement("div");
tempDOM.innerHTML = template;
const templateDOM = tempDOM.children[0];
const ast = walkElement(templateDOM, null);
return ast;
}
// 将ast转化成render函数的函数体的字符串
function generate(ast = {}) {
// 构建子元素
const genElmChildren = (children = []) => {
let str = "[";
children.forEach((child, i) => {
str += genElm(child) + `${i == children.length - 1 ? "" : ", "}`;
});
return str + "]";
};
// 构建data
const genData = (ast = {}) => {
const data = {}
// 处理事件
if (ast.events && Object.keys(ast.events).length) {
data.on = ast.events;
}
// 处理属性
if (ast.attrs && ast.attrs.length) {
data.attrs = {}
ast.attrs.forEach(item => {
const skip = item.name.startsWith('@') || item.name === 'v-if'; // 跳过@xxx和v-if
let key;
let value;
if (!skip) {
if (item.name.startsWith(':')) { // parse :class
key = item.name.replace(':', '');
if (data.attrs[key]) {
const oldVal = data.attrs[key]
const valList = JSON.parse(item.value.replaceAll(`'`, `"`) || '[]');
value = `${oldVal} ${valList.join(' ')}`
}
} else {
key = item.name;
value = item.value;
}
}
data.attrs[key] = value;
})
}
return data;
};
// 构建_c()
const genElm = (ast) => {
let str = "";
if (ast['if'] && ast['if'].exp) { // 处理v-if
let elStr = ''
if (ast.tag) {
elStr += `_c('${ast.tag}', ${JSON.stringify(genData(ast))}, ${ast.children ? genElmChildren(ast.children) || "[]" : "[]"})`;
}
// v-if构造出来,就是拼接一个三元运算符,例如count % 2 === 0 ? _c(xxx) : _e()
str += `${ast['if'].exp} ? ${elStr} : _e()`
} else if (ast.tag) {
// 处理元素节点,data参数通过genData函数处理,children通过genElmChildren处理
str += `_c('${ast.tag}', ${JSON.stringify(genData(ast))}, ${ast.children ? genElmChildren(ast.children) || "[]" : "[]"})`;
} else if (ast.text) { // 处理文本节点
// 处理文本中插值语法,例如:将countVal:{{count}}解析生成'countVal:'+ _s(count)
if (ast.expression && ast.expression.values.length) {
// 解析插值语法
const replaceVarWithFn = (name, target = "") => {
const toReplace = `' + _s('${name}')`;
const content = ".*?";
const reg = new RegExp(`{{(${content})}}`, "g");
let newStr = "";
newStr = target.replaceAll(reg, (item) => {
const matchs = [...item.matchAll(reg)] || [];
let tempStr = "";
if (matchs.length) {
matchs.forEach((matItem) => {
const mated = matItem[1];
if (mated && mated.trim() === name) {
tempStr = item.replaceAll(reg, toReplace);
}
});
}
return tempStr;
});
return newStr;
};
let varName = "";
ast.expression.values.forEach((item) => {
varName += replaceVarWithFn(item.name, ast.text);
});
str += `_v('${varName})`;
} else {
// 静态文本
str += `_v('${ast.text}')`;
}
}
return str;
};
let code = genElm(ast);
return code;
}
function createASTElement(tag, attrs, parent) {
return {
tag,
attrsMap: {},
parent,
children: []
}
}
function createFunction(code = '') {
return new Function(`
with(this) {
return ${code};
}
`)
}
// 获取元素和模板字符串
const el = document.getElementById('app');
const template = el.outerHTML;
// 执行编译
const compiled = compile(template);
console.log('alan->compiled', compiled);
</script>
</body>
</html>
转载自:https://juejin.cn/post/7268186502442041381