1.模板编译在整个渲染过程中的位置
模板->模板编译->渲染函数->vnode->用户界面
模板编译的主要目的就是生成渲染函数,而渲染函数的作用是每次执行它,它就会使用当前最新的状态生成vnode
2.将模板编译成渲染函数
1.将模板解析成AST
2.遍历AST标记静态节点
3.使用AST生成渲染函数
抽象出三个模块,解析器,优化器,代码生成器
解析器:解析器的作用就是将模板解析成AST
在解析器的内部,分成了很多小解析器,其中包括过滤解析器,文本解析器和HTML解析器,然后通过一条主线将这些主线解析器组装在一起
在使用模板时,我们可以在其中使用过滤器,而过滤器解析器就是用来解析过滤器,文本解析器用来解析{{}}语法,HTML解析器每当解析到HTML标签的开始位置和结束位置都会触发钩子函数
主线做的事情就是监听HTML解析器,每当触发钩子函数,就生成一个对应的AST节点。生成AST前,会根据类型使用不同的方式生成不同的AST
优化器:优化器的目的是遍历AST,检测出所有静态子树并给其打标记。当AST静态子树被打上标记之后,每次重新渲染时,就不需要为打上标记的静态节点创建新的虚拟节点,而是直接克隆已存在的虚拟节点
代码生成器是模板编译的最后一步,它的作用是将AST转换成渲染函数的内容
3.解析器
解析器的功能是将模板解析成AST,事实上,解析器内部也分了好几个解析器,比如HTML解析器,文本解析器以及过滤器解析器,其中最主要的HTML解析器,HTML解析器的主要作用是解析HTML,它在解析HTML的过程中会不断触发钩子函数。所以我们可以在钩子函数中构建AST节点,当HTML解析器不在触发钩子函数时,就说明所有模板都解析完毕,所有类型的节点都在钩子函数中构建完成,即AST构建完成。钩子函数start有三个参数,分别是tag,attrs,unary,分别说明标签名,标签的属性以及是否是自闭合标签
function createASTElement(tag, attrs, parent) {
return {
type: 1,
tag,
attrsList: attrs,
parent,
children: []
}
}
parseHTML(template, {
start(tag, attrs, unary) {
let element = createASTElement(tag, attrs, currentParent)
}
}
})
AST层级关系需要用一个栈来维护,每次触发start钩子函数的时候,把当前正在构建的节点推入栈中,每当触发钩子函数end时,从栈中弹出一个节点,这样就可以保证每当触发钩子函数的时候,栈的最后一个节点就是当前正在构建节点的父节点
(1)HTML解析器的运行原理
事实上,解析HTML模板的过程就是循环的过程,简单来说就是用HTML模板字符串来循环,每轮循环都从HTML模板中截取一小段字符串,然后重复以上过程,知道HTML模板被截取一个空字符串时结束循环,解析完毕,使用正则表达式对开始标签进行匹配。如果剩余模板的开始部分符合标签开始的规则,此时把解析出来的结果取出来并触发钩子函数start即可,如果是字符串,触发chars钩子函数,如果是结束标签,触发end钩子函数。HTML解析器的全部逻辑都在循环中执行,循环结束说明解析结束。
开始标签解析出来有三个属性, tagName, attrs,unary
解析标签属性
标签内的属性个数是不确定的,可能有一个,多个,或者没有,所以对于标签属性的解析我们使用的是解析一个属性截取一个属性,然后用正则表达式判断是否符合标签属性的特点,以此进行循环,直到满足标签结尾部分的特征,说明解析完毕
解析自闭合标签
自闭合标签是没有子节点的,而构建AST层级时,是需要维护一个栈的,一个节点是否需要被推入到栈中,可以使用自闭合标识来判断
结束标签的解析
结束标签的解析与开始标签的解析类似,先对模板进行截取,然后触发钩子函数
截取注释
注释的钩子函数触发与否可以通过配置来决定是否触发,只有options.shouldKeepCommet为真时,才会触发钩子函数
截取Doctype
只截取内容,不触发钩子函数
截取文本
如果HTML模板的第一个字符不是<,那么一定是文本了。
文本的范围则是再两个<括号之间。
如果整个模板都找不到<括号,则整个HTML模板都是文本。
最后则是触发钩子函数并将截取出来的文本放到参数中
一种特殊情况
如果将<前面的字符截取完之后,剩余的模板不符合任何需要被解析的片段的类型,就说明<是文本的一部分
比如1<2是需要被解析成文本的
while(html) {
let text, rest, next
let textEnd = html.indexOf('<')
//截取文本
if(textEnd >= 0) {
rest = html.slice(textEnd)
while(
!endTag.test(rest) &&
!startTagOpen.test(rest) &&
!comment.test(rest) &&
!conditionalCommet.test(rest)
) {
next = rest.indexOf('<', 1)
if(next < 0) break
textEnd += next
rest = html.slice(textEnd)
}
text = html.slice(0, textEnd)
html = html.substring(textEnd)
}
if(textEnd < 0) {
text = html
html = ''
}
if(options.chars && text) {
options.chars(text)
}
}
使用栈维护DOM层级
取父元素只需要拿到栈中的最后一项即可,另外栈还可以检测HTML标签是否正确的闭合了。
HTML解析器的功能总结
首先,html解析器是一个函数,有两个参数html, options,模板需要一段一段的去截取,直到全部截取完毕,循环中需要判断父元素是不是纯文本元素
如果父元素不是纯文本元素,则对应着几种类型,文本,注释,条件注释,Doctype,结束标签,开始标签
(2)文本解析器
文本解析器是对HTML解析的文本元素进行二次解析,其实主要是解析{{}}语法中的变量。
在HTML解析器解析到文本时,都会触发chars函数,并且从参数中得到解析出的文本。在chars函数中,我们需要构建文本类型的AST,并将它添加到父节点的children属性中
parseHTML(template, {
start(tag, attrs, unary) {
//每当解析到标签的开始位置时,触发该函数
},
end () {
//每当解析到标签的结束位置时,触发该函数
},
chars(text) {
text = text.trim()
if(text) {
const children = currentParent.children
let expression
if(expression = parseText(text)) {
children.push({
type: 2,
expression,
text
})
} else {
children.push({
type: 3,
text
})
}
}
},
commet(text) {
//每当遇到注释时,触发该函数
}
})
在chars函数中,如果执行parseText后有返回结果,则说明文本是带变量的文本,并且已经通过文本解析器(parseText)二次加工,此时构建一个带变量的文本类型的AST并将其添加到父节点的children属性中,否则直接构造一个普通的文本节点并将其添加到父节点中,也就是栈中的最后一个节点。
文本解析器的作用主要是使用正则表达式判断文本是否为带变量的文本,先把变量左边的文本添加到数组中,然后再把变量变成_s(x)这样的形式也添加到数组中,如果还有变量也是同样的做法。每次一个处理完成之后,修改lastIndex的值,而lastIndex之前的变量将不会再被匹配
解析器总结:
解析器的作用是通过模板得到AST。
生成AST的过程需要借助HTML解析器,当HTML解析器触发不同的钩子函数时,我们可以构造出不同的节点。
随后,我们可以通过栈来得到当前正在构建的节点的父节点,然后将构建出的节点添加到父节点的下面。。
最终,当HTML解析器运行完毕后,我们就可以得到一个完整的带DOM层级关系的AST。
HTML解析器的内部原理是一小段一小段的截取模板字符串,就会根据截取出来的字符串类型触发不同的钩子函数,直到模板字符串截空停止运行。
文本分两种类型,不带变量的纯文本和带变量的文本,后者需要用文本解析器进行二次加工
4.优化器
优化器的作用是在AST中找出静态子树并打上标记。
静态子树指的是那些永远不会变化的节点,所以一个纯文本节点就在静态子树中
标记静态子树的好处:
1.每次重新渲染的时候,不需要为静态子树创建新节点
在生成vnode的过程中,如果发现一个节点被标记为静态子树,那么除了首次渲染会生成节点之后,在重新渲染时并不会生成新的子节点树,而是克隆已经存在的静态子树
2.在虚拟DOM中打补丁的过程可以跳过
如果是都是静态子树,不需要对比都知道是一样的。
优化器的内部实现主要分为两个步骤
1.在AST中找出所有静态节点并打上标记
2.在AST中找出所有静态根节点并打上标记
静态节点的static属性为true
静态根节点的定义:如果一个节点下面所有子节点都是静态节点,并且父节点是动态节点,
静态根节点staticRoot属性为true
function markStatic(node) {
node.static = isStatic(node)
if(node.type === 1) {
for(let i = 0, l = node.children.length; i < l; i++) {
const child = node.children[i]
markStatic(child)
if(!child.static) {
node.static = false
}
}
}
}
//isStatic是判断一个节点是否是静态节点
function isStatic(node){
if(node.type === 2) {
return false
}
if(node.type === 3) {
return true
}
return !!(node.pre || ( //如果节点使用了指令v-pre,直接断定它是一个静态节点
!node.hasBindings && //没有动态绑定
!node.if && !node.for && //没有v-if&&v-for&&v-else
!isBuiltInTag(node.tag) && //不是内置标签
isPlatformReservedTag(node.tag) && //不是组件
!isDirectChildOfTemplateFor(node) && //当前节点的父节点不能是带v-for指令的template标签
Object.keys(node).every(isStaticKey) //节点中不存在动态节点才有的属性
))
}
type取值的说明:
1.元素节点
2.带变量的动态文本节点
3.不带变量的纯文本节点
由于递归是从上向下标记的,父节点被标记为静态节点之后,子节点却有可能是动态的,所以在子节点被打完标记之后,我们需要判断它是否为静态节点。
大部分情况下,我们找到的静态节点都会被标记为静态根节点,但是有一种情况不会标记,就是这个元素节点只有一个文本节点的时候,优化成本大于收益。比如一个元素节点是一个文本节点,即使它此时是一个静态节点也不会被标记。
优化器的作用和原理的总结:
优化器的作用是在AST中找出静态子树并打上标记,这样做有两个好处:
1.每次重新渲染时,不需要为静态子树创建新节点
2.在虚拟DOM中打补丁的过程可以跳过
优化器的内部实现主要分为两个部分
1.在AST中找出所有的静态节点并打上标记
2.在AST中找出所有的静态根节点并打上标记
5.代码生成器
代码生成是模板编译的最后一步,它的作用是将AST转换成渲染函数中的内容,这个内容可以称为代码字符串。
渲染函数执行createElement,createElement可以创建一个VNode节点
1.通过AST生成代码字符串
_c createElement
_v createTextVNode
_e createEmptyVNode
代码字符串的格式
_c('div',{attrs: "id": "el"}],[_c('div')])
节点名 节点属性名 子节点列表