一文帶你吃透Vue3編譯原理
一直對編譯原理的東西都有一種恐懼感,感覺太難了,看不懂,打開vue3源碼看到編譯相關(guān)的代碼,直接嚇退。直到我學(xué)習(xí)了大崔哥的mini-vue,so ga ~
主要流程
現(xiàn)在我們就來一起分析一個簡易的vue3的編譯原理。一句話概括一下我們想要實現(xiàn)的功能,那就是將template模板生成我們想要的render函數(shù)即可。簡單的一句話卻蘊含著大量的知識。
<div>hi, {{message}}</div>
最后生成
import { toDisplayString as _toDisplayString, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock("div", null, "hi, " + _toDisplayString(_ctx.message), 1 /* TEXT */))
}首先template會通過詞法分析、語法分析解析成AST(抽象語法樹),然后利用transform對AST進行優(yōu)化,最后通過generate模塊生成我們想要的render函數(shù)。
在vue3的源碼中主要分成了3個部分(以下是簡化后的源碼)
export function baseCompile(template){
const ast = baseParse(template)
transform(ast)
return generate(ast)
}- 通過
parse將template生成ast - 通過
transform優(yōu)化ast - 通過
generate生成render函數(shù)
由于這3個部分牽扯的東西比較多,我們這篇文章主要來講解一下parse的實現(xiàn)(友情提示:為了讓大家剛好的理解,本文的代碼全部都是精簡過得哦)
parse的實現(xiàn)
我們就拿一個簡單的例子入手
<div><p>hi</p>{{message}}</div>
看似一個簡單的例子,其實3種類型:element、text、插值。我們將這三種類型用枚舉定義一下。
const enum NodeTypes {
ROOT,
INTERPOLATION,
SIMPLE_EXPRESSION,
ELEMENT,
TEXT
}ROOT類型表示根節(jié)點,SIMPLE_EXPRESSION類型表示插值的內(nèi)容。最后我們想要通過parse生成一個ast。
{
type: NodeTypes.ROOT
children: [
{
type: NodeTypes.ELEMENT,
tag: "div",
children: [
{
type: NodeTypes.ELEMENT,
tag: "p",
children: [
{
type: NodeTypes.TEXT,
content: "hi"
}
]
},
{
type: NodeTypes.INTERPOLATION,
content: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: "message"
}
}
]
}
]
}基于源碼我們可以知道ast是由函數(shù)baseParse生成。那我們就從這個函數(shù)入手。
baseParse
export function baseParse(content: string) {
const context = createParseContext(content)
return createRoot(parserChildren(context, []))
}
function createParseContext(content: string) {
return {
source: content
}
}
function createRoot(children) {
return {
children,
type: NodeTypes.ROOT
}
}首先創(chuàng)建一個全局的上下文對象context,并且存儲了source。source就是我們傳入的模板內(nèi)容。接著創(chuàng)建根節(jié)點,包含了type和children。而children是由parseChildren創(chuàng)建。
parseChildren
function parseChildren(context, ancestors) {
const nodes: any = []
while (!isEnd(context, ancestors)) {
const s = context.source
let node
if (s.startsWith("{{")) {
node = parseInterpolation(context)
} else if (s[0] === "<") {
if (/[a-z]/i.test(s[1])) {
node = parseElement(context, ancestors)
}
} else {
node = parseText(context)
}
nodes.push(node)
}
return nodes
}parseChildren是負責解析子節(jié)點并創(chuàng)建ast節(jié)點數(shù)組。parseChildren是自頂向下分析各個子節(jié)點的,對于模板內(nèi)容要從左到右依次解析。每當碰到一個element節(jié)點都要遞歸的調(diào)用parseChildren去解析它的子節(jié)點。當碰到{{則認為需要處理的是插值節(jié)點,當碰到<則認為需要處理的是element節(jié)點,其余的則統(tǒng)一認為處理的是text節(jié)點。每處理完一個節(jié)點都會生成node并push到nodes中,最后返回nodes當做是父ast節(jié)點的children屬性。
當然從左到右依次循環(huán)解析就一定要有一個退出循環(huán)的條件isEnd
function isEnd(context, ancestors) {
const s = context.source
if (s.startsWith("</")) {
for (let i = 0; i < ancestors.length; i++) {
const tag = ancestors[i]
if (startsWithEndTagOpen(s, tag)) {
return true
}
}
}
return !s
}
function startsWithEndTagOpen(source, tag) {
return (
source.startsWith("</") &&
source.slice(2, 2 + tag.length).toLowerCase() === tag.toLowerCase()
)
}ancestors表示element標簽的集合,大致的意思就是當碰到了結(jié)束標識符</,并且結(jié)束標簽(source.slice(2, 2 + tag.length))和element標簽的集合中的標簽匹配則說明當前的element節(jié)點處理完畢,則退出循環(huán)
下面我們就來看一下插值節(jié)點parseInterpolation、element節(jié)點parseElement和文本節(jié)點parseText分別是怎么處理的
parseInterpolation
function parseInterpolation(context) {
const openDelimiter = "{{"
const closeDelimiter = "}}"
const closeIndex = context.source.indexOf(
closeDelimiter,
openDelimiter.length
)
advanceBy(context, openDelimiter.length)
const rawContentLength = closeIndex - openDelimiter.length
const rawContent = parseTextData(context, rawContentLength)
const content = rawContent.trim()
advanceBy(context, closeDelimiter.length)
return {
type: NodeTypes.INTERPOLATION,
content: {
type: NodeTypes.SIMPLE_EXPRESSION,
content
}
}
}
function advanceBy(context: any, length: number) {
context.source = context.source.slice(length)
}
function parseTextData(context: any, length) {
const content = context.source.slice(0, length)
advanceBy(context, content.length)
return content
}我們主要是為了獲取插值的內(nèi)容然后返回一個插值對象即可。closeIndex表示“}}”所在的位置。advanceBy函數(shù)的功能是推進。比如"{{"是不需要處理的,那么就直接把它截取掉。rawContentLength代表“{{”和“}}”中間內(nèi)容的長度,通過parseTextData獲取“{{”和“}}”中間的內(nèi)容,并返回。然后把中間內(nèi)容的部分做推進。由于我們寫代碼習(xí)慣可能會給內(nèi)容的前后做留白,所以需要用trim做處理。然后把最后的“}}”推進,返回一個插值類型的對象即可。
parseElement
function parseElement(context, ancestors) {
const element: any = parseTag(context, TagType.Start)
ancestors.push(element)
element.children = parseChildren(context, ancestors)
ancestors.pop()
if (startsWithEndTagOpen(context.source, element.tag)) {
parseTag(context, TagType.End)
} else {
throw new Error(`缺少結(jié)束標簽: ${element.tag}`)
}
return element
}
function parseTag(context: any, type: TagType) {
const match: any = /^<\/?([a-z]*)/i.exec(context.source)
const tag = match[1]
advanceBy(context, match[0].length)
advanceBy(context, 1)
if (type === TagType.End) return
return {
type: NodeTypes.ELEMENT,
tag
}
}
function startsWithEndTagOpen(source, tag) {
return (
source.startsWith("</") &&
source.slice(2, 2 + tag.length).toLowerCase() === tag.toLowerCase()
)
}parseElement第二個參數(shù)ancestors是一個數(shù)組來收集標簽的(作用在上面的isEnd已經(jīng)提到了)。通過parseTag獲取標簽名,parseTag通過正則拿到標簽名然后返回一個標簽對象,處理過的內(nèi)容繼續(xù)做推進。如果是結(jié)束標簽則什么都不做。然后通過parseChildren遞歸的處理element的子節(jié)點。然后對結(jié)束標簽進行處理,startsWithEndTagOpen判斷是夠存在結(jié)束標簽,如果不存在則報錯。
parseText
function parseText(context: any): any {
let endIndex = context.source.length
let endToken = ["<", "{{"]
for (let i = 0; i < endToken.length; i++) {
const index = context.source.indexOf(endToken[i])
if (index !== -1 && endIndex > index) {
endIndex = index
}
}
const content = parseTextData(context, endIndex)
return {
type: NodeTypes.TEXT,
content
}
}endIndex表示內(nèi)容長度(此時內(nèi)容的長度是已經(jīng)推進過的字符到最后一個字符的長度)。比如
<div>hi,{{message}}</div>
能夠進入到parseText函數(shù)中說明開始標簽已經(jīng)處理過了,所以context.source應(yīng)該是
hi,{{message}}</div>
所以endIndex的長度應(yīng)該是上面代碼的長度。當碰到”<“或者”{{“的時候,則我們需要改變endIndex的值,比如上面的代碼,我們想要拿到的文本內(nèi)容應(yīng)該是hi,,所以當碰到”{{“時,改變endIndex然后通過parseTextData拿到文本內(nèi)容,返回一個文本對象。
總結(jié)
parse的作用就是將template生成ast對象。則需要對template從左到右依次處理,處理過了則進行推進,碰到element標簽還需要遞歸處理,并把添加到element.children上,最終返回一個ast抽象語法樹。
以上就是一文帶你吃透Vue3編譯原理的詳細內(nèi)容,更多關(guān)于Vue3編譯原理的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
vue-router history模式下的微信分享小結(jié)
本篇文章主要介紹了vue-router history模式下的微信分享小結(jié),小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-07-07
vue路由傳參方式的方式總結(jié)及獲取參數(shù)詳解
vue 路由傳參的使用場景一般都是應(yīng)用在父路由跳轉(zhuǎn)到子路由時,攜帶參數(shù)跳轉(zhuǎn),下面這篇文章主要給大家介紹了關(guān)于vue路由傳參方式的方式總結(jié)及獲取參數(shù)的相關(guān)資料,文中通過實例代碼介紹的非常詳細,需要的朋友可以參考下2022-07-07
vue el-table 動態(tài)添加行與刪除行的實現(xiàn)
這篇文章主要介紹了vue el-table 動態(tài)添加行與刪除行的實現(xiàn)方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-07-07
vue3+vite:src使用require動態(tài)導(dǎo)入圖片報錯的最新解決方法
這篇文章主要介紹了vue3+vite:src使用require動態(tài)導(dǎo)入圖片報錯和解決方法,本文給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2023-04-04
vue3+Typescript實現(xiàn)路由標簽頁和面包屑功能
在使用 Vue.js 開發(fā)后臺管理系統(tǒng)時,經(jīng)常會遇到需要使用路由標簽頁的場景,這篇文章主要介紹了vue3+Typescript實現(xiàn)路由標簽頁和面包屑,需要的朋友可以參考下2023-05-05
vue2.0中click點擊當前l(fā)i實現(xiàn)動態(tài)切換class
本篇文章主要介紹了vue2.0中click點擊當前l(fā)i實現(xiàn)動態(tài)切換class ,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-06-06

