深入理解 Vue 模板渲染:Vue 模板反編譯
vue 文件的構成
熟悉 vue 的同學應該都知道,vue 單文件模板中一般含有三個部分,template,script,style。
但是在編譯后的 js 文件中,我們卻沒法在代碼中直接找到這三部分,如果我們想從編譯后的 js 中獲取原始模板,應該怎么做?
vue 并非直接使用 template 進行渲染,而是需要把 template 編譯成渲染函數(shù),才能渲染。
new?Vue({
????render:function?()?{},
????staticRenderFns:[]
})
并且當一個 vue 單文件同時存在 template 標簽和 render 函數(shù)時,render 函數(shù)優(yōu)先生效。
事實上編譯工具也確實會把 vue 單文件模板編譯成這種形式,style 會單獨提取出來,綁定作用域作為標識,而 script 部分除了加入了 render 和 staticRenderFns 以外,基本不變。
/*?作用域標識為?data-v-3fd7f12e?*/
.effect-mask[data-v-3fd7f12e]?{
????opacity:?0;
}
//?js?中肯定能找到對應的作用域標識,關聯(lián)某個組件,上面的?css?就是這個組件的?style
j?=?i("X/U8")(F.a,?W,?!1,?Y,?"data-v-3fd7f12e",?null).exports,
因此,我們如果想把一個編譯后的單文件模板還原,主要的工作,就是把 render 和 staticRenderFns 中的模板從渲染函數(shù)還原成 template 模板。之后再把 script 引入的模塊還原,根據作用域標識找回樣式并格式化即可。
本文主要說明如何把 js 代碼構成的渲染函數(shù),還原成 template 模板。
處理 staticRenderFns
staticRenderFns 是 template 中的靜態(tài)模板片段,片段是純 html,不含變量和表達式。
對于這種靜態(tài)模板,我們通過構造上下文對渲染函數(shù)求值,就可以獲取到想要的結果。
staticRenderFns 格式如下:
staticRenderFns:?[function?()?{
????var?t?=?this.$createElement,
????????e?=?this._self._c?||?t;
????return?e("div",?{
????????staticClass:?"btn?on"
????},?[e("i",?{
????????staticClass:?"icon?iconfont"
????}),?e("span",?[this._v("下載")])])
}]
我們可以構造一個類 StaticRender,實現(xiàn) $createElement、_v、_self ,然后把 staticRenderFns 中的渲染函數(shù)掛載到 StaticRender 的實例上,這樣渲染函數(shù)就可以正常執(zhí)行。
$createElement 的函數(shù)簽名如下:
//?vue/types/vue.d.ts
export?interface?CreateElement?{
??(tag?:?string?|?Component?|?AsyncComponent?|?(()?=>?Component),?children?:?VNodeChildren):?VNode;
??(tag?:?string?|?Component?|?AsyncComponent?|?(( )?=>?Component),?data?:?VNodeData,?children?:?VNodeChildren):?VNode;
}
在 staticRenderFns 渲染函數(shù)中,我們可以認為 $createElement 第一個參數(shù)是節(jié)點標簽名,第二個參數(shù)是節(jié)點屬性對象,第三個參數(shù)是子節(jié)點數(shù)組,第二、三個參數(shù)可選,返回值是一個元素節(jié)點。
_v 只有一個參數(shù),返回一個文本節(jié)點。
我們只要構造好這兩個方法,就可以輕松獲得節(jié)點樹,然后把節(jié)點轉換成 html。
//?定義節(jié)點類型
interface?TextNode?{
????type:'text'
????text:string
}
interface?Element?{
????type:'element'
????tag:string
????attrMap?:{[key:string]:any}
????children:Node[]
}
type?Node?=?Element?|?TextNode?
//?定義?StaticRender?類
export?class?StaticRender?{
????_self?=?{}
????renderFunc:()=>Node??//?掛載的渲染函數(shù)
????constructor?(renderFunc:()=>Node)?{
????????this._self?=?{}
????????this.renderFunc?=?renderFunc
????}
????render?()?{??//?執(zhí)行渲染函數(shù),輸出html
????????var?root?=?this.renderFunc?()
????????var?_html?=?this.toHtml(root)
????????return?_html
????}
????toHtml?(root:Node):string?{
????????//?生成?html?
????}
????attrToString??(obj:{[key:string]:any})?{
????????//?格式化屬性到字符串
????}
????_v??(str:string)?{
????????return?{
????????????text:str,
????????????type:'text'
????????}
????}
????$createElement?(tag:string,attrMap:{[key:string]:any},children:Node[])?{
????????var?_tag,?_attrMap,?_children;
????????_tag?=?tag;
????????if(Array.isArray(attrMap)){
????????????_children?=?attrMap
????????}else{
????????????_attrMap?=?attrMap
????????}
????????if(Array.isArray(children)){
????????????_children?=?children
????????}
????????var?ret?=?{
????????????tag:_tag,
????????????type:'element',
????????????attrMap:_attrMap?||?{},
????????????children:_children?||?[]
????????}
????????return?ret;
????}
}
執(zhí)行求值,結果如下:
<div?class="btn?on">
????<i?class="icon?iconfont">?i>
????<span>?下載?span>?
div>
staticRenderFns 生成的 html 片段我們之后還會用到。
處理 render
render 渲染函數(shù)包含大量的變量、表達式,例如 v-if、v-for 的內容。我們很難通過構造簡單的上下文求值得到模板。
整體流程
編譯和還原本質上都是把代碼解析成語法樹然后進行變換,再生成新的代碼。
vue 模板在編譯時基本沒有丟掉原始信息,因為我們可以做到比較精準的還原。
并且由于 vue 模板涉及的語法特性較少,主體是聲明式的 xml,只涉及少量的 js 表達式,并且只用到了部分 js 語言特性,還原起來相對比較容易。
因此,對于 render,我們使用變換語法樹的方法獲得模板。

