模板编译
浏览器其实并不能理解vue的模板语法,vue实际上是先利用编译器(compiler)在编译阶段实现模板编译 ,即将模板语法转换成vnode,然后在通过patch操作修改DOM来达到更改视图的目的。
模板编译包含几个重要操作和中间产物,大致流程如下:
复制 template -> (parse) -> ast -> (transform) => codegenNode -> (generate) -> render函数
三个重要的步骤:
parse: 解析template生成token并构建模板AST。
transform: 将模板AST转换成能够生成JavaScript渲染函数代码的AST(codegenNode)
generate:根据前面生成的AST来生成渲染函数代码。
解析模板并构建AST的思路
HTML解析规范
事实上,解析html并构造token是有规范的,见HTML规范 。
**vue使用有限状态自动机来解析模板。**所谓“有限状态”,就是指有限个状态,而“自动机”意味着随着字符的输入,解析器会自动地在不同状态间迁移。
例如上面的Data 状态,它规定了在当前状态下,不同的下个输入字符所能进入的不同状态以及相应的处理方法。像这样的状态有很多,解析过程就在这些状态中不停的流转,直到到达结尾。在这个过程中会创建很多token,利用这些token可以构建ast。
另外除了state外,HTML规范还定义了模式(mode),规则如下:
<title>
标签、<textarea>
标签,当解析器遇到这两个标签时,会切换到 RCDATA 模式;
<style>
、<xmp>
、<iframe>
、<noembed>
、<noframes>
、<noscript>
等标签,当解析器遇到这些标签时,会切换到 RAWTEXT 模式;
当解析器遇到 <![CDATA[
字符串时,会进入 CDATA 模式。
其他模式与Data模式的解析方式相似,简单起见,这里只考虑Data state。
以一个简单的html来说明解析流程:
解析过程是这样:
读取下一个输入字符,是ASCII码h
,创建一个新的start tag token
,将其标签名称设置为空字符串,切换到tag name state ,并将当前字符作为下一个输入字符。
读取下一个输入字符,是ASCII码h
,将当前输入字符的小写版本添加到当前tag token
的标签名称,不流转状态。
读取下一个输入字符,是ASCII码1
,将当前输入字符的小写版本添加到当前tag token
的标签名称,不流转状态。
读取下一个输入字符,发现是V
,将该字符作为character token
发送 。
读取下一个输入字符,发现是u
,将该字符作为character token
发送 。
读取下一个输入字符,发现是e
,将该字符作为character token
发送 。
读取下一个输入字符,发现是ASCII码h
,创建一个新的end tag token
,将其标签名称设置为空字符串,切换到tag name state ,并将当前字符作为下一个输入字符。
读取下一个输入字符,发现是ASCII码h
,将当前输入字符的小写版本添加到当前tag token
的标签名称,不流转状态。
读取下一个输入字符,发现是ASCII码1
,将当前输入字符的小写版本添加到当前tag token
的标签名称,不流转状态。
读取下一个输入字符串,EOF,发现读取完毕,发送end-of-file token
。
在上面的例子中我们可以看到每次都只取出一个字符进行判断,并且每次判断完后都会自动读取下一个字符再判断(某些情况会将当前字符重新作为下一个字符),假设当前组件模板字符串存于context.source
中,我们可以通过不断的删除前面已经判断过的字符来使每次都能通过context.source[0]
获取到下一个字符。
复制 while ( context .source){
if (mode == 'data' && context .source[ 0 ] === '<' ){
mode = 'tag open'
// ...
} else if (mode == 'tag open' && /[a-z]/ .test ( context .source[ 0 ])){
mode = 'tag name'
// ...
} else if ( context .source[ 0 ] === '>' ){
mode = 'data'
// ...
}
// ...
context .source = context . source .slice ( 1 )
}
判断是否是自闭和标签
HTML中有些标签是自闭和的,例如<br />
。假如该标签是自闭和标签,则表示它没有闭合标签,解析方式与闭和标签不同,因此我们在解析标签时还得判断其是否是自闭和标签。
判断的方式有两种,一种是设立自闭和标签名单,然后判断标签名是否在自闭和名单之中。
另一种更简单的方法是判断标签是否以/>
结束。
复制 let isSelfClosing = false
isSelfClosing = startsWith ( context .source , '/>' )
解析插值语法和attribute绑定
通过HTML规范我们已经能够实现解析HTML,但是vue的template与HTML并不完全相同,vue template对HTML做了增强,例如插值语法({{}}
)、Attribute 绑定等。
判断是否是插值语法很简单,只需要在文本状态时读取是否有{{
即可,当读到}}
表示插值语法结束。
而attribute属性也有规律可循,首先attribute属性是在标签内部,其次是name=value
的形式,例如<a href=”http://123.com” >
,因此我们可以用正则来获取name和value,判断是否是attribute绑定也很简单,直接判断是否以:
、 v-on
等开始即可。
复制 // Name.
const start = getCursor (context)
const match = / ^ [ ^ \t\r\n\f />][ ^ \t\r\n\f />=] * / .exec ( context .source) !
// 假设此时context.source = ':href="">',可以匹配到:href
const name = match[ 0 ]
// 删除已匹配的name和=
context .source = context . source .slice ( name . length + 1 )
if ( ! context .inVPre && / ^ (v- | : | \. | @ | #)/ .test (name)) {
// 如果attribute有使用绑定,则需要进一步处理
}
value的处理分三种情况:被单引号包裹、被双引号包裹、没有引号包裹,例如a='a'
、 b="b"
和c=c
。被引号包裹的两种情况容易处理,第三种情况复杂一些,因为属性之间由空格、制表符等符号分隔,所以可以通过正则表达式/^[^\t\r\n\f >]+/
匹配到从当前字符到下个属性或标签结束标志(>号)前的内容,例如foo=value a=c>
,因为name和=被使用过删除了,因此剩下value a=c>
,正则表达式/^[^\t\r\n\f >]+/
能匹配到value。
这里还要注意,如果是不被引号包裹,那么value
就不能再出现引号等特殊符号。例如foo=f"oot"
,这可能是用户书写错误,可以用正则表达式/["'<=
]/g`提取并发送错误。
复制 // a='a' b="b" c=c
const quote = context .source[ 0 ]
const isQuoted = quote === `"` || quote === `'`
if (isQuoted) {
// value被引号包裹的情况
const endIndex = context . source .indexOf (quote)
context .source = context . source .slice ( 1 ) // 删除左侧的引号
if (endIndex === - 1 ) {
// 如果没有右侧的引号,则将source后面的所有内容都作为attribute
content = parseTextData (
context ,
context . source . length ,
TextModes . ATTRIBUTE_VALUE
)
} else {
// 如果有右侧的引号,则提取引号内的value
content = parseTextData (context , endIndex , TextModes . ATTRIBUTE_VALUE )
}
}
else {
// 没有引号包裹的情况
const match = / ^ [ ^ \t\r\n\f >] + / .exec ( context .source);
if ( ! match) {
return undefined ;
}
const unexpectedChars = /["'<=`]/ g ;
let m;
while ((m = unexpectedChars .exec (match[ 0 ]))) {
// 将引号的位置发送给错误处理机制,提醒用户。
emitError (
context ,
ErrorCodes . UNEXPECTED_CHARACTER_IN_UNQUOTED_ATTRIBUTE_VALUE ,
m .index
)
}
content = parseTextData (context , match[ 0 ]. length , TextModes . ATTRIBUTE_VALUE )
}
属性解析阶段结束的标志应当是下一个输入字符为>
或者/>
的时候。所以整个属性解析过程应该是在while循环的内部。
复制 while (
context . source . length > 0 &&
! startsWith ( context .source , '>' ) &&
! startsWith ( context .source , '/>' )
) {
const attr = parseAttribute (context , attributeNames)
// ...
}
构建AST
通过上一节的介绍,我们已经能够解析template,下一步就是根据解析过程中生成的token来构建AST。
以下面这个例子:
复制 < h1 >< span >Vue</ span ></ h1 >
通过解析获得了下面的tokens:
复制 [
{type : "start tag" , tagName : "h1" } ,
{type : "start tag" , tagName : "span" } ,
{type : "text" , value : "Vue" } ,
{type : "end tag" , tagName : "span" } ,
{type : "end tag" , tagName : "h1" } ,
{type : "end-of-file" }
]
ast是树结构,我们需要扫描tokens来构建这颗树,这里需要借助栈结构。每次从tokens出队一个节点,如果type是开始标签,则作为栈顶的子节点,并将其添加进栈结构中,作为下一个节点的父节点,如果type是结束标签,则移除栈顶节点,当type为EOF时结束。
复制 const tokens = [
{ type : "start tag" , tagName : "h1" } ,
{ type : "start tag" , tagName : "span" } ,
{ type : "text" , value : "Vue" } ,
{ type : "end tag" , tagName : "span" } ,
{ type : "end tag" , tagName : "h1" } ,
{type : "end-of-file" }
];
const root = {type : "root" , children : []}
const nodeStack = [root];
while ( nodeStack . length ) {
const parent = nodeStack[ nodeStack . length - 1 ];
const currentNode = tokens .shift ();
if ( currentNode .type === 'start tag' ) {
const element = {type : 'element' , tag : currentNode .tagName , children : []}
parent . children .push (element);
nodeStack .push (element)
} else if ( currentNode .type === 'text' ) {
const textNode = {type : 'text' , value : currentNode .value}
parent . children .push (textNode)
} else if ( currentNode .type === 'end tag' ) {
nodeStack .pop ()
} else if ( currentNode .type === 'end-of-file' ){
break ;
}
}
console .log (root);
vue-compiler源码
在实际的vue源码中,由compiler来完成从解析模板到生成渲染函数的整个过程。
在vue的源码中有4个负责compiler的package,分别是:
compiler-core
存放compile核心逻辑代码,这部分代码无关运行环境,compiler-sfc
提供转换sfc为渲染函数的底层API,而compiler-ssr
和compiler-dom
则是分别提供SSR环境下和DOM环境下的编译API。
compiler-ssr这部分不展开讲
vue编译器最主要的作用就是vue模板转换成渲染函数,然后再由vue渲染器渲染成DOM。
vue编译器的主要步骤是:
parse函数将vue单页面组件转换成模板ast,然后再调用transform函数将模板ast进行转换,最后调用generator函数将ast转换成渲染函数。
parse
vue解析模板最核心的代码存放在@vue/compiler-core下的parse.ts文件中。
vue解析模板的入口是baseParse
函数。
复制 export function baseParse (
content : string ,
options : ParserOptions = {}
) : RootNode {
const context = createParserContext (content , options)
const start = getCursor (context)
return createRoot (
parseChildren (context , TextModes . DATA , []) ,
getSelection (context , start)
)
}
其中最重要的解析逻辑位于parseChildren
函数中,其实parseChildren
函数就相当于一个状态机了。
实际上vue源码要比我们前面写的代码复杂一些,因为vue考虑的情况更多,node的种类也比较多。下面是解析element和text生成的token。
复制 // 解析标签
function parseTag (
context : ParserContext ,
type : TagType ,
parent : ElementNode | undefined
) : ElementNode | undefined {
// ...
return {
type : NodeTypes . ELEMENT , // 节点类型,例如root、element、text、comment等
ns , // Namespace
tag , // 标签名
tagType , // element、component、slot、template
props ,
isSelfClosing , // 是否是自闭和标签
children : [] ,
loc : getSelection (context , start) ,
codegenNode : undefined // to be created during transform phase
}
}
// 解析文本
function parseText (context : ParserContext , mode : TextModes ) : TextNode {
// ...
return {
type : NodeTypes . TEXT ,
content ,
loc : getSelection (context , start)
}
}
vue定义了很多函数来表示html解析状态,例如:
parseChildren: 代表data state
。
parseElement:包含tag open state
和tag end state
两个阶段。
parseTag: 代表tag name state
。
按照HTML规范,每当进入另一个状态时,在代码的体现就是调用对应的函数。例如从data state
到tag open state
,实际上就是在parseChildren
中调用parseElement
函数。
复制 // 注意,为了方便理解,省略了部分不重要的代码
function parseChildren (
context : ParserContext ,
mode : TextModes ,
ancestors : ElementNode []
) : TemplateChildNode [] {
// ancestors是一个栈,调用last函数获取这个栈的栈顶元素
const parent = last (ancestors)
const ns = parent ? parent .ns : Namespaces . HTML
const nodes : TemplateChildNode [] = []
while ( ! isEnd (context , mode , ancestors)) {
const s = context .source
let node : TemplateChildNode | TemplateChildNode [] | undefined = undefined
if (mode === TextModes . DATA || mode === TextModes . RCDATA ) {
if ( ! context .inVPre && startsWith (s , context . options .delimiters[ 0 ])) {
// 插值语法, 即匹配'{{'
node = parseInterpolation (context , mode)
} else if (mode === TextModes . DATA && s[ 0 ] === '<' ) {
// Tag open state
if ( s . length === 1 ) {
emitError (context , ErrorCodes . EOF_BEFORE_TAG_NAME , 1 )
} else if (s[ 1 ] === '!' ) {
// markup declaration open state.
if ( startsWith (s , '<!--' )) {
// 注释节点
node = parseComment (context)
} else if ( startsWith (s , '<!DOCTYPE' )) {
// 忽略文档声明.
node = parseBogusComment (context)
} else {
emitError (context , ErrorCodes . INCORRECTLY_OPENED_COMMENT )
node = parseBogusComment (context)
}
} else if ( /[a-z]/ i .test (s[ 1 ])) {
// tag open state
node = parseElement (context , ancestors)
} else {
emitError (context , ErrorCodes . INVALID_FIRST_CHARACTER_OF_TAG_NAME , 1 )
}
}
}
if ( ! node) {
// 解析文本
node = parseText (context , mode)
}
if ( isArray (node)) {
for ( let i = 0 ; i < node . length ; i ++ ) {
pushNode (nodes , node[i])
}
} else {
pushNode (nodes , node)
}
}
// ...
return nodes
}
当出现不合规范的模板语法时,vue会通过emitError
来发送error
提示用户,context对象包含当前解析字符的信息,包含source(模板字符串)、line(当前字符第几行)、column(当前字符第几列)、offset(当前字符串的偏移量)等 ,vue的错误处理机制会通过这些信息将错误代码复现给用户,以便快速找到错误。
ancestors事实上就相当于我们前面所说的nodeStack
栈,通过这个栈来构建ast树结构,last(ancestors)
实际上就是获取这个栈的栈顶元素,这个栈顶元素就是当前节点的父节点。
vue不会先扫描完source生成所有的token后再构建ast,而是边扫描source生成token边构建ast。
transform
通过parse操作我们已经成功地将模板语法转换成了AST(抽象语法树),我们的最终目的是要将其转换成渲染函数代码,因此需要将前面的模板AST进行转换,以便后面更好地生成渲染函数代码。我们设计一个transform
函数来实现。
transform
函数位于@vue/compiler-core包下transform.ts文件中。
前面我们讲过,ast是一个N叉树,那么我们要将其转换就应该对它进行深度优先遍历。我们使用traverseNode
函数实现深度遍历逻辑,为了方便,我们将循环遍历node.children
的操作放到traverseChildren
函数里。
复制 function traverseNode (node , context) {
context .currentNode = node;
context .childrenIndex = 0 ;
/* 处理逻辑 */
if ( node .type === 'element' && node . props .find (i => i .name === 'if' )) {
// ...
}
if ( node .type === 'element' && node . props .find (i => i .name === 'slot' )) {
// ...
}
// 这里省略一大段代码
// if ...
// 深度优先遍历
if ( node .type === "element" || node .type === "root" ) {
traverseChildren ( node .context);
} else if ( node .type === "comment" ) {
// ...
}
context .currentNode = node
}
function traverseChildren (parent , context) {
for ( let i = 0 ; i < parent . children . length ; i ++ ) {
const child = parent .children[i];
context .parent = parent;
context .childIndex = i;
traverseNode (child , context);
}
}
const nodes = [
{
type : "root" ,
children : [
{
type : "element" ,
tag : "div" ,
children : [
{
type : "element" ,
tag : 'h1' ,
props : [{
name : "foo" ,
value : "value"
}] ,
children : [
{
type : "text" ,
value : "Hello World" ,
} ,
] ,
} ,
] ,
} ,
{
type : "element" ,
tag : "br" ,
children : [] ,
} ,
] ,
} ,
];
traverseNode (nodes , context)
随着转换逻辑越来越多,越来越复杂,我们的traverseNode
函数也会越来越臃肿,因此我们可以将这些转换逻辑提取到一个个函数中去。更好的办法是以插件化的形式来实现,也就是将这些转换函数当成一个个插件放到一个插件数组中去,然后循环调用这个插件数组中的每一个插件函数,这个插件数组就是context.nodeTransforms
。
在执行这些插件函数时会将上下文context
对象一同传入,我们可以提前将一些信息挂载到这个上下文对象中,例如将当前处理的node
对象挂载到context.currentNode
中,之后在插件函数执行时就可以通过这个context
对象来获取一些我们想要的信息,例如当前节点类型context.currentNode.type
。
复制 function traverseNode (node , context) {
context .currentNode = node;
context .childrenIndex = 0 ;
/* 处理逻辑 */
const { nodeTransforms } = context;
for ( let i = 0 ; i < nodeTransforms . length ; i ++ ) {
nodeTransforms[i](node);
}
// ...
}
traverseNode (nodes , {
nodeTransforms : [
transformOnce ,
transformIf ,
transformMemo ,
transformFor ,
... (__COMPAT__ ? [transformFilter] : []) ,
... ( ! __BROWSER__ && prefixIdentifiers
? [
// order is important
trackVForSlotScopes ,
transformExpression ,
]
: __BROWSER__ && __DEV__
? [transformExpression]
: []) ,
transformSlotOutlet ,
transformElement ,
trackSlotScopes ,
transformText ,
] ,
});
generate
现在就只差最后一步了,那就是生成渲染函数代码,这部分由generate
函数完成。
generate的逻辑存放在@vue/compiler-core包下的codegen.ts文件中。
codegenNode
transform
后的ast
存放在ast.codegenNode
属性中。例如下面这段代码,经过parse和transform后会变成这样:
复制 < div >
< h1 :foo="num">Hello World</h1>
</div>
复制 codegenNode = {
type : 13 ,
tag : "div" ,
props : undefined ,
children : [
{
type : 1 ,
ns : 0 ,
tag : "h1" ,
tagType : 0 ,
props : [
{
type : 7 ,
name : "bind" ,
exp : {
type : 4 ,
content : "_ctx.num" ,
isStatic : false ,
constType : 0 ,
loc : {
// ...
source : "num" ,
} ,
} ,
arg : {
type : 4 ,
content : "foo" ,
isStatic : true ,
constType : 3 ,
loc : {
// ...
source : "foo" ,
} ,
} ,
modifiers : [
] ,
loc : {
// ...
source : ":foo=\"num\"" ,
} ,
} ,
] ,
isSelfClosing : false ,
children : [
{
type : 2 ,
content : "Hello World" ,
loc : {
// ...
source : "Hello World" ,
} ,
} ,
] ,
loc : {
// ...
source : "<h1 :foo=\"num\">Hello World</h1>" ,
} ,
codegenNode : {
type : 13 ,
tag : "\"h1\"" ,
props : {
type : 15 ,
loc : {
// ...
source : "<h1 :foo=\"num\">Hello World</h1>" ,
} ,
properties : [
{
type : 16 ,
loc : {
source : "" ,
// ...
} ,
key : {
type : 4 ,
content : "foo" ,
isStatic : true ,
constType : 3 ,
loc : {
// ...
source : "foo" ,
} ,
} ,
value : {
type : 4 ,
content : "_ctx.num" ,
isStatic : false ,
constType : 0 ,
loc : {
// ...
source : "num" ,
} ,
} ,
} ,
] ,
} ,
children : {
type : 2 ,
content : "Hello World" ,
loc : {
// ...
source : "Hello World" ,
} ,
} ,
patchFlag : "8 /* PROPS */" ,
dynamicProps : {
type : 4 ,
loc : {
source : "" ,
// ...
} ,
content : "_hoisted_1" ,
isStatic : false ,
constType : 2 ,
hoisted : {
type : 4 ,
loc : {
source : "" ,
// ...
} ,
content : "[\"foo\"]" ,
isStatic : false ,
constType : 0 ,
} ,
} ,
directives : undefined ,
isBlock : false ,
disableTracking : false ,
isComponent : false ,
loc : {
// ...
source : "<h1 :foo=\"num\">Hello World</h1>" ,
} ,
} ,
} ,
] ,
patchFlag : undefined ,
dynamicProps : undefined ,
directives : undefined ,
isBlock : true ,
disableTracking : false ,
isComponent : false ,
loc : {
// ...
source : "<div>\r\n\t\t<h1 :foo=\"num\">Hello World</h1>\r\n\t</div>" ,
} ,
}
codegenNode
存放着能够生成渲染函数的一切信息,我们可以通过遍历codegenNode
来生成渲染函数代码。
在经过generate后会生成最终的渲染函数代码:
复制 import { createElementVNode as _createElementVNode , openBlock as _openBlock , createElementBlock as _createElementBlock } from "vue"
export function render (_ctx , _cache , $props , $setup , $data , $options) {
return ( _openBlock () , _createElementBlock ( "div" , null , [
_createElementVNode ( "h1" , { foo : _ctx .num } , "Hello World" , 8 /* PROPS */ , [ "foo" ])
]))
}
可以在vue模板编译预览网站查看Vue Template Explorer (vuejs.org) 模板编译结果,在控制台可以看到AST。
首先渲染函数以函数声明开头,因此我们可以直接写出:
复制 const functionName = ssr ? `ssrRender` : `render` ;
const args = ssr ? [ '_ctx' , '_push' , '_parent' , '_attrs' ] : [ '_ctx' , '_cache' ];
if ( options .bindingMetadata && ! options .inline) {
// binding optimization args
args .push ( '$props' , '$setup' , '$data' , '$options' );
}
const signature = options .isTS
? args .map (arg => ` ${ arg } : any` ) .join ( ',' )
: args .join ( ', ' );
push ( `function ${ functionName } ( ${ signature } ) {` );
indent ()
push ( `return ` )
genNode ( ast .codegenNode , context)
deindent ()
push ( `}` )
function push (code){
context .code += code; // 将代码添加到context.code中
// 。。。省略一些处理
}
functionName
是渲染函数的函数名,signature
则是函数形参。push
函数主要的作用是将code代码添加到context.code
中,indent
和deindent
函数则是为生成的context.code
增加/删除缩进。
genNode()
genNode
函数则是生成渲染函数内部逻辑的方法。
复制 function genNode (node : CodegenNode | symbol | string , context : CodegenContext ) {
if ( isString (node)) {
context .push (node)
return
}
if ( isSymbol (node)) {
context .push ( context .helper (node))
return
}
switch ( node .type) {
case NodeTypes . ELEMENT :
case NodeTypes . IF :
case NodeTypes . FOR :
__DEV__ &&
assert (
node .codegenNode != null ,
`Codegen node is missing for element/if/for node. ` +
`Apply appropriate transforms first.`
)
genNode ( node .codegenNode ! , context)
break
case NodeTypes . TEXT :
genText (node , context)
break
case NodeTypes . SIMPLE_EXPRESSION :
genExpression (node , context)
break
case NodeTypes . INTERPOLATION :
genInterpolation (node , context)
break
case NodeTypes . TEXT_CALL :
genNode ( node .codegenNode , context)
break
case NodeTypes . COMPOUND_EXPRESSION :
genCompoundExpression (node , context)
break
case NodeTypes . COMMENT :
genComment (node , context)
break
case NodeTypes . VNODE_CALL :
genVNodeCall (node , context)
break
// ...
default :
if (__DEV__) {
assert ( false , `unhandled codegen node type: ${ (node as any ).type } ` )
// make sure we exhaust all possible types
const exhaustiveCheck : never = node
return exhaustiveCheck
}
}
}
codegenNode
本质上是一颗N叉树,因此vue采用递归的方式进行深度遍历。例如在创建元素vnode时(即生成_createElementVNode函数时),genNode
会调用genVNodeCall
函数(此时node.type===NodeTypes.VNODE_CALL
),genVNodeCall
函数内部会调用genNodeList
函数,而genNodeList
函数又会遍历node.children
并将其作为参数传入genNode
函数中。
复制 function genVNodeCall (node : VNodeCall , context : CodegenContext ) {
const { push , helper , pure } = context
const {
tag ,
props ,
children ,
patchFlag ,
dynamicProps ,
directives ,
isBlock ,
disableTracking ,
isComponent
} = node
// ...
genNodeList (
genNullableArgs ([tag , props , children , patchFlag , dynamicProps]) ,
context
)
push ( `)` )
// ...
}
function genNodeList (
nodes : ( string | symbol | CodegenNode | TemplateChildNode [])[] ,
context : CodegenContext ,
multilines : boolean = false ,
comma : boolean = true
) {
const { push , newline } = context
for ( let i = 0 ; i < nodes . length ; i ++ ) {
const node = nodes[i]
if ( isString (node)) {
push (node)
} else if ( isArray (node)) {
genNodeListAsArray (node , context)
} else {
genNode (node , context)
}
if (i < nodes . length - 1 ) {
if (multilines) {
comma && push ( ',' )
newline ()
} else {
comma && push ( ', ' )
}
}
}
}
helper()
vue渲染函数内部会调用不同的函数来创造不同类型的vnode,而这些函数名都保存在helperNameMap
中。
复制 export const helperNameMap : any = {
[ FRAGMENT ] : `Fragment` ,
[ TELEPORT ] : `Teleport` ,
[ SUSPENSE ] : `Suspense` ,
[ KEEP_ALIVE ] : `KeepAlive` ,
[ BASE_TRANSITION ] : `BaseTransition` ,
[ OPEN_BLOCK ] : `openBlock` ,
[ CREATE_BLOCK ] : `createBlock` ,
[ CREATE_ELEMENT_BLOCK ] : `createElementBlock` ,
[ CREATE_VNODE ] : `createVNode` ,
[ CREATE_ELEMENT_VNODE ] : `createElementVNode` ,
[ CREATE_COMMENT ] : `createCommentVNode` ,
[ CREATE_TEXT ] : `createTextVNode` ,
[ CREATE_STATIC ] : `createStaticVNode` ,
[ RESOLVE_COMPONENT ] : `resolveComponent` ,
[ RESOLVE_DYNAMIC_COMPONENT ] : `resolveDynamicComponent` ,
[ RESOLVE_DIRECTIVE ] : `resolveDirective` ,
[ RESOLVE_FILTER ] : `resolveFilter` ,
[ WITH_DIRECTIVES ] : `withDirectives` ,
[ RENDER_LIST ] : `renderList` ,
[ RENDER_SLOT ] : `renderSlot` ,
[ CREATE_SLOTS ] : `createSlots` ,
[ TO_DISPLAY_STRING ] : `toDisplayString` ,
[ MERGE_PROPS ] : `mergeProps` ,
[ NORMALIZE_CLASS ] : `normalizeClass` ,
[ NORMALIZE_STYLE ] : `normalizeStyle` ,
[ NORMALIZE_PROPS ] : `normalizeProps` ,
[ GUARD_REACTIVE_PROPS ] : `guardReactiveProps` ,
[ TO_HANDLERS ] : `toHandlers` ,
[ CAMELIZE ] : `camelize` ,
[ CAPITALIZE ] : `capitalize` ,
[ TO_HANDLER_KEY ] : `toHandlerKey` ,
[ SET_BLOCK_TRACKING ] : `setBlockTracking` ,
[ PUSH_SCOPE_ID ] : `pushScopeId` ,
[ POP_SCOPE_ID ] : `popScopeId` ,
[ WITH_SCOPE_ID ] : `withScopeId` ,
[ WITH_CTX ] : `withCtx` ,
[ UNREF ] : `unref` ,
[ IS_REF ] : `isRef` ,
[ WITH_MEMO ] : `withMemo` ,
[ IS_MEMO_SAME ] : `isMemoSame`
}
vue通过调用helper函数来获取这些函数名,例如_createCommentVNode
:
复制 function helper (key) {
return `_ ${ helperNameMap[key] } ` ;
}
function genComment (node : CommentNode , context : CodegenContext ) {
const { push , helper , pure } = context
if (pure) {
push ( PURE_ANNOTATION )
}
push ( ` ${ helper ( CREATE_COMMENT ) } ( ${ JSON .stringify ( node .content) } )` , node)
}
Block
渲染函数和我们平常使用的h
函数它们的目的都是为了创建vnode
。但是经过模板编译生成的渲染函数比h
函数多了一个令人困惑的Block
:
复制 export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock("div", null, [
_createElementVNode("h1", { foo: _ctx.num }, "Hello World", 8 /* PROPS */, ["foo"])
]))
}
关于这个Block的作用,在vue官网由提及: 渲染机制 | Vue.js (vuejs.org)
这里我们引入一个概念“区块”(Block),内部结构是稳定的一个部分可被称之为一个区块。在这个用例中,整个模板只有一个区块,因为这里没有用到任何结构性指令 (比如 v-if
或者 v-for
)。
**每一个块都会追踪其所有带更新类型标记的后代节点 (不只是直接子节点)。**所谓的更新标记是指patchFlag
,例如上面代码中patchFlag
就是8
。patchFlag的作用是为了提升虚拟 DOM 运行时性能,一个元素可以有多个更新类型标记,它们会被合并成一个数字。运行时渲染器也将会使用位运算来检查这些标记,确定相应的更新操作。
假如这里使用v-if等结构性指令,那么模板编译后的渲染函数是这样的:
复制 < div >
< h1 v-if = "isShow" >Hello World</ h1 >
</ div >
// 编译后的代码
import { openBlock as _openBlock , createElementBlock as _createElementBlock , createCommentVNode as _createCommentVNode } from "vue"
export function render (_ctx , _cache , $props , $setup , $data , $options) {
return ( _openBlock () , _createElementBlock ( "div" , null , [
( _ctx .isShow)
? ( _openBlock () , _createElementBlock ( "h1" , { key : 0 } , "Hello World" ))
: _createCommentVNode ( "v-if" , true )
]))
}
openBlock必须要在“createBlock”前调用,它会初始化currentBlock
,并将该currentBlock
加入到blockStack
中。由于Block树是能够嵌套的,因此用一个栈结构来存放所有的Block树。
复制 // Since v-if and v-for are the two possible ways node structure can dynamically
// change, once we consider v-if branches and each v-for fragment a block, we
// can divide a template into nested blocks, and within each block the node
// structure would be stable. This allows us to skip most children diffing
// and only worry about the dynamic nodes (indicated by patch flags).
export const blockStack : ( VNode [] | null )[] = []
export let currentBlock : VNode [] | null = null
/**
* Open a block.
* This must be called before `createBlock`. It cannot be part of `createBlock`
* because the children of the block are evaluated before `createBlock` itself
* is called. The generated code typically looks like this:
*
* ```js
* function render() {
* return (openBlock(),createBlock('div', null, [...]))
* }
* ```
* disableTracking is true when creating a v-for fragment block, since a v-for
* fragment always diffs its children.
*
* @private
*/
export function openBlock (disableTracking = false ) {
blockStack .push ((currentBlock = disableTracking ? null : []))
}
export function closeBlock () {
blockStack .pop ()
currentBlock = blockStack[ blockStack . length - 1 ] || null
}
后面的createElementBlock
函数内部会先调用createBaseVNode
函数创建vnode
,然后再调用setupBlock
函数。此外渲染函数中用于创建元素vnode的createElementVNode
函数其实就是createBaseVNode
函数的别名。
复制 // createElementVNode函数其实就是createBaseVNode函数的别名
export { createBaseVNode as createElementVNode }
export function createElementBlock (
type : string ,
props ?: Record < string , any > | null ,
children ?: any ,
patchFlag ?: number ,
dynamicProps ?: string [] ,
shapeFlag ?: number
) {
return setupBlock (
createBaseVNode (
type ,
props ,
children ,
patchFlag ,
dynamicProps ,
shapeFlag ,
true /* isBlock */
)
)
}
function createBaseVNode (
type : VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT ,
props : ( Data & VNodeProps ) | null = null ,
children : unknown = null ,
patchFlag = 0 ,
dynamicProps : string [] | null = null ,
shapeFlag = type === Fragment ? 0 : ShapeFlags . ELEMENT ,
isBlockNode = false ,
needFullChildrenNormalization = false
) {
// ...
// track vnode for block tree
if (
isBlockTreeEnabled > 0 &&
// avoid a block node from tracking itself
! isBlockNode &&
// has current parent block
currentBlock &&
// presence of a patch flag indicates this node needs patching on updates.
// component nodes also should always be patched, because even if the
// component doesn't need to update, it needs to persist the instance on to
// the next vnode so that it can be properly unmounted later.
( vnode .patchFlag > 0 || shapeFlag & ShapeFlags . COMPONENT ) &&
// the EVENTS flag is only for hydration and if it is the only flag, the
// vnode should not be considered dynamic due to handler caching.
vnode .patchFlag !== PatchFlags . HYDRATE_EVENTS
) {
currentBlock .push (vnode)
}
return vnode
}
function setupBlock (vnode : VNode ) {
// save current block children on the block vnode
vnode .dynamicChildren =
isBlockTreeEnabled > 0 ? currentBlock || ( EMPTY_ARR as any ) : null
// close block
closeBlock ()
// a block is always going to be patched, so track it as a child of its
// parent block
if (isBlockTreeEnabled > 0 && currentBlock) {
currentBlock .push (vnode)
}
return vnode
}
可以看到currentBlock
在追踪当前Block树的所有**带更新类型标记的后代节点。**最后在setupBlock
函数中将所有带更新类型标记的后代节点挂载到Block节点的dynamicChildren
属性下,这与官网的对Block的描述相对应。
总结
Vue模板编译的目的是将模板转换成渲染函数,期间会经过解析(parse)、转换(transform)和生成(generate)等操作,最终形成浏览器能够识别的JavaScript代码。
复制 template -> (parse) -> ast -> (transform) => codegenNode -> (generate) -> render
Vue模板编译主要依靠vue-compiler完成,将模板转换成渲染函数的目的是生成vnode,以便后续的vue-runtime能够动态更改视图(DOM)。