『壹』 vue3中的編譯器原理和優化策略
學習目標編譯器原理
vue3編譯過程剖析
vue3編譯優化策略
在初始化之前可能有編譯的過程,最終的產物是個渲染函數,我們知道渲染函數返回的值是一個虛擬DOM(vnode),那麼這個虛擬DOM在我們後續的更新過程中到底有什麼作用呢?我們今天就來探討一下。
編譯器原理1.概念廣義上的編譯原理:編譯器是將源代碼轉化成機器碼的軟體;所以編譯的過程則是將源代碼轉化成機器碼的過程,也就是cpu可執行的二進制代碼。例如使用高級語言java編寫的程序需要編譯成我們看不懂但計算機能看懂的的位元組碼。
如果了解過編譯器的工作流程的同學應該知道,一個完整的編譯器的工作流程會是這樣:
首先,parse解析原始代碼字元串,生成抽象語法樹AST。
其次,transform轉化抽象語法樹,讓它變成更貼近目標「DSL」的結構。
最後,codegen根據轉化後的抽象語法樹生成目標「DSL」的可執行代碼。
2.vue中的編譯在vue里也有編譯的過程,我們經常寫的那個HTML模版,在真正工作的時候,並不是那個HTML模版,它實際上是一個渲染函數,在這個過程中就發生了轉換,也就是編譯,也就是那個字元串的模版最終會變成一個JS函數,叫render函數。所以在這個過程中我們就需要引入編譯器的概念。在計算機中當一種東西從一種形態到另一種形態進行轉換的時候,就需要編譯。編譯器:用來將模板字元串編譯成為JavaScript渲染函數的代碼
那麼vue中的編譯發生在什麼時候呢?
這個時候我們就需要進一步了解vue包的不同版本的不同功能了。vue有攜帶編譯器和不攜帶編譯的包(對不同構建版本的解釋)。
3.運行時編譯在使用攜帶編譯器(compiler)的vue包的時候,vue編譯的時刻是發生在掛載($mount)的時候。
4.運行時不編譯如果使用未攜帶編譯器的vue包的時候,vue在運行時是不會進行編譯的。那麼它的編譯又發生在什麼時候呢?使用未攜帶編譯器的vue包的時候,需要進行預編譯,也就是基於構建工具使用,就是我們平時使用的vue-cli進行構建的項目,就是使用webpack調用vue-loader進行預編譯,將所有vue文件,就是SFC,將裡面的template模版部分轉換成render函數。這樣做的好處就是vue的包體積變小了,執行的時候速度更快了,因為不需要進行編譯了。
vue編譯器原理簡單來說就是:先將template模版轉換成ast抽象語法樹,ast再轉換成渲染函數render。
那麼什麼是是ast抽象語法樹呢?
1.ast抽象語法樹在template模版和render函數之間有一個中間產物叫做ast抽象語法樹。它就是個js對象,它能夠描述當前模版的結構信息,跟vnode很類似。注意,ast只是程序運行過程中編譯產生的,它跟我們最終程序的運行是沒有任何關系的。也就是當這個渲染函數生成之後,ast的生命周期就結束了,不再需要了,而那個虛擬DOM則伴隨整個程序的生命周期。這個就是ast和虛擬DOM的本質區別。
2.為什麼需要ast呢在ast轉換成render函數的過程中,需要進行特別的操作。第一次,將template轉成的ast是個非常粗糙的js對象,是一次非常粗糙的轉換,類似正則表達式的匹配,然後我們的template模版中還有很多表達式,指令,事件需要重新解析,經過這些具體的深加工的解析(transform)之後會得到一個終極ast,然後這個對這個終極ast進行generate,生成render函數
template=>ast=>transform=>ast=>render3.mini版vue編譯器下面我們來看一個mini版的vue編譯器,具體代碼已省略,具體代碼我已經放在Github上了:mini-vue-compiler
functiontokenizer(input){...}functionparse(template){consttokens=tokenizer(template)...}functiontransform(ast){...}functiontraverse(ast,context){...}functiongenerate(ast){...}functioncompile(template){//1.解析constast=parse(template)console.log(JSON.stringify(ast,null,2))//2.轉換transform(ast)//3.生成constcode=generate(ast)console.log(code)//returnfunctionrender(ctx){//returnh("h3",{},//ctx.title//)}returnnewFunction(code)()}lettmpl=`<h3>{{title}}</h3>`compile(tmpl)大概有以上操作,其中parse函數就是發生在把template轉換成ast的這過程,具體是通過一些正則表達式的匹配template中的字元串。比如將
xxx轉成ast對象,那麼就是通過正則表達式匹配如果是
那麼就設置一個開始標記,再往後面匹配到xxx內容,然後就設置一個子元素,最後匹配到那麼就設置一個結束標記,以此類推。parse解析之後得到的是一個粗糙的ast對象。經過parse解析得到一個粗糙的ast對象之後,就用transform進行深加工,最後要經過generate生成代碼。
Vue3編譯過程剖析掛載的時候先把template編譯成render函數,在創建實例之後,直接調用組件實例的render函數創建這個組件的真實DOM,然後繼續向下做遞歸。
1.vue2.x和vue3.x的編譯對比Vue2.x中的Compile過程會是這樣:
parse詞法分析,編譯模板生成原始粗糙的AST。
optimize優化原始AST,標記ASTElement為靜態根節點或靜態節點。
generate根據優化後的AST,生成可執行代碼,例如_c、_l之類的。
在Vue3中,整體的Compile過程仍然是三個階段,但是不同於Vue2.x的是,第二個階段換成了正常編譯器都會存在的階段transform。
parse詞法分析,編譯模板生成原始粗糙的AST。
transform遍歷AST,對每一個ASTelement進行轉化,例如文本元素、指令元素、動態元素等等的轉化
generate根據優化後的AST,生成可執行代碼函數。
2.源碼編譯入口我們先從一個入口來開始我們的源碼閱讀,packages/vue/index.ts。
//web平台特有編譯函數functioncompileToFunction(template:string|HTMLElement,options?:CompilerOptions):RenderFunction{//省略...if(template[0]==='#'){//獲取模版內容constel=document.querySelector(template)//省略...template=el?el.innerHTML:''}//編譯const{code}=compile(template,extend({//省略...},options))constrender=(__GLOBAL__?newFunction(code)():newFunction('Vue',code)(runtimeDom))asRenderFunction//省略...return(compileCache[key]=render)}//注冊編譯函數registerRuntimeCompiler(compileToFunction)export{compileToFunctionascompile}這個入口文件的代碼比較簡單,只有一個compileToFunction函數,但函數體內的內容卻又比較關鍵,主要是經歷以下步驟:
依賴注入編譯函數至(compileToFunction)
runtime調用編譯函數compileToFunction
調用compile函數
返回包含code的編譯結果
將code作為參數傳入Function的構造函數將生成的函數賦值給render變數
將render函數作為編譯結果返回
3.template獲取app.mount()獲取了templatepackages/runtime-dom/src/index.ts
compile將傳?template編譯為render函數,packages/runtime-core/src/component.ts
實際執?的是baseCompile,packages/compiler-core/src/compile.ts
第?步解析-parse:解析字元串template為抽象語法樹ast
第?步轉換-transform:解析屬性、樣式、指令等
第三步?成-generate:將ast轉換為渲染函數
Vue3編譯器優化策略這是一個非常典型的用內存換時間的操作
1.靜態節點提升<div><div>{{msg}}</div><p>coboy</p><p>coboy</p><p>coboy</p></div>以上這個段template如果沒有開啟靜態節點提升它編譯後是這樣的:
import{toDisplayStringas_toDisplayString,createVNodeas_createVNode,openBlockas_openBlock,createBlockas_createBlock}from"vue"exportfunctionrender(_ctx,_cache,$props,$setup,$data,$options){return(_openBlock(),_createBlock("div",null,[_createVNode("div",null,_toDisplayString(_ctx.msg),1/*TEXT*/),_createVNode("p",null,"coboy"),_createVNode("p",null,"coboy"),_createVNode("p",null,"coboy")]))}如果開啟了靜態節點提升之後它編譯後則是這樣的:
import{toDisplayStringas_toDisplayString,createVNodeas_createVNode,openBlockas_openBlock,createBlockas_createBlock}from"vue"const_hoisted_1=/*#__PURE__*/_createVNode("p",null,"coboy",-1/*HOISTED*/)const_hoisted_2=/*#__PURE__*/_createVNode("p",null,"coboy",-1/*HOISTED*/)const_hoisted_3=/*#__PURE__*/_createVNode("p",null,"coboy",-1/*HOISTED*/)exportfunctionrender(_ctx,_cache,$props,$setup,$data,$options){return(_openBlock(),_createBlock("div",null,[_createVNode("div",null,_toDisplayString(_ctx.msg),1/*TEXT*/),_hoisted_1,_hoisted_2,_hoisted_3]))}我們可以看到template里存在大量的不會變的p標簽,所以當這個組件重新渲染的時候,這些靜態的不會變的標簽就不應該再次創建了。所以vue3就把這些靜態的不會變的標簽的VNode放在了render函數作用域的外面,在下次render函數再次執行的時候,那些靜態標簽的VNode已經在內存里了,不需要重新創建了。相當於佔用當前機器的內存,避免重復創建VNode,用內存來換時間。大家仔細斟酌一番靜態提升的字眼,靜態二字我們可以不看,但是提升二字,直抒本意地表達出它(靜態節點)被提高了。
2.補丁標記和動態屬性記錄<div><div:title="title">coboy</div></div>意思就是在編譯的過程中,像人眼一樣對模版進行掃描看哪些東西是動態的,然後提前把這些動態的東西提前保存起來,作個標記和記錄,等下次更新的時候,只更新這些保存起來的動態的記錄。比如上面模版的title是動態的,提前做個標記和記錄,更新的時候就只更新title部分的內容。
import{createVNodeas_createVNode,openBlockas_openBlock,createBlockas_createBlock}from"vue"exportfunctionrender(_ctx,_cache,$props,$setup,$data,$options){return(_openBlock(),_createBlock("div",null,[_createVNode("div",{title:_ctx.title},"coboy",8/*PROPS*/,["title"])]))}<div><div:title="title">{{text}}</div></div>import{toDisplayStringas_toDisplayString,createVNodeas_createVNode,openBlockas_openBlock,createBlockas_createBlock}from"vue"exportfunctionrender(_ctx,_cache,$props,$setup,$data,$options){return(_openBlock(),_createBlock("div",null,[_createVNode("div",{title:_ctx.title},_toDisplayString(_ctx.text),9/*TEXT,PROPS*/,["title"])]))}我們可以觀察到在_createVNode函數的第四個參數是個9,後面是一個注釋:/TEXT,PROPS/,這個是表示在當前的節點裡面有兩個東西是動態的,一個是內部的文本,一個是屬性,然後具體是哪個屬性,在第五個參數的數組裡面則記錄了下來["title"],有個title的屬性是動態的。
在將來進行patch更新的時候,就可以根據當前記錄的信息,進行更新,縮減更新過程和操作,可以非常精確地只進行title和文本的更新。
如果div標簽里是靜態文本的話,_createVNode函數的第四個參數則變成了8,後面的注釋變成了:/PROPS/,後面的第五個參數數據不變。
_createVNode函數的第四個參數的數字其實是一個二進制數字轉成十進制的數字。
8的二進制是1000,9的二進制是1001,很容易可以看出二進制的每一位的數字都代表著特殊的含義。這些數字就是patchFlag,那麼什麼是patchFlag呢?
什麼是patchFlagpatchFlag是complier時的transform階段解析ASTElement打上的補丁標記。它會為runtime時的patchVNode提供依據,從而實現靶向更新VNode和靜態提升的效果。
patchFlag被定義為一個數字枚舉類型,它的每一個枚舉值對應的標識意義是:
TEXT=1動態文本的元素
CLASS=2動態綁定class的元素
STYLE=4動態綁定style的元素
PROPS=8動態props的元素,且不含有class、style綁定
FULL_PROPS=16動態props和帶有key值綁定的元素
HYDRATE_EVENTS=32事件監聽的元素
STABLE_FRAGMENT=64子元素的訂閱不會改變的Fragment元素
KEYED_FRAGMENT=128自己或子元素帶有key值綁定的Fragment元素
UNKEYED_FRAGMENT=256沒有key值綁定的Fragment元素
NEED_PATCH=512帶有ref、指令的元素
DYNAMIC_SLOTS=1024動態slot的組件元素
HOISTED=-1靜態的元素
BAIL=-2不是render函數生成的一些元素,例如renderSlot
整體上patchFlag的分為兩大類:
當patchFlag的值大於0時,代表所對應的元素在patchVNode時或render時是可以被優化生成或更新的
當patchFlag的值小於0時,代表所對應的元素在patchVNode時,是需要被fulldiff,即進行遞歸遍歷VNodetree的比較更新過程。
以上就是vue3的一個非常高效的優化策略叫補丁標記和動態屬性記錄。
3.緩存事件處理程序functiontokenizer(input){...}functionparse(template){consttokens=tokenizer(template)...}functiontransform(ast){...}functiontraverse(ast,context){...}functiongenerate(ast){...}functioncompile(template){//1.解析constast=parse(template)console.log(JSON.stringify(ast,null,2))//2.轉換transform(ast)//3.生成constcode=generate(ast)console.log(code)//returnfunctionrender(ctx){//returnh("h3",{},//ctx.title//)}returnnewFunction(code)()}lettmpl=`<h3>{{title}}</h3>`compile(tmpl)0將來框架會像react那樣把@click="onClick"變成@click="()=>onClick()",最後可能是這樣的一個箭頭函數。那就意味著每次onClick的函數都是一個全新的函數,那就會造成這個回調函數明明沒有變,都會被認為變了,那就必須進行一系列的更新,那麼如果能把這個回調函數緩存起來,更新的時候,就不要再創建了。
未進行緩存事件處理程序之前的編譯
functiontokenizer(input){...}functionparse(template){consttokens=tokenizer(template)...}functiontransform(ast){...}functiontraverse(ast,context){...}functiongenerate(ast){...}functioncompile(template){//1.解析constast=parse(template)console.log(JSON.stringify(ast,null,2))//2.轉換transform(ast)//3.生成constcode=generate(ast)console.log(code)//returnfunctionrender(ctx){//returnh("h3",{},//ctx.title//)}returnnewFunction(code)()}lettmpl=`<h3>{{title}}</h3>`compile(tmpl)1進行緩存事件處理程序之後的編譯
functiontokenizer(input){...}functionparse(template){consttokens=tokenizer(template)...}functiontransform(ast){...}functiontraverse(ast,context){...}functiongenerate(ast){...}functioncompile(template){//1.解析constast=parse(template)console.log(JSON.stringify(ast,null,2))//2.轉換transform(ast)//3.生成constcode=generate(ast)console.log(code)//returnfunctionrender(ctx){//returnh("h3",{},//ctx.title//)}returnnewFunction(code)()}lettmpl=`<h3>{{title}}</h3>`compile(tmpl)24.塊block這是什麼意思呢?根據尤雨溪本人的解析,他說,根據他的統計那個動態的部分最多隻有三分之一,基本上都是靜態部分,所以在編譯的過程中,能不能發現那個比較小的動態部分,把它放到比較靠上
『貳』 原來 vue3 文件編譯是這樣工作的!看完後更懂vue3了
理解Vue3文件編譯過程的關鍵在於熟悉vue-loader和@vitejs/plugin-vue的工作機制。以@vitejs/plugin-vue為例,讓我們通過調試來揭示這個過程。首先,Vue源代碼在App.vue文件中編寫,如設置變數msg並在template中渲染。在瀏覽器中,這些文件需要經過編譯轉換為瀏覽器能理解的js。
編譯實際發生於node環境,而不是瀏覽器端。通過調試工具如VSCode的Javascript Debug Terminal,我們可以在vite.config.ts中使用@vitejs/plugin-vue的地方設置斷點。這里,關鍵的函數是vuePlugin,它包含了buildStart和transform鉤子,分別在伺服器啟動和模塊解析時被調用。
當啟動服務,會首先調用buildStart鉤子,此時compiler變數為null,隨後通過resolveCompiler函數定位到vue/compiler-sfc庫。在transform鉤子中,我們重點關注App.vue文件的解析,這時transformMain函數會執行,它處理了template、scriptSetup(如果有setup)和styles內容。
在transformMain中,createDescriptor函數被調用,傳入App.vue的文件路徑和代碼。這個函數內部,會使用vue/compiler-sfc的parse函數,該函數接收源代碼和選項,解析出包含模板、script內容以及可能的style內容的SFCDescriptor對象。
通過這些步驟,Vue文件被逐步分解成瀏覽器可識別的組件描述符,包括了HTML模板、腳本邏輯和樣式信息。這讓你更深入地理解了Vue3文件的編譯過程,使得源代碼和瀏覽器之間的轉換過程更加清晰。
『叄』 vue 如何實現 AOT 編譯
Vue 2 引入了 AOT 編譯功能,開發者可以在構建過程中使用 AOT 編譯,將模板轉化為 JavaScript 代碼。使用 AOT 編譯可以減少應用程序的初始化時間、減小文件大小,並幫助開發者在構建過程中發現問題。
AOT 預編譯是一種將應用程序的代碼在構建過程中編譯成可執行的機器代碼的技術。與即時編譯(JIT)相比,AOT編譯在應用程序運行之前就完成,因此在運行時不需要再進行編譯。
『肆』 vue3源碼學習--編譯階段匯總
Vue3編譯階段匯總:
核心包與工具:
編譯流程:
模式差異:
總結: