熟悉 Vue 的同窗都知道,從 Vue2 開始,在實際運行的時候,是將用戶所寫的 template 轉換爲 render 函數,獲得 vnode 數據(虛擬 DOM),而後再繼續執行,最終通過 patch 到真實的 DOM,而當有數據更新的時候,也是靠這個進行 vnode 數據的 diff,最終決定更新哪些真實的 DOM。html
這個也是 Vue 的一大核心優點,尤大不止一次的講過,由於用戶本身寫的是靜態的模板,因此 Vue 就能夠根據這個模板信息作不少標記,進而就能夠作針對性的性能優化,這個在 Vue 3 中作了進一步的優化處理,block 相關設計。前端
因此,咱們就來看一看,在 Vue 中,template 到 render 函數,到底經歷了怎麼樣的過程,這裏邊有哪些是值得咱們借鑑和學習的。vue
template 到 render,在 Vue 中實際上是對應的 compile 編譯的部分,也就是術語編譯器 cn.vuejs.org/v2/guide/in… 本質上來說,這個也是不少框架所採用的的方案 AOT,就是將本來須要在運行時作的事情,放在編譯時作好,以提高在運行時的性能。node
關於 Vue 自己模板的語法這裏就不詳細介紹了,感興趣的同窗能夠看 cn.vuejs.org/v2/guide/sy… ,大概就是形以下面的這些語法(插值和指令):webpack
render 函數呢,這部分在 Vue 中也有着詳細的介紹,你們能夠參閱 cn.vuejs.org/v2/guide/re… ,簡單來說,大概就是這個樣子:git
那咱們的核心目標就是這樣:github
若是你想體驗,能夠這裏 template-explorer.vuejs.orgweb
固然 Vue 3 的其實也是能夠的 https://vue-next-template-explorer ,雖然這裏咱們接下來要分析的是 Vue 2 版本的。正則表達式
要想了解是如何作到的,咱們就要從源碼入手,編譯器相關的都在 github.com/vuejs/vue/t… 目錄下,咱們這裏從入口文件 index.js 開始:express
import { parse } from './parser/index'
import { optimize } from './optimizer'
import { generate } from './codegen/index'
import { createCompilerCreator } from './create-compiler'
// `createCompilerCreator` allows creating compilers that use alternative
// parser/optimizer/codegen, e.g the SSR optimizing compiler.
// Here we just export a default compiler using the default parts.
export const createCompiler = createCompilerCreator(function baseCompile ( template: string, options: CompilerOptions ): CompiledResult {
// 重點!
// 第1步 parse 模板 獲得 ast
const ast = parse(template.trim(), options)
// 優化 能夠先忽略
if (options.optimize !== false) {
optimize(ast, options)
}
// 第2步 根據 ast 生成代碼
const code = generate(ast, options)
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
})
複製代碼
其實,你會發現,這是一個經典的編譯器(Parsing、Transformation、Code Generation)實現的步驟(這裏實際上是簡化):
接下來咱們就分別來看下對應的實現。
parse 的實如今 github.com/vuejs/vue/b… 這裏,因爲代碼比較長,咱們一部分一部分的看,先來看暴露出來的 parse 函數:
export function parse ( template: string, options: CompilerOptions ): ASTElement | void {
// options 處理 這裏已經忽略了
// 重要的棧 stack
const stack = []
const preserveWhitespace = options.preserveWhitespace !== false
const whitespaceOption = options.whitespace
// 根節點,只有一個,由於咱們知道 Vue 2 的 template 中只能有一個根元素
// ast 是樹狀的結構,root 也就是這個樹的根節點
let root
let currentParent
let inVPre = false
let inPre = false
let warned = false
// parseHTML 處理
parseHTML(template, {
warn,
expectHTML: options.expectHTML,
isUnaryTag: options.isUnaryTag,
canBeLeftOpenTag: options.canBeLeftOpenTag,
shouldDecodeNewlines: options.shouldDecodeNewlines,
shouldDecodeNewlinesForHref: options.shouldDecodeNewlinesForHref,
shouldKeepComment: options.comments,
outputSourceRange: options.outputSourceRange,
// 注意後邊的這些 options 函數 start end chars comment
// 約等因而 parseHTML 所暴露出來的鉤子,以便於外界處理
// 因此純粹的,parseHTML 只是負責 parse,可是並不會生成 ast 相關邏輯
// 這裏的 ast 生成就是靠這裏的鉤子函數配合
// 直觀理解也比較容易:
// start 就是每遇到一個開始標籤的時候 調用
// end 就是結束標籤的時候 調用
// 這裏重點關注 start 和 end 中的邏輯就能夠,重點!!
// chars comment 相對應的純文本和註釋
start (tag, attrs, unary, start, end) {
// check namespace.
// inherit parent ns if there is one
const ns = (currentParent && currentParent.ns) || platformGetTagNamespace(tag)
// handle IE svg bug
/* istanbul ignore if */
if (isIE && ns === 'svg') {
attrs = guardIESVGBug(attrs)
}
// 建立一個 ASTElement,根據標籤 屬性
let element: ASTElement = createASTElement(tag, attrs, currentParent)
if (ns) {
element.ns = ns
}
if (process.env.NODE_ENV !== 'production') {
if (options.outputSourceRange) {
element.start = start
element.end = end
element.rawAttrsMap = element.attrsList.reduce((cumulated, attr) => {
cumulated[attr.name] = attr
return cumulated
}, {})
}
attrs.forEach(attr => {
if (invalidAttributeRE.test(attr.name)) {
warn(
`Invalid dynamic argument expression: attribute names cannot contain ` +
`spaces, quotes, <, >, / or =.`,
{
start: attr.start + attr.name.indexOf(`[`),
end: attr.start + attr.name.length
}
)
}
})
}
if (isForbiddenTag(element) && !isServerRendering()) {
element.forbidden = true
process.env.NODE_ENV !== 'production' && warn(
'Templates should only be responsible for mapping the state to the ' +
'UI. Avoid placing tags with side-effects in your templates, such as ' +
`<${tag}>` + ', as they will not be parsed.',
{ start: element.start }
)
}
// 一些前置轉換 能夠忽略
for (let i = 0; i < preTransforms.length; i++) {
element = preTransforms[i](element, options) || element
}
if (!inVPre) {
processPre(element)
if (element.pre) {
inVPre = true
}
}
if (platformIsPreTag(element.tag)) {
inPre = true
}
if (inVPre) {
processRawAttrs(element)
} else if (!element.processed) {
// structural directives
// 處理 vue 指令 等
processFor(element)
processIf(element)
processOnce(element)
}
if (!root) {
// 若是尚未 root 即當前元素就是根元素
root = element
if (process.env.NODE_ENV !== 'production') {
checkRootConstraints(root)
}
}
if (!unary) {
// 設置當前 parent 元素,處理 children 的時候須要
currentParent = element
// 由於咱們知道 html 的結構是 <div><p></p></div> 這樣的,因此會先 start 處理
// 而後繼續 start 處理 而後 纔是兩次 end 處理
// 是一個經典的棧的處理,先進後出的方式
// 其實任意的編譯器都是離不開棧的,處理方式也是相似
stack.push(element)
} else {
closeElement(element)
}
},
end (tag, start, end) {
// 當前處理的元素
const element = stack[stack.length - 1]
// 彈出最後一個
// pop stack
stack.length -= 1
// 最新的尾部 就是接下來要處理的元素的 parent
currentParent = stack[stack.length - 1]
if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {
element.end = end
}
closeElement(element)
},
chars (text: string, start: number, end: number) {
if (!currentParent) {
if (process.env.NODE_ENV !== 'production') {
if (text === template) {
warnOnce(
'Component template requires a root element, rather than just text.',
{ start }
)
} else if ((text = text.trim())) {
warnOnce(
`text "${text}" outside root element will be ignored.`,
{ start }
)
}
}
return
}
// IE textarea placeholder bug
/* istanbul ignore if */
if (isIE &&
currentParent.tag === 'textarea' &&
currentParent.attrsMap.placeholder === text
) {
return
}
const children = currentParent.children
if (inPre || text.trim()) {
text = isTextTag(currentParent) ? text : decodeHTMLCached(text)
} else if (!children.length) {
// remove the whitespace-only node right after an opening tag
text = ''
} else if (whitespaceOption) {
if (whitespaceOption === 'condense') {
// in condense mode, remove the whitespace node if it contains
// line break, otherwise condense to a single space
text = lineBreakRE.test(text) ? '' : ' '
} else {
text = ' '
}
} else {
text = preserveWhitespace ? ' ' : ''
}
if (text) {
if (!inPre && whitespaceOption === 'condense') {
// condense consecutive whitespaces into single space
text = text.replace(whitespaceRE, ' ')
}
let res
let child: ?ASTNode
if (!inVPre && text !== ' ' && (res = parseText(text, delimiters))) {
child = {
type: 2,
expression: res.expression,
tokens: res.tokens,
text
}
} else if (text !== ' ' || !children.length || children[children.length - 1].text !== ' ') {
child = {
type: 3,
text
}
}
if (child) {
if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {
child.start = start
child.end = end
}
children.push(child)
}
}
},
comment (text: string, start, end) {
// adding anything as a sibling to the root node is forbidden
// comments should still be allowed, but ignored
if (currentParent) {
const child: ASTText = {
type: 3,
text,
isComment: true
}
if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {
child.start = start
child.end = end
}
currentParent.children.push(child)
}
}
})
// 返回根節點
return root
}
複製代碼
能夠看出作的最核心的事情就是調用 parseHTML,且傳的鉤子中作的事情最多的仍是在 start 開始標籤這裏最多。針對於在 Vue 的場景,利用鉤子的處理,最終咱們返回的 root 其實就是一個樹的根節點,也就是咱們的 ast,形如:
模板爲:
<div id="app">{{ msg }}</div>
複製代碼
{
"type": 1,
"tag": "div",
"attrsList": [
{
"name": "id",
"value": "app"
}
],
"attrsMap": {
"id": "app"
},
"rawAttrsMap": {},
"children": [
{
"type": 2,
"expression": "_s(msg)",
"tokens": [
{
"@binding": "msg"
}
],
"text": "{{ msg }}"
}
],
"plain": false,
"attrs": [
{
"name": "id",
"value": "app"
}
]
}
複製代碼
因此接下來纔是parse最核心的部分 parseHTML,取核心部分(不全),一部分一部分來分析,源文件 github.com/vuejs/vue/b…
// parse的過程就是一個遍歷 html 字符串的過程
export function parseHTML (html, options) {
// html 就是一個 HTML 字符串
// 再次出現棧,最佳數據結構,用於處理嵌套解析問題
// HTML 中就是處理 標籤 嵌套
const stack = []
const expectHTML = options.expectHTML
const isUnaryTag = options.isUnaryTag || no
const canBeLeftOpenTag = options.canBeLeftOpenTag || no
// 初始索引位置 index
let index = 0
let last, lastTag
// 暴力循環 目的爲了遍歷
while (html) {
last = html
// Make sure we're not in a plaintext content element like script/style
if (!lastTag || !isPlainTextElement(lastTag)) {
// 沒有 lastTag 即初始狀態 或者說 lastTag 是 script style
// 這種須要當作純文本處理的標籤元素
// 正常狀態下 都應進入這個分支
// 判斷標籤位置,其實也就是判斷了非標籤的end位置
let textEnd = html.indexOf('<')
// 在起始位置
if (textEnd === 0) {
// 註釋,先忽略
if (comment.test(html)) {
const commentEnd = html.indexOf('-->')
if (commentEnd >= 0) {
if (options.shouldKeepComment) {
options.comment(html.substring(4, commentEnd), index, index + commentEnd + 3)
}
advance(commentEnd + 3)
continue
}
}
// http://en.wikipedia.org/wiki/Conditional_comment#Downlevel-revealed_conditional_comment
// 條件註釋,先忽略
if (conditionalComment.test(html)) {
const conditionalEnd = html.indexOf(']>')
if (conditionalEnd >= 0) {
advance(conditionalEnd + 2)
continue
}
}
// Doctype 先忽略
const doctypeMatch = html.match(doctype)
if (doctypeMatch) {
advance(doctypeMatch[0].length)
continue
}
// 結束標籤,第一次先忽略,其餘case會進入
const endTagMatch = html.match(endTag)
if (endTagMatch) {
const curIndex = index
advance(endTagMatch[0].length)
// 處理結束標籤
parseEndTag(endTagMatch[1], curIndex, index)
continue
}
// 重點,通常場景下,開始標籤
const startTagMatch = parseStartTag()
// 若是存在開始標籤
if (startTagMatch) {
// 處理相關邏輯
handleStartTag(startTagMatch)
if (shouldIgnoreFirstNewline(startTagMatch.tagName, html)) {
advance(1)
}
continue
}
}
let text, rest, next
if (textEnd >= 0) {
// 剩餘的 html 去掉文本以後的
rest = html.slice(textEnd)
while (
!endTag.test(rest) &&
!startTagOpen.test(rest) &&
!comment.test(rest) &&
!conditionalComment.test(rest)
) {
// 純文本內的 <
// < in plain text, be forgiving and treat it as text
next = rest.indexOf('<', 1)
if (next < 0) break
textEnd += next
rest = html.slice(textEnd)
}
// 獲得真正的文本內容
text = html.substring(0, textEnd)
}
// 已經沒有 < 了 因此內容就是純文本
if (textEnd < 0) {
text = html
}
if (text) {
// 重點 前進指定長度
advance(text.length)
}
if (options.chars && text) {
// 鉤子函數處理
options.chars(text, index - text.length, index)
}
} else {
// lastTag 存在 且是 script style 這樣的 將其內容當作純文本處理
let endTagLength = 0
// 存在棧中的tag名
const stackedTag = lastTag.toLowerCase()
// 指定 tag 的 匹配正則 注意 是到對應結束標籤的 正則,例如 </script>
const reStackedTag = reCache[stackedTag] || (reCache[stackedTag] = new RegExp('([\\s\\S]*?)(</' + stackedTag + '[^>]*>)', 'i'))
// 作替換
// 即把 <div>xxxx</div></script> 這樣的替換掉
const rest = html.replace(reStackedTag, function (all, text, endTag) {
// 結束標籤自己長度 即 </script>的長度
endTagLength = endTag.length
if (!isPlainTextElement(stackedTag) && stackedTag !== 'noscript') {
text = text
.replace(/<!\--([\s\S]*?)-->/g, '$1') // #7298
.replace(/<!\[CDATA\[([\s\S]*?)]]>/g, '$1')
}
if (shouldIgnoreFirstNewline(stackedTag, text)) {
text = text.slice(1)
}
// 鉤子函數處理
if (options.chars) {
options.chars(text)
}
// 替換爲空
return ''
})
// 索引前進 注意沒有用 advance 由於 html 實際上是已經修正過的 即 rest
index += html.length - rest.length
html = rest
// 處理結束標籤
parseEndTag(stackedTag, index - endTagLength, index)
}
if (html === last) {
options.chars && options.chars(html)
if (process.env.NODE_ENV !== 'production' && !stack.length && options.warn) {
options.warn(`Mal-formatted tag at end of template: "${html}"`, { start: index + html.length })
}
break
}
}
}
複製代碼
這裏邊有幾個重點的函數,他們都是定義在 parseHTML 整個函數上下文中的,因此他們能夠直接訪問上邊定義的 index stack lastTag 等關鍵變量:
// 比較好理解,前進n個位置
function advance (n) {
index += n
html = html.substring(n)
}
複製代碼
// 開始標籤
function parseStartTag () {
// 正則匹配開始 例如 <div
const start = html.match(startTagOpen)
if (start) {
// 匹配到的
const match = {
tagName: start[1],
attrs: [],
start: index
}
// 移到 <div 以後
advance(start[0].length)
let end, attr
// 到結束以前 即 > 以前
while (!(end = html.match(startTagClose)) && (attr = html.match(dynamicArgAttribute) || html.match(attribute))) {
// 匹配屬性們
attr.start = index
// 逐步移動
advance(attr[0].length)
attr.end = index
// 收集屬性
match.attrs.push(attr)
}
// 遇到了 > 結束了
if (end) {
// 是不是 自閉合標籤,例如 <xxxx />
match.unarySlash = end[1]
advance(end[0].length)
match.end = index
return match
}
}
}
複製代碼
// 當遇到開始標籤的狀況 去處理他們
// 由於開始標籤的狀況比較複雜 因此 單獨了一個函數處理
function handleStartTag (match) {
const tagName = match.tagName
const unarySlash = match.unarySlash
if (expectHTML) {
// HTML 場景
// p 標籤以內不能存在 isNonPhrasingTag 的tag
// 詳細的看 https://github.com/vuejs/vue/blob/v2.6.14/src/platforms/web/compiler/util.js#L18
if (lastTag === 'p' && isNonPhrasingTag(tagName)) {
// 因此在瀏覽器環境 也是會自動容錯處理的 直接閉合他們
parseEndTag(lastTag)
}
if (canBeLeftOpenTag(tagName) && lastTag === tagName) {
parseEndTag(tagName)
}
}
// 自閉和的場景 或者 能夠省略結束標籤的case
// 即 <xxx /> 或者 <br> <img> 這樣的場景
const unary = isUnaryTag(tagName) || !!unarySlash
const l = match.attrs.length
const attrs = new Array(l)
for (let i = 0; i < l; i++) {
const args = match.attrs[i]
const value = args[3] || args[4] || args[5] || ''
const shouldDecodeNewlines = tagName === 'a' && args[1] === 'href'
? options.shouldDecodeNewlinesForHref
: options.shouldDecodeNewlines
attrs[i] = {
name: args[1],
value: decodeAttr(value, shouldDecodeNewlines)
}
if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {
attrs[i].start = args.start + args[0].match(/^\s*/).length
attrs[i].end = args.end
}
}
if (!unary) {
// 若是不是自閉和case 也就意味着能夠當作有 children 處理的
// 棧裏 push 一個當前的
stack.push({ tag: tagName, lowerCasedTag: tagName.toLowerCase(), attrs: attrs, start: match.start, end: match.end })
// 把 lastTag 設置爲當前的
// 爲了下次進入 children 作準備
lastTag = tagName
}
// start 鉤子處理
if (options.start) {
options.start(tagName, attrs, unary, match.start, match.end)
}
}
複製代碼
// 結束標籤處理
function parseEndTag (tagName, start, end) {
let pos, lowerCasedTagName
if (start == null) start = index
if (end == null) end = index
// Find the closest opened tag of the same type
if (tagName) {
lowerCasedTagName = tagName.toLowerCase()
// 這裏須要找到 最近的 相同類型的 未閉合標籤
// 相對應的配對的那個元素
for (pos = stack.length - 1; pos >= 0; pos--) {
if (stack[pos].lowerCasedTag === lowerCasedTagName) {
break
}
}
} else {
// If no tag name is provided, clean shop
pos = 0
}
if (pos >= 0) {
// 回到那個未閉合的標籤,這中間裏邊全部的元素都須要閉合掉
// Close all the open elements, up the stack
for (let i = stack.length - 1; i >= pos; i--) {
if (process.env.NODE_ENV !== 'production' &&
(i > pos || !tagName) &&
options.warn
) {
options.warn(
`tag <${stack[i].tag}> has no matching end tag.`,
{ start: stack[i].start, end: stack[i].end }
)
}
// end 鉤子
if (options.end) {
options.end(stack[i].tag, start, end)
}
}
// 裏邊的元素也不須要處理了 直接修改棧的長度便可
// Remove the open elements from the stack
stack.length = pos
// 記得更新 lastTag
lastTag = pos && stack[pos - 1].tag
} else if (lowerCasedTagName === 'br') {
// br 的狀況 若是寫的是 </br> 其實效果至關於 <br>
if (options.start) {
options.start(tagName, [], true, start, end)
}
} else if (lowerCasedTagName === 'p') {
// p 的狀況 若是找不到 <p> 直接匹配到了 </p> 那麼認爲是 <p></p> 由於瀏覽器也是這樣兼容
if (options.start) {
options.start(tagName, [], false, start, end)
}
if (options.end) {
options.end(tagName, start, end)
}
}
}
複製代碼
因此大概瞭解了上邊三個函數的做用,再和 parseHTML 的主邏輯結合起來,咱們能夠大概整理下 parseHTML 的整個過程。
這裏爲了方便,以一個具體的示例來進行,例如
<div id="app">
<p :class="pClass">
<span>
This is dynamic msg:
<span>{{ msg }}</span>
</span>
</p>
</div>
複製代碼
那麼首先直接進入 parseHTML,進入 while 循環,很明顯會走入到對於開始標籤的處理 parseStartTag
此時通過上邊的一輪處理,html已是這個樣子了,由於每次都有 advance 前進:
也就是關於最開始的根標籤 div 的開始部分 <div id="app">
已經處理完成了。
接着進入到 handleStartTag 的邏輯中
此時,stack 棧中已經 push 了一個元素,即咱們的開始標籤 div,也保存了相關的位置和屬性信息,lastTag 指向的就是 div。
接着繼續 while 循環處理
由於有空格和換行的關係,此時 textEnd 的值是 3,因此要進入到文本的處理邏輯(空格和換行原本就屬於文本內容)
因此這輪循環會處理好文本,而後進入下一次循環操做,此時已經和咱們第一輪循環的效果差很少:
再次lastTag變爲了 p,而後進入處處理文本(空格、換行)的邏輯,這裏直接省略,過程是同樣的;
下面直接跳到第一次處理 span
其實仍是重複和第一次的循環同樣,處理普通元素,處理完成後的結果:
此時棧頂的元素是外部的這個 span。而後進入新一輪的處理文本:
接着再一次進入處理裏層的 span 元素,同樣的邏輯,處理完成後
而後處理最裏層的文本,結束後,到達最裏層的結束標籤 </span>
,
這個時候咱們重點看下這一輪的循環:
能夠看到通過這一圈處理,最裏層的 span 已經通過閉合處理,棧和lastTag已經更新爲了外層的 span 了。
剩下的循環的流程,相信你已經可以大概猜到了,一直是處理文本內容(換行 空格)以及 parseEndTag 相關處理,一次次的出棧,直到 html 字符串處理完成,爲空,即中止了循環處理。
十分相似的原理,咱們的 parse 函數也是同樣的,根據 parseHTML 的鉤子函數,一次次的壓榨,處理,而後出棧 處理,直至完成,這些鉤子作的核心事情就是根據 parse HTML 的過程當中,一步步構建本身的 ast,那麼最終的 ast 結果
到這裏 parse 的階段已經完全完成。
接下來看看如何根據上述的 ast 獲得咱們想要的 render 函數。相關的代碼在 github.com/vuejs/vue/b…
export function generate ( ast: ASTElement | void, options: CompilerOptions ): CodegenResult {
const state = new CodegenState(options)
// fix #11483, Root level <script> tags should not be rendered.
const code = ast ? (ast.tag === 'script' ? 'null' : genElement(ast, state)) : '_c("div")'
return {
render: `with(this){return ${code}}`,
staticRenderFns: state.staticRenderFns
}
}
複製代碼
能夠看出,generate 核心,第一步建立了一個 CodegenState 實例,沒有很具體的功能,約等因而配置項的處理,而後進入核心邏輯 genElement,相關代碼 github.com/vuejs/vue/b…
// 生成元素代碼
export function genElement (el: ASTElement, state: CodegenState): string {
if (el.parent) {
el.pre = el.pre || el.parent.pre
}
if (el.staticRoot && !el.staticProcessed) {
return genStatic(el, state)
} else if (el.once && !el.onceProcessed) {
return genOnce(el, state)
} else if (el.for && !el.forProcessed) {
return genFor(el, state)
} else if (el.if && !el.ifProcessed) {
return genIf(el, state)
} else if (el.tag === 'template' && !el.slotTarget && !state.pre) {
return genChildren(el, state) || 'void 0'
} else if (el.tag === 'slot') {
return genSlot(el, state)
} else {
// component or element
let code
if (el.component) {
code = genComponent(el.component, el, state)
} else {
let data
if (!el.plain || (el.pre && state.maybeComponent(el))) {
data = genData(el, state)
}
const children = el.inlineTemplate ? null : genChildren(el, state, true)
code = `_c('${el.tag}'${ data ? `,${data}` : '' // data }${ children ? `,${children}` : '' // children })`
}
// module transforms
for (let i = 0; i < state.transforms.length; i++) {
code = state.transforms[i](el, code)
}
return code
}
}
複製代碼
基本上就是根據元素類型進行對應的處理,依舊是上邊的示例的話,會進入到
接下來會是一個重要的 genChildren github.com/vuejs/vue/b…
export function genChildren ( el: ASTElement, state: CodegenState, checkSkip?: boolean, altGenElement?: Function, altGenNode?: Function ): string | void {
const children = el.children
if (children.length) {
const el: any = children[0]
// optimize single v-for
if (children.length === 1 &&
el.for &&
el.tag !== 'template' &&
el.tag !== 'slot'
) {
const normalizationType = checkSkip
? state.maybeComponent(el) ? `,1` : `,0`
: ``
return `${(altGenElement || genElement)(el, state)}${normalizationType}`
}
const normalizationType = checkSkip
? getNormalizationType(children, state.maybeComponent)
: 0
const gen = altGenNode || genNode
return `[${children.map(c => gen(c, state)).join(',')}]${ normalizationType ? `,${normalizationType}` : '' }`
}
}
複製代碼
能夠看出,基本上是循環 children,而後 調用 genNode 生成 children 的代碼,genNode github.com/vuejs/vue/b…
function genNode (node: ASTNode, state: CodegenState): string {
if (node.type === 1) {
return genElement(node, state)
} else if (node.type === 3 && node.isComment) {
return genComment(node)
} else {
return genText(node)
}
}
複製代碼
這裏就是判斷每個節點類型,而後基本遞歸調用 genElement 或者 genComment、genText 來生成對應的代碼。
最終生成的代碼 code 以下:
能夠理解爲,遍歷上述的 ast,分別生成他們的對應的代碼,藉助於遞歸,很容易的就處理了各類狀況。固然,有不少細節這裏其實被咱們忽略掉了,主要仍是看的正常狀況下的核心的大概簡要流程,便於理解。
到此,這就是在 Vue 中是如何處理編譯模板到 render 函數的完整過程。
要找到背後的緣由,咱們能夠拆分爲兩個點:
這個問題其實尤大本人本身講過,爲何在 Vue 2 中引入 Virtual DOM,是否是有必要的等等。
來自方應杭的聚合回答:
這裏有一些文章和回答供參考(也包含了別人的總結部分):
這個在官網框架對比中有講到,原文 cn.vuejs.org/v2/guide/co…
固然,除了上述緣由以外,就是咱們在前言中提到的,模板是靜態的,Vue 能夠作針對性的優化,進而利用 AOT 技術,將運行時性能進一步提高。
這個也是爲何 Vue 中有構建出來了不一樣的版本,詳細參見 cn.vuejs.org/v2/guide/in…
經過上邊的分析,咱們知道在 Vue 中,template到render函數的大概過程,最核心的仍是:
這個也是編譯器作的最核心的事情。
那麼咱們能夠從中學到什麼呢?
編譯器,聽起來就很高大上了。經過咱們上邊的分析,也知道了在 Vue 中是如何處理的。
編譯器的核心原理和相比較的標準化的過程基本上仍是比較成熟的,無論說這裏分析和研究的對於 HTML 的解析,而後生成最終的 render 函數代碼,仍是其餘任何的語言,或者是你本身定義的」語言「都是能夠的。
想要深刻學習的話,最好的就是看編譯原理。在社區中,也有一個很出名的項目 github.com/jamiebuilds… 裏邊有包含了一個」五臟俱全「的編譯器,核心只有 200 行代碼,裏邊除了代碼以外,註釋也是精華,甚至於註釋比代碼更有用,很值得咱們去深刻學習和研究,且易於理解。
樹的這種數據結構,上述咱們經過parse獲得的 ast 其實就是一種樹狀結構,樹的應用,基本上隨處可見,只要你善於發現。利用他,能夠很好的幫助咱們進行邏輯抽象,統一處理。
在上述的分析中,咱們是屢次看到了對於棧的運用,以前在響應式原理中也有提到過,可是在這裏是一個十分典型的場景,也能夠說是棧這個數據結構的最佳實踐之一。
基本上你在社區中不少的框架或者優秀庫中,都能看到棧的相關應用的影子,能夠說是一個至關有用的一種數據結構。
咱們在 parseHTML 的 options 中看到了鉤子的應用,其實不止是這裏有用到這種思想。經過 parseHTML 對外暴露的鉤子函數 start、end、chars、comment 能夠很方便的讓使用者鉤入到 parseHTML 的執行邏輯當中,相信你也感覺到了,這是一種頗有簡單,可是確實很實用的思想。固然,這種思想自己,也經常和插件化設計方案或者叫微內核的架構設計一塊兒出現;針對於不一樣的場景,能夠有更復雜一些的實現,進而提供更增強大的功能,例如在 webpack 中,底層的 tapable 庫,本質也是這種思想的應用。
在整個的parser過程當中,咱們遇到了不少種使用正則的場景,尤爲是在 github.com/vuejs/vue/b… 這裏:
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
const dynamicArgAttribute = /^\s*((?:v-[\w-]+:|@|:|#)\[[^=]+?\][^\s"'<>\/=]*)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z${unicodeRegExp.source}]*`
const qnameCapture = `((?:${ncname}\\:)?${ncname})`
const startTagOpen = new RegExp(`^<${qnameCapture}`)
const startTagClose = /^\s*(\/?)>/
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`)
const doctype = /^<!DOCTYPE [^>]+>/i
// #7298: escape - to avoid being passed as HTML comment when inlined in page
const comment = /^<!\--/
const conditionalComment = /^<!\[/
const encodedAttr = /&(?:lt|gt|quot|amp|#39);/g
const encodedAttrWithNewLines = /&(?:lt|gt|quot|amp|#39|#10|#9);/g
複製代碼
這裏邊仍是包含了不少種正則的使用,也有正則的動態生成。正則自己有簡單的,有複雜的,若是你不能很好的理解這裏的正則,推薦你去看精通正則表達式這本書,相信看過以後,你會收穫不少。
滴滴前端技術團隊的團隊號已經上線,咱們也同步了必定的招聘信息,咱們也會持續增長更多職位,有興趣的同窗能夠一塊兒聊聊。