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