Vue模板编译原理
关于vue的内部原理其实有很多个重要的部分,变化侦测,模板编译,virtualDOM,整体运⾏流程等。
之前写过⼀篇《深⼊浅出 - vue变化侦测原理》讲了关于变化侦测的实现原理。
那今天主要把模板编译这部分的实现原理单独拿出来讲⼀讲。
本⽂我可能不会在⽂章中说太多细节部分的处理,我会把 vue 对模板编译这部分的整体原理讲清楚,主要是让读者读完⽂章后对模板编译的整体实现原理有⼀个清晰的思路和理解。
关于 Vue 编译原理这块的整体逻辑主要分三个部分,也可以说是分三步,这三个部分是有前后关系的:
•
第⼀步是将模板字符串转换成 element ASTs(解析器)
•侵袭的袭是什么意思
第⼆步是对 AST 进⾏静态节点标记,主要⽤来做虚拟DOM的渲染优化(优化器)
•
第三步是使⽤ element ASTs ⽣成 render 函数代码字符串(代码⽣成器)
解析器
解析器主要⼲的事是将模板字符串转换成 element ASTs,例如:
< div> < p>{{name}}</ p></ div>
上⾯这样⼀个简单的模板转换成 element AST 后是这样的:
{ tag :"div"type :1, staticRoot :fal, static :fal, plain :true, parent :undefined, attrsList :[], attrsMap :{}, children :[ { tag :"p"type :1, staticRoot :fal, static :fal, plain :true, parent :{tag :"div", ...}, attrsList :[], attrsMap :{}, children :[{ type :2, text :"{{name}}", static :fal, expression :"_s(name)"}] } ]}
我们先⽤这个简单的例⼦来说明这个解析器的内部究竟发⽣了什么。
这段模板字符串会扔到 while 中去循环,然后⼀段⼀段的截取,把截取到的每⼀⼩段字符串进⾏解析,直到最后截没了,也就解析完了。
上⾯这个简单的模板截取的过程是这样的:
< div> < p>{{name}}</ p></ div> < p>{{name}}</ p></ div> < p>{{name}}</ p></ div> {{name}}</ p></ div> </ p></ div> </ div> </ div>
那是根据什么截的呢?换句话说截取字符串有什么规则么?
当然有
只要判断模板字符串是不是以 < 开头我们就可以知道我们接下来要截取的这⼀⼩段字符串是标签还是⽂本。
举个 :
<div></div> 这样的⼀段字符串是以 < 开头的,那么我们通过正则把 <div> 这⼀部分 match 出来,就可以拿到这样的数据:
小学四年级英语下册
{ tagName :'div', attrs :[], unarySlash :'', start :0, end :5}
好奇如何⽤正则解析出 tagName 和 attrs 等信息的同学可以看下⾯这个demo代码:
constncname='[a-zA-Z_][w-.]*'constqnameCapture=`((?:${ncname}:)?${ncname})`conststartTagOpen=newRegExp( `^<${qnameCapture}`) conststartTagClo=/^s*(/?)>/lethtml =`<div></div>`letindex =0conststart=html.
match(startTagOpen) constmatch={ tagName :start[ 1], attrs :[], start :0}html =html. substring(start[ 0]. length)index
+=start[ 0]. lengthletend, attr while( !(end =html. match(startTagClo)) &&(attr =html. match(attribute))) { html =html. substring(attr[ 0]. length) index +=attr[ 0]. lengthmatch. attrs. push(attr)} if(end) { match. unarySlash=end[ 1] html =html. substring(end[ 0]. length) index +=end[ 0]. lengthmatch. end=index} console. log(match) Stack
⽤正则把开始标签中包含的数据(attrs, tagName 等)解析出来之后还要做⼀个很重要的事,就是要维护⼀个 stack。
那这个 stack 是⽤来⼲什么的呢?
这个 stack 是⽤来记录⼀个层级关系的,⽤来记录DOM的深度。
更准确的说,当解析到⼀个开始标签或者⽂本,⽆论是什么, stack 中的最后⼀项,永远是当前正在被解析的节点的parentNode ⽗节点。
通过 stack 解析器就可以把当前解析到的节点 push 到⽗节点的 children 中。
也可以把当前正在解析的节点的 parent 属性设置为⽗节点。
事实上也确实是这么做的。
但并不是只要解析到⼀个标签的开始部分就把当前标签 push 到 stack 中。
因为在 HTML 中有⼀种⾃闭和标签,⽐如 input。
<input /> 这种⾃闭和的标签是不需要 push 到 stack 中的,因为 input 并不存在⼦节点。
所以当解析到⼀个标签的开始时,要判断当前被解析的标签是否是⾃闭和标签,如果不是⾃闭和标签才 push 到 stack 中。
if( !unary) { currentParent =element stack. push(element)}
现在有了 DOM 的层级关系,也可以解析出DOM的开始标签,这样每解析⼀个开始标签就⽣成⼀个 A
STElement (存储当前标签的attrs,tagName 等信息的object)
并且把当前的 ASTElement push 到 parentNode 的 children 中,同时给当前 ASTElement 的 parent属性设置为 stack 中的最后⼀项
currentParent. children. push(element) element. parent=currentParent < 开头的⼏种情况
但并不是所有以 < 开头的字符串都是开始标签,以 < 开头的字符串有以下⼏种情况:
•
开始标签 <div>
•negatively
结束标签 </div>
•
•
HTML注释 <!-- 我是注释 -->
•
Doctype <!DOCTYPE html>
•
peoples条件注释(Downlevel-revealed conditional comment)
va怎么读
当然我们解析器在解析的过程中遇到的最多的是开始标签结束标签和注释
截取⽂本wannabe
我们继续上⾯的例⼦解析,div 的开始标签解析之后剩余的模板字符串是下⾯的样⼦:
克龙< p>{{name}}</ p></ div>
这⼀次我们在解析发现模板字符串不是以 < 开头了。
那么如果模板字符串不是以 < 开头的怎么处理呢??
其实如果字符串不是以 < 开头可能会出现这么⼏种情况:
我是text < div></ div>
或者:
我是text </ p>
不论是哪种情况都会将标签前⾯的⽂本部分解析出来,截取这段⽂本其实并不难,看下⾯的例⼦:
//可以直接将本 demo 放到浏览器 console 中去执⾏consthtml='我是text </p>'lettextEnd =html. indexOf( '<')
consttext=html. substring( 0, textEnd) console. log(text)
当然 vue 对⽂本的截取不只是这么简单,vue对⽂本的截取做了很安全的处理,如果 < 是⽂本的⼀部分,那上⾯ DEMO 中截取的内容就不是我们想要的,例如这样的:
a <
b </ p>
如果是这样的⽂本,上⾯的 demo 肯定就挂了,截取出的⽂本就会遗漏⼀部分,⽽ vue 对这部分是进
strict怎么读⾏了处理的,看下⾯的代码:
lettextEnd =html. indexOf( '<') lettext, rest, next if(textEnd >=0) { rest =html. slice(textEnd) //剩余部分的 HTML 不符合标签的格式那肯定就是⽂本//并且还是以 < 开头的⽂本while( !endTag. test(rest) &&!startTagOpen. test(rest) &&!comment. test(rest) &&!conditionalComment. test(rest) ) { //< in plain text, be forgiving and treat it as textnext =rest. indexOf( '<', 1) if(next <0) breaktextEnd +=next rest =html. slice(textEnd) } text =html. substring( 0, textEnd) html =html. substring( 0, textEnd)}
这段代码的逻辑是如果⽂本截取完之后,剩余的模板字符串开头不符合标签的格式规则,那么肯定就是有没截取完的⽂本
这个时候只需要循环把 textEnd 累加,直到剩余的模板字符串符合标签的规则之后在⼀次性把 text 从模板字符串中截取出来就好了。
继续上⾯的例⼦,当前剩余的模板字符串是这个样⼦的:
继续上⾯的例⼦,当前剩余的模板字符串是这个样⼦的:
< p>{{name}}</ p></ div>
types截取之后剩余的模板字符串是这个样⼦的:
< p>{{name}}</ p></ div>
被截取出来的⽂本是这样的:
"n"
截取之后就需要对⽂本进⾏解析,不过在解析⽂本之前需要进⾏预处理,也就是先简单加⼯⼀下⽂本,vue 是这样做的:
constchildren=currentParent. childrentext =inPre ||text. trim() ?isTextTag(currentParent) ?text
:decodeHTMLCached(text) //only prerve whitespace if its not right after a starting tag:prerveWhitespace
&&children. length?'':''
这段代码的意思是:
•
如果⽂本不为空,判断⽗标签是不是或style,
1.
如果是则什么都不管,
2.
如果不是需要 decode ⼀下编码,使⽤github上的 he 这个类库的 decodeHTML ⽅法
如果⽂本为空,判断有没有兄弟节点,也就是 parent.children.length 是不是为 0
1.
如果⼤于0 返回 ' '
2.
如果为 0 返回 ''
结果发现这⼀次的 text 正好命中最后的那个 '',所以这⼀次就什么都不⽤做继续下⼀轮解析就好
继续上⾯的例⼦,现在的模板字符串变是这个样⼦:
< p>{{name}}</ p></ div>
接着解析 <p>,解析流程和上⾯的 <div> ⼀样就不说了,直接继续:
{{name}}</ p></ div>
通过上⾯写的⽂本的截取⽅式这⼀次截取出来的⽂本是这个样⼦的 "{{name}}"
如何提高自信心解析⽂本
其实解析⽂本节点并不难,只需要将⽂本节点 push 到 currentParent.children.push(ast) 就⾏了。
其实解析⽂本节点并不难,只需要将⽂本节点 push 到 currentParent.children.push(ast) 就⾏了。
但是带变量的⽂本和不带变量的纯⽂本是不同的处理⽅式。
带变量的⽂本是指 Hello {{ name }} 这个 name 就是变量。
不带变量的⽂本是这样的 Hello Berwin 这种没有访问数据的纯⽂本。
纯⽂本⽐较简单,直接将⽂本节点的ast push 到 parent 节点的 children 中就⾏了,例如:
children. push({ type :3, text :'我是纯⽂本'})
⽽带变量的⽂本要多⼀个解析⽂本变量的操作:
constexpression=parText(text, delimiters) //对变量解析 {{name}} => _s(name)children. push({ type :2, expression, text})
上⾯例⼦中 "{{name}}" 是⼀个带变量的⽂本,经过 parText 解析后 expression 是 _s(name),所以最后 push 到currentParent.children 中的节点是这个样⼦的:
{ expression :"_s(name)", text :"{{name}}", type :2} 结束标签的处理
现在⽂本解析完之后,剩余的模板字符串变成了这个样⼦:
</ p></ div>
这⼀次还是⽤上⾯说的办法,html.indexOf('<') === 0,发现是 < 开头的,然后⽤正则去 match 发现符合结束标签的格式,把它截取出来。
并且还要做⼀个处理是⽤当前标签名在 stack 从后往前找,将找到的 stack 中的位置往后的所有标签
全部删除(意思是,已经解析到当前的结束标签,那么它的⼦集肯定都是解析过的,试想⼀下当前标签都关闭了,它的⼦集肯定也都关闭了,所以需要把当前标签位置往后从 stack中都清掉)
结束标签不需要解析,只需要将 stack 中的当前标签删掉就好。
虽然不⽤解析,但 vue 还是做了⼀个优化处理,children 中的最后⼀项如果是空格 " ",则删除最后这⼀项:
if(lastNode &&lastNode. type===3&&lastNode. text===''&&!inPre) { element. children. pop()}
因为最后这⼀项空格是没有⽤的,举个例⼦:
< ul> < li></ li></ ul>
上⾯例⼦中解析成 element ASTs之后 ul 的结束标签 </ul> 和 li 的结束标签 </li> 之间有⼀个空格,这个空格也属于⽂本节点在 ul 的 children 中,这个空格是没有⽤的,把这个空格删掉每次渲染dom都会少渲染⼀个⽂本节点,可以节省⼀定的性能开销。
现在剩余的模板字符串已经不多了,是下⾯的样⼦:
</ div>
然后解析⽂本,就是⼀个其实就是⼀个空格的⽂本节点。
然后再⼀次解析结束标签 </div>
</ div>