從流程來看,我們需要解析器,變換器,生成器三個部分。
解析器將渲染函數(shù)轉換為 js 語法樹。
變換器將 js 語法樹轉換成 vue 模板語法樹。
生成器將 vue 模板語法樹轉換成 vue 模板字符串。
解析器
其中解析器屬于比較大眾化的需求,eslint、壓縮/優(yōu)化、代碼高亮、類型檢查等等都需要用到解析器,自然可以找到可用的輪子。
把 js 代碼轉換成語法樹我們可以使用 @typescript-eslint/typescript-estree。
項目 estree[1] 則提供了各個版本 js 所定義的節(jié)點類型標準。
一個 estree 節(jié)點的基本類型定義如下,包含類型、位置、長度等信息:
interface?BaseNode?{
????type:string
????loc:{
????????end:{
????????????line:number
????????????start:number
????????},
????????start:{
????????????line:number
????????????start:number
????????}
????},
????range:[number,number]
}
不同的節(jié)點類型會增加各自特有的屬性,例如函數(shù)調用表達式的類型定義如下:
interface?CallExpressionBase?extends?BaseNode?{
????callee:?LeftHandSideExpression;
????arguments:?Expression[];
????typeParameters?:?TSTypeParameterInstantiation;
????optional:?boolean;
}
interface?CallExpression?extends?CallExpressionBase?{
????type:?AST_NODE_TYPES.CallExpression;
????optional:?false;
}
函數(shù)有調用者 callee 和參數(shù) arguments 兩個特有屬性。
完整的 js 語法樹節(jié)點類型定義可以在 ts-estree.ts[2] 查閱。
簡單的 api 調用就可以獲取到渲染函數(shù)的語法樹。
import?{?parse,?TSESTreeOptions,AST?}?from?"@typescript-eslint/typescript-estree"
class?Render?{
????options:TSESTreeOptions?=?{
????????errorOnUnknownASTType:true,
????????loc:true,
????????range:true,
????}
????ast:AST
????constructor?(code:string,staticTpls:string[])?{
????????this.ast?=?parse(code,?this.options);??//?獲取語法樹
????}
}
變換器
有了 js 語法樹節(jié)點類型定義,我們還需要 vue 模板的語法樹節(jié)點類型定義,才能正確地完成轉換。
一個 vue 模板語法樹節(jié)點類型定義如下:
刪減了非必要屬性,完整版本可以查看 index.d.ts[3]
type?ASTNode?=?ASTElement?|?ASTText?|?ASTExpression;
interface?ASTElement?{
??type:?1;
??tag:?string;
??attrsList:?{?name:?string;?value:?any?}[];
??attrsMap:?Record<string,?any>;
??parent:?ASTElement?|?undefined;
??children:?ASTNode[];
}
interface?ASTText?{
??type:?3;
??text:?string;
}
interface?ASTExpression?{
??type:?2;
??expression:?string;
??text:?string;
??tokens:?(string?|?Record<string,?any>)[];
}
render 用到的特性
編寫轉換邏輯前,我們先來看看 render 渲染函數(shù)的基本形式,以及它用到了哪些 js 特性、我們需要處理哪些東西。
此渲染函數(shù)包含了動態(tài)/靜態(tài)屬性,指令,v-for 列表,事件綁定等特性。
function()?{
????var?t?=?this
????????,?e?=?t.$createElement
????????,?i?=?t._self._c?||?e;
????return?i("transition",?{
????????attrs:?{??
????????????name:?"el-zoom"???//?屬性
????????},
????????on:?{???//?事件綁定
????????????click:?function(e)?{
????????????????t.onClick();
????????????}
????????}
????},?[i("div",?{???//?指令
????????directives:?[{
????????????name:?"show",
????????????rawName:?"v-show",
????????????value:?t.visible,
????????????expression:?"visible"
????????}],
????????staticClass:?"el-time-panel",
????????class:?t.popperClass
????},t._l(t.list,?function(e,?s)?{??//?v-for?列表
????return?i("ListPreview",?{
??????key:?s?+?"_"?+?e.id,???//?動態(tài)屬性
??????attrs:?{
????????spriteData:?e,
????????playFlag:?t.playing??//?動態(tài)屬性
??????}
????});
??}))])
}
render 渲染函數(shù)和 staticRenderFns 函數(shù)的格式一樣,都是定義一個局部變量賦值為 $createElement 方法,定義一個局部變量賦值為 this。
但是變量名并不是固定的,所以我們首先要分析出代表 $createElement 和 this 的變量。
staticRenderFns 渲染函數(shù)中,this下只用到了 _v 方法,render 渲染函數(shù)中,this 下掛載了更多的內置方法,它們都以 _ 開頭,我們主要需要處理的有:
_l:生成 v-for 結構 _e:生成空節(jié)點 _s:生成插值字符串 _m:生成靜態(tài) html 片段(staticRenderFns 中的 html 片段) _v:生成文本節(jié)點
其他不常見的內置函數(shù)可以遇到后再完善,例如 _u、_p 等。
完整的內置方法列表可以查閱 vue/render-helpers[4],其生成邏輯在 vue/codegen[5]
vue/codegen[6] 可以認為是 vue 模板的生成規(guī)范。
除此之外,this 下面還掛載了 vue 實例的 data 和 methods,這些都可以在模板中使用,也是我們要處理的對象。
v-if 以三元表達式的方式呈現(xiàn)。
轉換的基本思路
從 js 語法樹根節(jié)點開始遍歷,先獲取到 this 和 $createElement 對應的標識符
render 渲染函數(shù)內部一般不直接使用 this 和 $createElement,而是賦值給兩個局部變量。這兩個局部變量在渲染函數(shù)內會被大量使用,但是變量名并不是固定的,因此我們先要獲取到變量名,在上面的渲染函數(shù)示例中,變量名分別為 t 和 i。
在后面的遍歷中,如果 t 作為參數(shù)出現(xiàn)在表達式中,我們要判斷它是否是 this。如果 i 作為函數(shù)調用者出現(xiàn),我們要判斷它是否是 $createElement。
然后,我們遍歷到 return 語句處,它的節(jié)點類型是 ReturnStatement, ReturnStatement 的 argument 屬性就是 return 后面跟著的表達式。
這個表達式就是我們獲取 vue 模板語法樹的起點。
interface?ReturnStatement?extends?BaseNode?{
????type:?AST_NODE_TYPES.ReturnStatement;
????argument:?Expression?|?null;
}
轉換主體
入口表達式通常就是一個 $createElement 的函數(shù)調用表達式,但是也有可能是一個三元表達式。這是因為 v-if 可以出現(xiàn)在模板根節(jié)點。
$createElement 的函數(shù)簽名和 staticRenderFns 中的一樣。
//?vue/types/vue.d.ts
export?interface?CreateElement?{
??(tag?:?string?|?Component?|?AsyncComponent?|?(()?=>?Component),?children?:?VNodeChildren):?VNode;
??(tag?:?string?|?Component?|?AsyncComponent?|?(( )?=>?Component),?data?:?VNodeData,?children?:?VNodeChildren):?VNode;
}
我們應把 $createElement 的函數(shù)調用表達式解析成一個 vue 語法樹節(jié)點,tag 參數(shù)作為標簽名,從 data 參數(shù)中獲得屬性對象,然后對其 children 參數(shù)遞歸解析,作為子節(jié)點。
如果入口是一個三元表達式,三元表達式有如下定義:
interface?ConditionalExpression?extends?BaseNode?{
????type:?AST_NODE_TYPES.ConditionalExpression;
????test:?Expression;
????consequent:?Expression;
????alternate:?Expression;
}
test 解析為 v-if 的判斷條件,consequent 解析為 v-if 內的節(jié)點,alternate 解析為 v-else 內的節(jié)點。
我們一般最終會轉換成
這是兩個節(jié)點,為了保持解析方法的一致性和簡單性,統(tǒng)一只返回一個節(jié)點。因此創(chuàng)建一個 wrap 節(jié)點,將這兩個節(jié)點作為它的 children。
//?e1?為?v-if?解析后的節(jié)點,e2?為?v-else?解析后的節(jié)點
function?conditionElement(_e1:ASTNode,_e2:ASTNode){
????var?element:ASTElement??=?{
????????tag:'$$condition_wrap',
????????type:1,
????????attrsList:[],
????????attrsMap:{},
????????children:[_e1,_e2],
????????parent:undefined
????}
????return?element
}
因為 wrap 節(jié)點造成不必要的過多嵌套,我們會在后續(xù)的優(yōu)化環(huán)節(jié)把節(jié)點合并。
處理表達式
render 渲染函數(shù)中存在大量的表達式,例如指令屬性中、綁定屬性中、插值字符串。表達式種類繁多,處理表達式是轉換的重要一環(huán)。
處理表達式的整體思路就是把它轉換成一個字符串返回,例如二元表達式的處理:
function?expToString?(?_exp:TSESTree.Expression):string?{
????switch?(_exp.type)?{
????????case?AST_NODE_TYPES.BinaryExpression:??//?例如??a?===?b?
????????????if(_exp.operator?==?'=='?||?_exp.operator?==?'!='?||?_exp.operator?==?'!=='?||?_exp.operator?==?'==='){?//?==?就把左右互換
????????????????var?ret?=?`${this.expToString(_exp.right)}?${_exp.operator}?${this.expToString(_exp.left)}`
????????????????return?ret;
????????????}else{
????????????????var?ret?=?`${this.expToString(_exp.left)}?${_exp.operator}?${this.expToString(_exp.right)}`
????????????????return?ret;
????????????}
????????//?...
????}
}
把標識符和操作符正確地拼接在一起即可。
至少有十幾種表達式會出現(xiàn)在 render 渲染函數(shù)中,我們都需要處理。
除此之外,部分表達式還需要一些額外處理,我們看如下渲染函數(shù)片段:
i("transition",?{
????on:?{???//?事件綁定
????????click:?function(e)?{
????????????t.onClick();
????????}
????}
})
它的 vue 模板應該是這樣的:
<transition?@click="onClick()">?transition>
模板中用的屬性和方法都掛載在 this,也就是這里的 t 下。渲染函數(shù)需要用 t. 來調用,但是模板中不需要,所以我們需要把它去掉。
但是我們碰到 t. 就去掉也不行,例如下面的情況:
i("transition",?{
????on:?{???//?事件綁定
????????click:?function(t)?{
????????????t.onClick();
????????}
????}
})
參數(shù)里有 t,函數(shù)里的 t 顯然不再是 this,它已經被參數(shù)中的 t 覆蓋了,這時我們就需要保留 t。
除此之外,我們還會遇到這種情況:
i("transition",?{
????on:?{???//?事件綁定
????????mousedown:?function(i)?{
????????????i.stopPropagation(),
????????????t.globalMouseDown(
????????????????arguments[0],
????????????????"r",
????????????????e
????????????);
????????}
????}
})
它的 vue 模板應該是這樣的:
<transition?@mousedown="$event.stopPropagation(),globalMouseDown($event,'r',e)">?transition>
或者
<transition?@mousedown.stop="globalMouseDown($event,'r',e)">?transition>
$event 是 vue 模板的特有參數(shù),事件函數(shù)的第一個參數(shù)都可以寫作 $event,我們同樣需要在處理表達式時處理此種情況。
我們需要根據函數(shù)參數(shù)處理函數(shù)內部的表達式,但是顯然這跨越了幾個節(jié)點層次,我們需要知道前幾層節(jié)點的情況,我們可以引入上下文解決此問題。
上下文
函數(shù)有調用棧,我們同樣用棧式結構生成上下文,為了保證不同節(jié)點間的上下文不會因為賦值互相干擾,我們引入 immutable, 使用不可變對象生成上下文。
類型定義如下:
import?{?List?}?from?"immutable"
type?Context?=?{
????[key:string]:string
}
type?ContextStack?=?List
處理 $event 示例
expToString?(?_exp:TSESTree.Expression,_ctx:ContextStack):string?{
????switch?(_exp.type)?{
????????case?AST_NODE_TYPES.FunctionExpression:?//?節(jié)點類型為函數(shù)表達式節(jié)點
????????????var?params?=?_exp.params.map(node=>{return?this.parameterToString(node,_ctx)})?//?獲取所有參數(shù)
????????????if(params.length?>?0){
????????????????var?eventId?=?params[0];
????????????????var?nextCtx1?=?_ctx.push({type:'eventId',value:eventId})?//?生成新的上下文
????????????????var?bodyStr?=?this.statementToString(_exp.body,nextCtx1);
????????????????return?bodyStr
????????????}
????}
}
處理內置函數(shù)
前面我們列出了一系列 _ 開頭的內置函數(shù),它們會影響節(jié)點的生成,我們都需要處理。
_l:生成 v-for 結構
一個 t._l 調用的基本形式如下:
t._l(t.list,?function(e,?s)?{
????return?i("Item",?{
??????key:?s?+?"_"?+?e.id,
??????attrs:?{
????????data:?e,
????????flag:?t.playing
??????}
????});
})
轉換后應為
<Item?v-for="(e,s)?in?list"?:key="s?+?'_'?+?e.id"?:data="e"?:flag="playing">Item>
我們需要從 _l 函數(shù)調用表達式的第一參數(shù)中獲取到循環(huán)用的列表標識符,從第二個參數(shù)的函數(shù)表達式中獲取到參數(shù)列表,從 return 語句中獲取到循環(huán)用的元素節(jié)點。
_e:生成空節(jié)點
空節(jié)點都是可以去掉的,為了保持解析方法的一致性,返回一個標識為 $$null 的節(jié)點。
function?nonNode?()?{
????var?element:ASTElement??=?{
????????tag:'$$null',
????????type:1,
????????attrsList:[],
????????attrsMap:{},
????????children:[],
????????parent:undefined
????}
????return?element
}
_s:生成插值字符串 & _v:生成文本節(jié)點
_s 可能出現(xiàn)在 _v 內部,因此一起處理。
//?t._v(t._s(t.title.length)?+?"/15")?=>?_s(t.title.length)?+?"/15"??=>??{{title.length?+?"/15"}}
//?t._v("保存")?=>?"保存"?=>?保存
function?textNode?(text:string)?{
????var?re?=?/_s\((.*?)\)/g;??//?匹配?_s()?將?_s()?去掉,整體用?{{}}?包裹
????if(re.test(text)){??//?處理?_s?,_s只會在?_v內部
????????text?=`{{${text.replace(re,(_a:string,b:any)=>{
????????????return?b
????????})}}}`?
????}else{?//?去掉靜態(tài)文本兩側的雙引號
????????if(text.startsWith('"')?&&?text.endsWith('"')){
????????????text?=?text.slice(1,-1)
????????}
????}
????var?element:ASTElement??=?{??//?簡化類型,用?$$text?標識文本節(jié)點
????????tag:'$$text',
????????type:1,
????????attrsList:[],
????????attrsMap:{text:text},
????????children:[],
????????parent:undefined
????}
????return?element
}
_m:生成靜態(tài) html 片段(staticRenderFns 中的 html 片段)
m 一般以類似 t._m(0) 的形式出現(xiàn),只有一個參數(shù),參數(shù)為索引。我們之前解析的 staticRenderFns 數(shù)組中的索引,最終替換成之前生成好的 html片段即可。因此返回一個標識為 $$static_ 加索引的節(jié)點。
function?staticNode?(_exp:TSESTree.Expression)?{
????if(_exp.type?==?AST_NODE_TYPES.Literal){
????????var?index?=?_exp.raw;
????????var?tag?=?`$$static__${index}`
????????var?element:ASTElement??=?{
????????????tag:tag,
????????????type:1,
????????????attrsList:[],
????????????attrsMap:{},
????????????children:[],
????????????parent:undefined
????????}
????????return?element
????}else{
????????throw?new?Error("解析?static?node?錯誤")
????}
}
處理屬性對象
屬性都是鍵值對的形式,值主要就是表達式,我們之前已經處理過了。鍵的處理主要如下:
鍵為 on 時,按綁定事件的格式處理。 鍵為 model 時,按 v-model 處理。 鍵為 directives 時,按指令格式處理。 鍵為 attrs 時,值是靜態(tài)屬性集合,需要拆開。 鍵為 staticClass、staticStyle 時,是靜態(tài)類名和樣式。 除此之外,如果值是個雙引號包裹的字符串,則是靜態(tài)屬性,否則為綁定屬性,屬性名前加冒號。
部分不常用的屬性對象未列出,可以查閱 vue/codegen[7]
優(yōu)化
經過以上處理,我們已經得到了 vue 模板語法樹,但是它還有冗余。有 _e 生成的空節(jié)點,還可能有 wrap 節(jié)點多層嵌套。
生成出來的模板可能是這樣的,因為 wrap 節(jié)點都會使用 template 標簽:
<template>
????<template>
????????<template>
????????template>
????template>
template>
我們可以遍歷 vue 模板語法樹,刪掉空節(jié)點,把多層 template 節(jié)點合并。
每種類型的優(yōu)化可以單獨寫成一個方法,例如:
//?刪除空節(jié)點
function?optimizeNode1(_root:?ASTElement):?ASTElement?{
????_root.children?=?_root.children.filter(child=>{
????????if(child.type?==?1?&&?child.tag?==?'$$null'?&&?!child.attrsMap['v-if']){
????????????????return?false
????????}else{
????????????return?true;
????????}
????}).map(child=>{
????????if(child.type?==?1){
????????????optimizeNode1(child)
????????}
????????return?child;
????})
????return?_root;
}
然后各個優(yōu)化方法依次調用即可。
每個優(yōu)化環(huán)節(jié)都重新遍歷一遍節(jié)點并非一種高效的做法,如果優(yōu)化方法能夠支持流式處理,流水線模式能夠大幅提高效率。
生成器
將 vue 模板語法樹轉換成字符串的過程并不復雜,需要注意點有:
將 $$static__ 節(jié)點替換成 staticRenderFns 中的 html 片段 區(qū)分自閉合標簽 v-else 屬性不需要值
最后可以用 js-beautify 庫進行格式化。
實例
本文的完整代碼在這里[8]
并且支持在線轉換[9]
可以從含有 vue 模板的編譯后代碼中,例如,element-ui 官網下的 js[10] 中,用 $createElement 搜索渲染函數(shù),然后按照以下格式輸入到輸入框,執(zhí)行在線轉換。
{
????render:function?()?{
????????var?t?=?this.$createElement
????????//....
????},
????staticRenderFns:?[function?()?{
????????var?t?=?this.$createElement
????????//....
????}]
}
例如:
{
????render:function()?{
????????var?t?=?this
????????,?e?=?t.$createElement
????????,?i?=?t._self._c?||?e;
????????return?i("transition",?{
????????????attrs:?{
????????????????name:?"el-zoom-in-top"
????????????},
????????????on:?{
????????????????"after-leave":?function(e)?{
????????????????????t.$emit("dodestroy")
????????????????}
????????????}
????????},?[i("div",?{
????????????directives:?[{
????????????????name:?"show",
????????????????rawName:?"v-show",
????????????????value:?t.visible,
????????????????expression:?"visible"
????????????}],
????????????staticClass:?"el-time-panel?el-popper",
????????????class:?t.popperClass
????????},?[i("div",?{
????????????staticClass:?"el-time-panel__content",
????????????class:?{
????????????????"has-seconds":?t.showSeconds
????????????}
????????},?[i("time-spinner",?{
????????????ref:?"spinner",
????????????attrs:?{
????????????????"arrow-control":?t.useArrow,
????????????????"show-seconds":?t.showSeconds,
????????????????"am-pm-mode":?t.amPmMode,
????????????????date:?t.date
????????????},
????????????on:?{
????????????????change:?t.handleChange,
????????????????"select-range":?t.setSelectionRange
????????????}
????????})],?1),?i("div",?{
????????????staticClass:?"el-time-panel__footer"
????????},?[i("button",?{
????????????staticClass:?"el-time-panel__btn?cancel",
????????????attrs:?{
????????????????type:?"button"
????????????},
????????????on:?{
????????????????click:?t.handleCancel
????????????}
????????},?[t._v(t._s(t.t("el.datepicker.cancel")))]),?i("button",?{
????????????staticClass:?"el-time-panel__btn",
????????????class:?{
????????????????confirm:?!t.disabled
????????????},
????????????attrs:?{
????????????????type:?"button"
????????????},
????????????on:?{
????????????????click:?function(e)?{
????????????????????t.handleConfirm()
????????????????}
????????????}
????????},?[t._v(t._s(t.t("el.datepicker.confirm")))])])])])
????}
}
點擊轉換,輸出:
<template>
????<transition?name="el-zoom-in-top"?@after-leave="$emit('dodestroy')">
????????<div?v-show="visible"?class="el-time-panel?el-popper"?:class="popperClass">
????????????<div?class="el-time-panel__content"?:class="{?'has-seconds':showSeconds}">
????????????????<time-spinner?ref="spinner"?:arrow-control="useArrow"?:show-seconds="showSeconds"?:am-pm-mode="amPmMode"?:date="date"?@change="handleChange"?@select-range="setSelectionRange">
????????????????time-spinner>
????????????div>
????????????<div?class="el-time-panel__footer">
????????????????<button?class="el-time-panel__btn?cancel"?type="button"?@click="handleCancel">
????????????????????{{t("el.datepicker.cancel")}}
????????????????button>
????????????????<button?class="el-time-panel__btn"?:class="{?confirm:!disabled}"?type="button"?@click="handleConfirm()">
????????????????????{{t("el.datepicker.confirm")}}
????????????????button>
????????????div>
????????div>
????transition>
template>
和 element-ui 源碼對比,邏輯完全一致。
參考資料
estree: https://github.com/estree/estree
[2]ts-estree.ts: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/types/src/ts-estree.ts
[3]index.d.ts: https://github.com/vuejs/vue/blob/dev/packages/vue-template-compiler/types/index.d.ts
[4]vue/render-helpers: https://github.com/vuejs/vue/tree/dev/src/core/instance/render-helpers/index.js
[5]vue/codegen: https://github.com/vuejs/vue/tree/dev/src/compiler/codegen/index.js
[6]vue/codegen: https://github.com/vuejs/vue/tree/dev/src/compiler/codegen/index.js
[7]vue/codegen: https://github.com/vuejs/vue/tree/dev/src/compiler/codegen/index.js
[8]這里: https://github.com/mk33mk333/vue-template-transform
[9]在線轉換: https://mk33mk333.github.io/vue-template-transform/index.html
[10]js: https://element.eleme.io/element-ui.0216a22.js

