Vue源码剖析-模板编译
- 模板编译简介
- 模板编译的结果
- Vue Template Explorer
- 编译的入口函数
- createCompilerCreator函数详解
- 模板编译的过程
- compile函数
- baseCompile-AST
- baseCompile-parse
- baseCompile-optimize
- baseCompile-generate 上
- 调试
- 实例
- 模板编译过程思维导图
模板编译简介
- 模板编译的主要目的是将模板(template)转换成渲染函数(render)
- 渲染函数
render (h) {
return h('div', [
h('h1', { on: { click: this.hander } }, 'title'),
h('p', 'some content')
])
}
- 模板编译的作用
模板编译的结果
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<div id="app">
<h1>Vue<span>模板编译过程</span>></h1>
<p>{{msg}}</p>
<comp @myclick="hander">
</div>
<!-- 完成版本 -->
<script src="../../dist/vue.js"></script>
<script>
Vue.component('comp',{
template:'<div> I am a comp </div>'
})
const vm = new Vue({
el: "#app",
data: {
msg: "Hello compiler",
},
methods:{
hander(){
console.log('test')
}
}
});
// 编译生成的render函数
console.log(this.$options.render)
</script>
</body>
</html>
- 因为没有template,所以将el.outerHtml作为template
- 打印的结果是
(function anonymous() {
with (this) {
return _c(
"div",
{ attrs: { id: "app" } },
[
_m(0),
_v(" "),
_c("p", [_v(_s(msg))]),
_v(" "),
_c("comp", { on: { myclick: hander } }),
],
1
);
}
});
-
with(this)的作用是创建一个封闭的作用域,_m相当于this._m
-
-s的作用,将用户传入的内容转换成字符转,因为在html解析,自动对字符串进行解析,当行我们把msg的值改成[1,2,3],视图上显示的也是[1,2,3],实际上是将整个数组转换成了字符串
-
我们再来观察下这些下划线方法的定义位置
Vue Template Explorer
- 工具:帮助我们将html模板转换成render函数,可以通过该工具学习render函数
编译的入口函数
- render函数由compileToFunctions(template,options)函数生成,并将render函数绑定过到Vue实例的$options属性上(编译成函数)
- compileToFunctions函数又是createCompiler函数生成(创建编译器),这里只是创建了compileToFunctions还没使用到该函数
-
baseOptions对象,
最重要的是模块和指令
- 模块:处理行内样式和类样式以及处理和v-if一起使用的v-model
- 指令:v-指令名text/html/model
- 模块
-
指令
-
createCompiler函数由createCompilerCreator函数生成
-
createCompilerCreator函数详解
- 作用:闭包生成createCompiler函数,
- 参数: baseCompile函数=>核心函数,该函数做了三件事
- 把模板template编译成抽象语法树ast
- 优化抽象语法树
- 把抽象语法树生成字符串形式的js代码
-
之前都是函数调用时候传递的实参,我们在看下定义createCompilerCreator的地方
-
在 src\compiler\index.js中介绍了createCompilerCreator实参baseCompile函数的功能,但要了解createCompilerCreator函数真正执行了什么功能,如何返回creteCompiler函数,还得去createCompilerCreator函数的定义的地方看
-
createCompilerCreator函数定义的地方–src\compiler\create-compiler.js
-
createCompilerCreator函数的作用,经过参数baseCompile函数的处理,返回了createCompier函数,而createCompiler(baseOptions)函数最终又返回了compile函数和compileToFunctions函数组成的对象
-
总体的思维导图如下
模板编译的过程
- 在createCompiler函数中返回了入口函数compileToFunction,我们重点观察compileToFunction的定义
- 观察下createCompileTofunctionFn(compile)函数做了什么(这个时候就可以看出前面定义compile函数的作用了),实际上createCompilerCreator函数的参数(baseCompile核心函数是在compile函数中调用的)
compile函数
- 在createCompileTofunctionFn中实际上是调用了compile函数,我们重点观察compile函数做了什么事情
- compile函数是在createCompiler函数中定义的
- compile函数的核心作用
- 合并选项baseOptions+options=finalOptions
- 调用baseCompile进行编译,返回编译好的对象
baseCompile-AST
- 在compile函数中合并选项完成后通过baseCompile核心函数编译模板,生成包含抽象语法树的编译对象并且返回
- 什么是抽象语法树(实质是一个对象)
- 抽象语法树简称AST(Abstract Ayntax Tree)
- 使用对象的形式描述树形的结构代码
- 此处的抽象语法树是用来描述树形结构的HTML字符串
- 为什么要使用抽象语法树
- 模板字符串转换成AST后,可以通过AST对模板做优化处理
- 标记模板中的静态内容,在patch的时候直接跳过静态内容
- 在patch的过程中静态内容不需要对比和重新渲染
我们可以看下普通的html代码对应的抽象语法树是什么样的(https://astexplorer.net/)
baseCompile-parse
-
parse函数的作用试讲模板字符串转换成AST对象,这个过程比较复杂,vue内部借鉴了一个开源库来实现此功能,深入研究parse过程的事件和说活不成正比,所以这里我们只关注整体的执行流程
-
parse函数只接受两个参数:模板字符串-template及合并后的选项
-
返回值是解析好的AST对象
-
我们进入parse函数内部看下,parse函数中解析了选项中的成员和定义了一些变量,其中最重要的是在pasre函数内部调用的parseHTML函数(用来解析template模板),返回的root对象就是我们解析好的AST对象
-
我们再观察parseHTML函数内部的处理过程,参数
- 选项相关的参数
- start/end等相关函数(解析到标签/注释/文本等内容的时候调用的方法,将相关处理的结果拼接到AST对象上)
-
以上的parseHTML函数的调用,我们再观察该函数的定义src\compiler\parser\html-parser.js
- 首先定义了很多正则表达式,这些正则表达式的作用是用来匹配html字符串模板中的内容
- 再回到parseHTML函数中,这个函数会遍历html字符串(即传入的template),通过以上定义的正则表达式来判断html中的内容,是否是标签/属性/文本等,再调用parseHTML传入的start/end/coment等来处理正则匹配到的内容,然后进行拼接,我们以start方法来举例
- 首先定义了很多正则表达式,这些正则表达式的作用是用来匹配html字符串模板中的内容
-
首先start函数是正则匹配到开始标签的时候调用,内部首先会调用createASTElement创建AST对象
// 创建AST对象
let element: ASTElement = createASTElement(tag, attrs, currentParent)
-
我们进入createASTElement函数来分析,这个函数非常的简单,
就是返回了一个对象,这个对象就是我们说的AST对象
,所以可以看出抽象语法树这个概念并不难,它就是一个对象.这个对象具备以下属性
-
通过createASTElement创建AST对象后,就是填充相关的属性了(包含指令)
-
以v-pre指令为例,我们进入processPre函数来看下v-pre属性的处理过程
-
进入getAndRemoveAttr函数看下
-
处理完v-pre后,再处理v-for,v-if等结构化指令
-
我们以v-if举例 processIf(element)
-
不管是v-pre还是v-if等属性的处理过程,我们可以看出,都是将相关的属性值绑定到AST对象的相关属性上
-
小结:parse这个函数内部处理过程中会依次去遍历html模板字符串,把html字符串转换成AST对象,也就是一个普通的对象,html中的属性和指令都会记录在AST对象的相应属性上
baseCompile-optimize
- parse函数将html模板字符串转换成了AST对象,接下来解释optimize函数对AST对象的优化
- optimize函数的作用:优化AST对象
- 我们进入optimize函数
- 我们首先观察markStatic函数,将AST对象相应的节点及子节点标记为是否是静态节点node.static=true/false
- 接下来我们观察markStaticRoots-静态根节点
markStaticRoots小结:
- 静态根节点指的是,标签中包含子标签,并且没有动态内容,也就是子标签中都是纯文本内容
- 如果标签中包含纯文本内容,没有子标签,不是静态根节点,Vue中是不会对它做优化的,因为这样优化的成本大于收益
baseCompile-generate 上
-
generate函数的作用:将优化后的AST对象转换成js代码
-
我们进入generate函数,分析其实现
-
首先观察创建好的CodegenState对象(实例)
-
然后我们再观察genElement函数
-
genElement函数返回的对象中render属性和staticRenderFns(静态根节点的渲染函数)属性都是字符串形式,还不是真正的渲染函数,我们再来看render真正转换成函数的位置-createCompileToFunctionFn函数中
调试
通过查看源码我们观察到模板编译是把模板字符串首先转换成AST对象,然后优化AST对象,优化的过程其实在标记静态根节点
将优化好的AST对象转换成字符串形式的代码,最终再把字符串形式的代码通过new function转换成匿名函数,这个匿名函数就是最终的render函,模板编译最终就是把模板字符串转换成渲染函数
实例
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>compile</title>
</head>
<body>
<div id="app">
<!-- 静态根节点 -->
<h1>Vue<span>模板编译过程</span>></h1>
<!-- 非静态根节点,有插值表达式 -->
<div>
{{msg}}
<p>hello</p>
</div>
<!-- 非静态根节点,因为没有元素子节点 -->
<div>是否显示</div>
</div>
<!-- 完成版本 -->
<script src="../../dist/vue.js"></script>
<script>
const vm = new Vue({
el: "#app",
data: {
msg: "Hello compiler",
},
methods: {
hander() {
console.log("test");
},
},
});
// 编译生成的render函数
console.log(vm.$options.render);
</script>
</body>
</html>
- 因为首次编译,缓存中是没有编译好的渲染函数,所以继续执行compile函数,F11键入compile函数
- F11进入baseCompile函数
- F10直接跳过函数的运行看结果
- 注意:上面只是生成了AST对象,.并没有看到static等属性(优化后的AST对象才会有)
- 执行完optimize函数后,优化后的AST对象有static等属性(
首先是不是静态的,如果不是静态的,一定不是静态根节点
)
- 执行generate函数,生成js代码,AST对象中多了一个新属性staticProcessed,代表处理完毕
- 再找到把render字符串转换成函数的位置
- 缓存rende函数