上一篇,我们介绍了vue实现响应式的原理。其中,有2点需要重点展开分析:
-
- vue组件化实现
-
- render函数执行过程中发生了什么
在讨论上述2个问题之前,我们先待 了解 compiler过程。这是核心前提,只有先熟悉了它,我们才能清晰的认识到 数据的流向。
好了,废话不多说,扭起袖子就是干~
一. Compiler
在vue中,我们写template,显然浏览器不认识。那么就需要有个解释过程。
compiler 分两种情况:
-
- 构建时compiler
-
- 运行时compiler
构建时compiler
本地开发时,使用webpack + vue-loader,来处理.vue文件。 例如:
<template>
<div>{{ a }}</div>
</template>
<script>
export default {
data() {
return {
a: '1'
}
}
}
</script>
<style>
</style>
打包时,vue-loader会 将 .vue文件的内容,转化为render函数
运行时compiler
运行时compiler,我们 不使用 vue-loader这样的插件,而且直接写template,让vue在浏览器运行的时候,动态将template转化为render函数。例如:
<html>
<head>
<meta charset="utf-8"/>
</head>
<body>
<div id='root'>
</div>
<script src="../vue/dist/vue.js"></script>
<script>
let vm = new Vue({
el: '#root',
template: '<div>{{ a }}</div>',
data() {
return {
a: "这是根节点"
}
}
})
</script>
</body>
</html>
本质上,构建时compiler和运行时compiler都是转化为render函数。 显示构建时效率更高,在我们的生产环境中,尽量避免运行的时候,再去compiler。
细心的同学会问了:既然都是转化为render函数,那是不是也可以手写render函数?
答案是肯定的,例如:
<html>
<head>
<meta charset="utf-8"/>
</head>
<body>
<div id='root'>
</div>
<script src="../vue/dist/vue.js"></script>
<script>
let vm = new Vue({
el: '#root',
data() {
return {
a: "这是根节点"
}
},
render(createElement) {
return createElement('div', {
attrs: {
id: 'test'
}
}, this.a)
}
})
</script>
</body>
</html>
手写render,vue会直接执行render, 省去了compiler过程。 但是手写render,对于我们开发和维护 都不友好。还是建议大家 使用 webpack + vue-loader,构建时compiler。
另外,如果是学习的话,建议运行时compiler。
下面,我们将采用运行时compiler,来一探究竟。
二. compileToFunctions
mount挂载时,如果没有传入render函数,vue会 先执行 compileToFunctions函数,返回render函数,并将render函数,挂载到vm.$options上。以便后续 执行 patch之前,生成虚拟dom。
核心主流程代码如下:
export const createCompiler = createCompilerCreator(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
}
})
总体流程分为三部分:
-
- 模板编译阶段
-
- 优化阶段
-
- 代码生成阶段 - 即转化为render函数
下面,我们将逐个击破
三. AST
ast 全名:Abstract Syntax Tree,即抽象语法树。是源代码语法结构的一种抽象表示。
在计算机中,任何问题的本质就是 数据结构 + 算法,ast也是一种数据结构,来描述源代码的一种结构化表示。
以我们上面的运行时demo为例:
parse的方法入参,第一个参数是template,是一个字符串, 即:
"<div data-test='这是测试属性'>{{ a }}</div>"
抽丝剥茧,我们先看到ast生成阶段parse方法 的核心入口:
// ... 省略一堆函数定义
parseHTML(template, {
// options
...,
start() {
// ...
},
end() {
// ...
},
chars() {
// ...
},
comment() {
// ...
}
})
parseHTML 主干如下:
export function parseHTML (html, options) {
const stack = []
// ...options
let index = 0
let last, lastTag;
while(html) {
last = html
if (!lastTag || !isPlainTextElement(lastTag)) {
let textEnd = html.indexOf('<')
if(textEnd == 0) {
if(comment.test(html)) {
const commentEnd = html.indexOf('-->')
// ...
if(commentEnd >= 0) {
// ...
advance(commentEnd + 3)
continue
}
}
if(conditionalComment.test(html)) {
// ...
const conditionalEnd = html.indexOf(']>')
if (conditionalEnd >= 0) {
advance(conditionalEnd + 2)
continue
}
}
if(html.match(doctype)) {
// ...
advance(doctypeMatch[0].length)
continue
}
if(html.match(endTag)) {
// ...
const curIndex = index
advance(endTagMatch[0].length)
parseEndTag(endTagMatch[1], curIndex, index)
continue
}
startTagMatch = parseStartTag()
if(startTagMatch) {
// ...
handleStartTag(startTagMatch)
if (shouldIgnoreFirstNewline(startTagMatch.tagName, html)) {
advance(1)
}
continue
}
}
if(textEnd >= 0) {
// 如果 < 出现在 纯文本中,需要宽容处理,先做为文本处理
}
if(textEnd < 0) {
// ...赋值text, advance跳过文本长度
}
}
}else {
// 处理script,style,textarea的元素
// 这里我们只处理textarea元素, 其他的两种Vue 会警告,不提倡这么写
// ...
}
function advance (n) {
index += n
html = html.substring(n)
}
}
我们可以看到parseHTML里面,实际上写了许多正则,去处理字符串。 这个其实不是尤大 从零写起, 尤大是参考 大神 John Resig 之前写的html parse库。
John Resig 何许人也? 正是大名鼎鼎的 JQuery之父。经历过 jquery时代的人, 那个时候jquery是神一般 的存在。
好了,回归正题,template字符串处理,大致流程如下:
-
- while循环 template 字符串
-
- 判断不能是 script, style这些标签,给出对应的警告信息
-
- 通过正则,获取开始标签 < 的字符串位置
-
- 通过正则,判断是否是注释节点,调用advance方法,重新记录index下标,跳过注释长度,截取去注释继续循环
-
- 通过正则,判断是否是条件注释节点。因为我们可能在template中使用条件注释,针对ie做一些事件。同理,调用advance方法,将index下标,跳转到条件注释字符串的尾部,截取掉条件注释,继续循环。
-
- 通过正则,判断是否是 Doctype 节点,同理,调用advance方法,将index下标跳转到 doctype 节点字符串尾部,截取掉 doctype, 继续循环。
-
- 通过正则,判断是否是开始标签,将开始标签的内容提取出来,提取前后对比:
// 匹配开始标签之前
html = "<div data-test='这是测试属性'>{{ a }}</div>"
// 提取之后
html = "{{ a }}</div>"
// 而此时 startTagMatch 变成这样:
{
start: 0,
end: 24,
tagName: 'div',
unarySlash: '',
attrs: [
"data-test='这是测试属性'",
// ...
]
}
开始标签解析完成 后,会调用 parseHTML第二个参数options上的start方法,即上面提到的 parseHTML调用代码:
// ... 省略一堆函数定义
parseHTML(template, {
// options
...,
start(tag, attrs, unary, start, end) {
// 调用这里的start方法,可以理解成,每次parse一部分html字符串,都会调用本次的 生命周期函数,start, end, chars, comment
// ...
let element: ASTElement = createASTElement(tag, attrs, currentParent)
// ...
},
end() {
},
// ...
})
根据上面的流程,我们已经知道,parseHtml会先 提取出 开始标签相关内容,即:
<div data-test='这是测试属性'>
然后根据startTagMatch数据,调用start方法,start方法调用createASTElement 返回astElement。其结构如下:
{
type: 1,
tag: 'dev',
rawAttrsMap: {},
parent: undefined,
children: [],
attrsMap: {
"data-test": "这是测试属性"
},
attrsList: [
{
start: 5,
end: 23,
name: 'data-test',
value: "这是测试属性"
}
]
}
-
- 开始标签内容处理完成后,去除开始内容后的字符串,变成这样:
html = "{{ a }}</div>"
进入下一个while循环,剩下的字符串,继续做为html字段值,再去走一遍以上流程。 此时会进入 :
if(textEnd >= 0) {
}
text变量会记录下来,即:
text = "{{ a }}"
调用advance,将index调至text字符串的尾部,截取掉{{ a }}
-
- 再次进入下一个while循环,即:
html = "</div>"
重复上面的过程,条件匹配到了结束标签,进入:endTagMatch,即"</div>"。 调用advance方法,将index移动到最后。 调用 parseEndTag 方法,触发end钩子。
advance相当于一个下标计算器,每解析完一步,就自动的移动到 之前解析过的尾部,开始下一部分解析
简单的理解就是:parseHTML方法,一边解析不同的内容一边调用对应的钩子函数生成对应的AST节点,最终完成将整个模板字符串转化成AST
总体来说,ast的类型,有3类。
- 正常标签节点处理,通过createASTElement 方法创建,其结构如下:
{
type: 1,
tag,
attrsList: [
// ...
],
attrsMap: {
// ...
},
rawAttrsMap: {},
parent,
children: []
}
- 匹配到字符变量相关,使用parseText解释器,其结构如下:
{
type: 2,
text: "{{ a }}",
expression: "_s(a)",
tokens: [
{
'@binding': 'a'
}
],
start: 24,
end: 31
}
- 纯文本,不包含变量,其结构如下:
{
type: 3,
text: "文本内容"
isComment: true,
start: xx,
end: xx
}
最终,template字符串,解析出来的ast结构如下:
{
"type": 1,
"tag": "div",
"attrsList": [
{
"name": "data-test",
"value": "这是测试属性",
"start": 5,
"end": 23
}
],
"attrsMap": {
"data-test": "这是测试属性"
},
"rawAttrsMap": {
"data-test": {
"name": "data-test",
"value": "这是测试属性",
"start": 5,
"end": 23
}
},
"children": [
{
"type": 2,
"expression": "_s(a)",
"tokens": [
{
"@binding": "a"
}
],
"text": "{{ a }}",
"start": 24,
"end": 31
}
],
"start": 0,
"end": 37,
"plain": false,
"attrs": [
{
"name": "data-test",
"value": "\"这是测试属性\"",
"start": 5,
"end": 23
}
]
}
需要说明是,我们看到expression 中有个_s, 这个是什么东西呢? 实际上,这个是在vue instance的 render-helpers中定义的。_s = toString
其定义如下:
export function toString (val: any): string {
return val == null
? ''
: Array.isArray(val) || (isPlainObject(val) && val.toString === _toString)
? JSON.stringify(val, null, 2)
: String(val)
}
ok, 到这里, ast的主干流程就结束了。
四. optimize
获取到ast树后,vue做了一层静态标记优化。给一些不变的节点打上标记,提升后面patch diff的性能。比如,有这样的标签:
<div>
<div>这是不变的内容1</div>
<div>这是不变的内容2</div>
<div>这是不变的内容3</div>
<div>这是不变的内容4</div>
</div>
那么,在进行diff的时候,这种标签都不需要比对,他是纯静态的标签,不会变化。最外层的div,称为:静态根节点。
根据上面生成的ast,我们知道有3种类型的 ast,分别是:
- type == 1, 普通元素节点
- type == 2, 包含变量的文本
- type == 3, 纯文本,不包含变量
由此可见,将type = 3的 ast都加上static = true标识 type = 2的ast,都加上static = false标识 type = 1的,需要进一步判断:
function isStatic (node: ASTNode): boolean {
if (node.type === 2) { // expression
return false
}
if (node.type === 3) { // text
return true
}
return !!(node.pre || (
!node.hasBindings && // no dynamic bindings
!node.if && !node.for && // not v-if or v-for or v-else
!isBuiltInTag(node.tag) && // not a built-in
isPlatformReservedTag(node.tag) && // not a component
!isDirectChildOfTemplateFor(node) &&
Object.keys(node).every(isStaticKey)
))
}
即:
-
- 如果节点使用了v-pre指令,那就断定它是静态节点;
-
- 没有pre,需要满足以下条件:
- 2.1 不能有v-,@, :开头属性
- 2.2 不能是内置的slot, component
- 2.3 必须是浏览器保留标签,不能是组件
- 2.4 不能是v-for的template标签
- 2.5 判断ast上每个key,是不是只有静态节点才有
标计完成之后,再去ast上,递归每个children进行标记。
在此基础之上,计算出根静态节点,那么diff时候,是根静态节点,那么这个根节点以下的内容都不需要再比较了。
优化完成后,ast的结构变成这样:(多了2个属性)
{
"type": 1,
"tag": "div",
// 这里添加静态标记
"static": false,
// 这里添加是否是根静态节点标记
"staticRoot": false,
"attrsList": [
{
"name": "data-test",
"value": "这是测试属性",
"start": 5,
"end": 23
}
],
"attrsMap": {
"data-test": "这是测试属性"
},
"rawAttrsMap": {
"data-test": {
"name": "data-test",
"value": "这是测试属性",
"start": 5,
"end": 23
}
},
"children": [
{
// 这里添加标记
"static": false,
"type": 2,
"expression": "_s(a)",
"tokens": [
{
"@binding": "a"
}
],
"text": "{{ a }}",
"start": 24,
"end": 31
}
],
"start": 0,
"end": 37,
"plain": false,
"attrs": [
{
"name": "data-test",
"value": "\"这是测试属性\"",
"start": 5,
"end": 23
}
]
}
五. generate
代码生成阶段,通过ast将转化为render函数,其代码如下:
export function generate (
ast: ASTElement | void,
options: CompilerOptions
): CodegenResult {
const state = new CodegenState(options)
const code = ast ? genElement(ast, state) : '_c("div")'
return {
render: `with(this){return ${code}}`,
staticRenderFns: state.staticRenderFns
}
}
生成代码阶段比较清晰,genElement方法,主要是判断不同的类型,调用不同的生成方法。本质上是一个json转化成另外一个json。值得注意的是,转化json的过程中,我们会看到 _m, _c, _o的这些方法。
这些可以在instance/render-helpers/index.js中可查看到
方法名 | 对应的helper方法 | _m | renderStatic | _o | markOnce | _l | renderList | _e | createEmptyVNode | _t | renderSlot | _b | bindObjectProps | _v | createTextVNode | _s | toString |
---|
这里生成的render函数如下:
with(this) {
return _c('div', {
attrs: {
"data-test": "这是测试属性"
}
},
[_v(_s(a))])
}
到这里, compiler过程就结束啦。这个时候,compiler出的render函数,将挂载到 vm.$options上。等待执行updateComponent方法执行时,生成虚拟dom。
注意:compiler只是返回render函数,并未执行render函数,所以这个阶段,还未触发Dep类的依赖收集
六. 总结
-
- compiler是将template字符串转化为render函数的过程
-
- 调用parse方法生成ast
- 2.1 parseHTML通过正则动态匹配出标签的开始内容,标签内内容,标签结束内容
- 2.2 不建议template中出现script, style标签,给出警告
- 2.3 从index = 0开始,匹配开始标签内容,调用advance将index移动至前一次的字符串末尾位置,返回出对应的数据结构描述标签开始内容。另外调用parse的开始生命周期函数,生成对应的 ast
- 2.4 分别处理 注释节点, 条件注释,Doctype节点,调用advance将index移动到特殊节点字符串的末尾
- 2.5 while循环计算下一个字符串类型,匹配标签内容
- 2.6 标签内容调用 parse生命周期的chars方法,生成对应的ast
- 2.7 匹配结束标签,调用advance将index移动到对应字符串尾部,调用parse的end生命 周期方法,更新对应ast的end标识位
- 2.8 如此往复调用,直到解析html字符串的最后。
-
- 优化ast,给各个节点的ast打上静态标记,以及静态 根节点,以便patch过程做diff时,去除不必要的对比,提升性能。
-
- 将ast的数据结构,递归遍历每个childrens,将其转化为对应的方法调用。
-
-
返回render函数,将方法挂载至vm.$options上,等待后面执行到updateComponent时生成虚拟DOM
-
码字不易,多多关注,点赞 Thanks♪(・ω・)ノ
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!