做者:深山螞蟻html
Vue3的源代碼正在國慶假期就這麼忽然放出來了,假期還沒結束,陸陸續續看到努力的碼農就在各類分析了。vue
目前仍是 pre Alpha,樂觀估計還有 Alpha,Beta版本,最後纔是正式版。node
話很少說,看 Pre-Alpha。 瞧 compiler-corereact
熱門的 reactivity 被大佬翻來覆去再研究了,我就和大夥一塊兒來解讀一下 」冷門「 的 compiler 吧!😄😄😄😄git
若是你對 AST 還不太熟悉,或者對如何實現一個簡單的 AST解析器 還不太熟悉,能夠猛戳:手把手教你寫一個 AST 解析器github
vue3.0的模板解析和vue2.0差別比較大,可是不管怎樣變化,基本原理是一致的,咱們寫的各類 html 代碼,js使用的時候其實就是一個字符串,將非結構化的字符串數據,轉換成結構化的 AST,咱們都是使用強大的正則表達式和indexOf來判斷。
compiler-core 的一個核心做用就是將字符串轉換成 抽象對象語法樹AST。正則表達式
Let's do IT !微信
進入 compiler-core 目錄下,結構一目瞭然。這裏說下 _tests_ 目錄,是vue的jest測試用例。
閱讀源碼前先看看用例,對閱讀源碼有很大幫助哦。ide
以下,測試一個簡單的text,執行parse方法以後,獲得 ast,指望 ast 的第一個節點與定義的對象是一致的。
同理其餘的模塊測試用例,在閱讀源碼前能夠先瞄一眼,知道這個模塊如何使用,輸入輸出是啥。post
test('simple text', () => {
const ast = parse('some text')
const text = ast.children[0] as TextNode
expect(text).toStrictEqual({
type: NodeTypes.TEXT,
content: 'some text',
isEmpty: false,
loc: {
start: { offset: 0, line: 1, column: 1 },
end: { offset: 9, line: 1, column: 10 },
source: 'some text'
}
})
})
複製代碼
先看一張圖,重點是四塊:
其中起始標籤會用到遞歸來處理子節點。
接下來,咱們開始跟着源碼來閱讀吧~~~~~~
這個是對外暴露的核心方法,咱們先測試下結果:
const source = ` <div id="test" :class="cls"> <span>{{ name }}</span> <MyCom></MyCom> </div> `.trim()
import { parse } from './compiler-core.cjs'
const result = parse(source)
複製代碼
output:
一個簡單的轉換結果就呈現出來了,從生成的結構來看,相對於vue2.x有幾個比較重要的變化:
新版的 AST 明顯比 vue2.x 要複雜些,能夠看到vue3.0將不少能夠在編譯階段就能肯定的就在編譯階段肯定,標識編譯結果,不須要等到運行時再去判斷,節省內存和性能。這個也是尤大大重點說了的,優化編譯,提高性能。
接下來咱們來看下轉換的代碼,主要有以下幾個方法:
parse 的主入口,這裏建立了一個 parseContext,有利於後續直接從 context 上拿到 content,options 等。
getCursor 獲取當前處理的指針位置,用戶生成 loc,初始都是1。
export function parse(content: string, options: ParserOptions = {}): RootNode {
const context = createParserContext(content, options)
const start = getCursor(context)
return {
type: NodeTypes.ROOT,
children: parseChildren(context, TextModes.DATA, []),
helpers: [],
components: [],
directives: [],
hoists: [],
codegenNode: undefined,
loc: getSelection(context, start)
}
}
複製代碼
重點看下 parseChildren ,這是轉換的主入口方法。
function parseChildren(
context: ParserContext,
mode: TextModes,
ancestors: ElementNode[]
): TemplateChildNode[] {
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 (startsWith(s, context.options.delimiters[0])) {
// '{{'
node = parseInterpolation(context, mode)
} else if (mode === TextModes.DATA && s[0] === '<') {
// https://html.spec.whatwg.org/multipage/parsing.html#tag-open-state
if (s.length === 1) {
emitError(context, ErrorCodes.EOF_BEFORE_TAG_NAME, 1)
} else if (s[1] === '!') {
// <!DOCTYPE <![CDATA[ 等非節點元素 暫不討論
} else if (s[1] === '/') {
if (s.length === 2) {
} else if (s[2] === '>') {
advanceBy(context, 3)
continue
} else if (/[a-z]/i.test(s[2])) {
parseTag(context, TagType.End, parent)
continue
} else {
}
} else if (/[a-z]/i.test(s[1])) {
node = parseElement(context, ancestors)
} else if (s[1] === '?') {
} else {
}
}
if (!node) {
node = parseText(context, mode)
}
if (Array.isArray(node)) {
for (let i = 0; i < node.length; i++) {
pushNode(context, nodes, node[i])
}
} else {
pushNode(context, nodes, node)
}
}
return nodes
}
複製代碼
ancestors 用來存儲未匹配的起始節點,爲後進先出的stack。
循環處理 source,循環截止條件是 isEnd 方法返回true,便是處理完成了,結束有兩個條件:
匹配還沒有結束,則進入循環匹配。有三種狀況:
若是是第三種動態文本插入,則執行 parseInterpolation 組裝文本節點,其中 isStatic=false 標識是變量,比較簡答,方法就不貼了。
return {
type: NodeTypes.INTERPOLATION,
content: {
type: NodeTypes.SIMPLE_EXPRESSION,
isStatic: false,
content,
loc: getSelection(context, innerStart, innerEnd)
},
loc: getSelection(context, start)
}
複製代碼
再看下這兩個處理 source 內容後移的方法:
advanceBy(context,number) : 將須要處理的模板source ,後移 number 個字符,從新記錄 loc
advanceSpaces() : 後移存在的連續的空格
回到上面的匹配條件,若是是 < 開頭,分兩種狀況:
若是是截止標籤:parseTag,則直接處理完成。
若是是起始標籤:parseElement 執行,調用parseTag 處理標籤,而後再去遞歸處理子節點等。
正則:/^</?([a-z][^\t\r\n\f />]*)/i 這個就很少說了,匹配 <\div> </div>這種標籤。
測試 match :
/^<\/?([a-z][^\t\r\n\f />]*)/i.exec("<div class='abc'>")
(2) ["<div", "div", index: 0, input: "<div class='abc'>", groups: undefined]
複製代碼
顯然,mathch[1] 即匹配到的標籤元素。咱們看主方法:
function parseTag( context: ParserContext, type: TagType, parent: ElementNode | undefined ): ElementNode {
// Tag open.
const start = getCursor(context)
const match = /^<\/?([a-z][^\t\r\n\f />]*)/i.exec(context.source)!
const tag = match[1]
const props = []
const ns = context.options.getNamespace(tag, parent)
let tagType = ElementTypes.ELEMENT
if (tag === 'slot') tagType = ElementTypes.SLOT
else if (tag === 'template') tagType = ElementTypes.TEMPLATE
else if (/[A-Z-]/.test(tag)) tagType = ElementTypes.COMPONENT
advanceBy(context, match[0].length)
advanceSpaces(context)
// Attributes.
const attributeNames = new Set<string>()
while (
context.source.length > 0 &&
!startsWith(context.source, '>') &&
!startsWith(context.source, '/>')
) {
const attr = parseAttribute(context, attributeNames)
if (type === TagType.Start) {
props.push(attr)
}
advanceSpaces(context)
}
// Tag close.
let isSelfClosing = false
if (context.source.length === 0) {
} else {
isSelfClosing = startsWith(context.source, '/>')
advanceBy(context, isSelfClosing ? 2 : 1)
}
return {
type: NodeTypes.ELEMENT,
ns,
tag,
tagType,
props,
isSelfClosing,
children: [],
loc: getSelection(context, start),
codegenNode: undefined // to be created during transform phase
}
}
複製代碼
tagType有四種類型,在這裏定義了,分別是: 0 element,1 component,2 slot,3 template
咱們看while 循環,advanceBy 去掉起始 < 和標籤名以後:
若是跟着是 > 或者 /> ,那麼標籤處理結束,退出循環。
不然是標籤的元素,咱們執行 parseAttribute 來處理標籤屬性,該節點上增長props,保存 該起始節點的 attributes;
執行方法後面的!,是ts語法,至關於告訴ts,這裏必定會有值,無需作空判斷,如 const match = /^</?([a-z][^\t\r\n\f />]*)/i.exec(context.source)!
正則獲取屬性上的name
/^[^\t\r\n\f />][^\t\r\n\f />=]*/.exec('class='abc'>')
["class", index: 0, input: "class='abc'>", groups: undefined]
複製代碼
若是不是一個孤立的屬性,有value值的話(/[1]*=/.test(context.source)),那麼再獲取屬性的value。
function parseAttribute( context: ParserContext, nameSet: Set<string> ): AttributeNode | DirectiveNode {
// Name.
const start = getCursor(context)
const match = /^[^\t\r\n\f />][^\t\r\n\f />=]*/.exec(context.source)!
const name = match[0]
nameSet.add(name)
advanceBy(context, name.length)
// Value
let value:
| {
content: string
isQuoted: boolean
loc: SourceLocation
}
| undefined = undefined
if (/^[\t\r\n\f ]*=/.test(context.source)) {
advanceSpaces(context)
advanceBy(context, 1)
advanceSpaces(context)
value = parseAttributeValue(context)
}
const loc = getSelection(context, start)
if (/^(v-|:|@|#)/.test(name)) {
const match = /(?:^v-([a-z0-9-]+))?(?:(?::|^@|^#)([^\.]+))?(.+)?$/i.exec(
name
)!
let arg: ExpressionNode | undefined
if (match[2]) {
const startOffset = name.split(match[2], 2)!.shift()!.length
const loc = getSelection(
context,
getNewPosition(context, start, startOffset),
getNewPosition(context, start, startOffset + match[2].length)
)
let content = match[2]
let isStatic = true
if (content.startsWith('[')) {
isStatic = false
content = content.substr(1, content.length - 2)
}
arg = {
type: NodeTypes.SIMPLE_EXPRESSION,
content,
isStatic,
loc
}
}
if (value && value.isQuoted) {
const valueLoc = value.loc
valueLoc.start.offset++
valueLoc.start.column++
valueLoc.end = advancePositionWithClone(valueLoc.start, value.content)
valueLoc.source = valueLoc.source.slice(1, -1)
}
return {
type: NodeTypes.DIRECTIVE,
name:
match[1] ||
(startsWith(name, ':')
? 'bind'
: startsWith(name, '@')
? 'on'
: 'slot'),
exp: value && {
type: NodeTypes.SIMPLE_EXPRESSION,
content: value.content,
isStatic: false,
loc: value.loc
},
arg,
modifiers: match[3] ? match[3].substr(1).split('.') : [],
loc
}
}
return {
type: NodeTypes.ATTRIBUTE,
name,
value: value && {
type: NodeTypes.TEXT,
content: value.content,
isEmpty: value.content.trim().length === 0,
loc: value.loc
},
loc
}
}
複製代碼
parseAttributeValue 獲取屬性值的方法比較容易:
其中有處理vue的語法特性,若是屬性名稱是v-,:,@,#開頭的,須要特殊處理,看下這個正則:
/(?:^v-([a-z0-9-]+))?(?:(?::|^@|^#)([^\.]+))?(.+)?$/i.exec("v-name")
(4) ["v-name", "name", undefined, undefined, index: 0, input: "v-name", groups: undefined]
/(?:^v-([a-z0-9-]+))?(?:(?::|^@|^#)([^\.]+))?(.+)?$/i.exec(":name")
(4) [":name", undefined, "name", undefined, index: 0, input: ":name", groups: undefined]
複製代碼
mathch[2]若是有值,即匹配到了,說明是非 v-name,若是是名稱是[]包裹的則是 動態指令, 將 isStatic 置爲 false
parseElement 處理起始標籤,咱們先執行 parseTag 解析標籤,獲取到起始節點的 標籤元素和屬性,若是當前也是截止標籤(好比
),則直接返回該標籤。
不然,將起始標籤 push 到未匹配的起始 ancestors棧裏面。
而後繼續去處理子元素 parseChildren ,注意,將未匹配的 ancestors 傳進去了,parseChildren 的截止條件有兩個:
所以,若是是循環碰到匹配的截止標籤了,則須要 ancestors.pop(),將節點添加到當前的子節點。
固然,處理當前起始節點,該節點也多是截止節點,好比:<\img src="xxx"/>,則繼續去執行處理截止節點便可。
方法以下:
function parseElement( context: ParserContext, ancestors: ElementNode[] ): ElementNode | undefined {
// Start tag.
const parent = last(ancestors)
const element = parseTag(context, TagType.Start, parent)
if (element.isSelfClosing || context.options.isVoidTag(element.tag)) {
return element
}
// Children.
ancestors.push(element)
const mode = (context.options.getTextMode(
element.tag,
element.ns
) as unknown) as TextModes
const children = parseChildren(context, mode, ancestors)
ancestors.pop()
element.children = children
// End tag.
if (startsWithEndTagOpen(context.source, element.tag)) {
parseTag(context, TagType.End, parent)
} else {
}
element.loc = getSelection(context, element.loc.start)
return element
}
複製代碼
至此,vue3.0的 將 模板文件轉換成 AST 的主流程已經基本完成。
靜待下篇,AST 的 transform 處理。
若是你以爲這篇內容對你有價值,請點贊,並關注咱們的官網和咱們的微信公衆號(WecTeam),每週都有優質文章推送:
\t\r\n\f ↩︎