在整理這篇文章時,我感到了困惑,困惑的是不知道該怎麼用更好的方式來將這部分繁瑣的內容讓你很容易的看的明白。我也希望這篇文章能作為你的在閱讀時引導,你可以一起邊看引導,邊看原始碼。
如何找到我們需要關注的最終方法
還記得之前的《手拉手帶你過一遍vue部分原始碼》嗎?在那裡,我們已經知道,在src/platform/web/entry-runtime-with-compiler.js
重寫原型的$mount
方法時,已經將template轉換為render函式了,接下來,我們從這個地方作為入口。
const { render, staticRenderFns } = compileToFunctions(template, {
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this)
複製程式碼
compileToFunctions
從哪來的?說真的 這個問題看原始碼的時候 還挺繞的,首先引自於platforms/web/compiler/index.js
,然後發現是呼叫了src/compiler/index.js
的createCompiler
方法,而createCompiler
又是通過呼叫src/compiler/create-compiler.js
的createCompilerCreator
方法,而我們使用的compileToFunctions
方法呢,又是通過呼叫src/compiler/to-function.js
中的createCompileToFunctionFn
來建立的,所以,這裡,我們為了好記,暫時忽略中間的所有步驟。 先從最後一步開始看吧。
createCompileToFunctionFn(src/compiler/to-function.js)
程式碼有點多久不在這裡不全貼出來了,我說,你看著。
try {
new Function('return 1')
} catch (e) {
if (e.toString().match(/unsafe-eval|CSP/)) {
warn(
'It seems you are using the standalone build of Vue.js in an ' +
'environment with Content Security Policy that prohibits unsafe-eval. ' +
'The template compiler cannot work in this environment. Consider ' +
'relaxing the policy to allow unsafe-eval or pre-compiling your ' +
'templates into render functions.'
)
}
}
複製程式碼
這段程式碼做了什麼?在當前情景下,相當於
eval('function fn() {return 1}
,檢測當前網頁的內容安全政策,具體CSP是什麼,可以看一下阮老師的CSP。這裡為什麼要做檢測?好問題,先記住,繼續往下看,你會自己得到答案。
const key = options.delimiters
? String(options.delimiters) + template
: template
if (cache[key]) {
return cache[key]
}
複製程式碼
又一段程式碼來為了提高效率,直接從快取中查詢是否有已經編譯好的結果,有則直接返回。
const compiled = compile(template, options)
複製程式碼
這裡的compile
方法就需要看src/compiler/create-compiler.js
檔案的createCompilerCreator
中的function compile
了。
compile(src/compiler/create-compiler.js)
在compile方法裡,主要做了做了3件事
-
將傳入的
CompilerOptions
經過處理後掛載至finalOptions
中這裡
finalOptions
最終會變成:
- 將
template
、finalOptions
傳入src/compiler/index.js
檔案的baseCompile
中。 - 收集
ast
轉換時的錯誤資訊。
baseCompile(src/compiler/index.js)
這裡我們進入到baseCompile
中,看看baseCompile
做了什麼。
parse
// 將傳入html 轉換為ast語法樹
const ast = parse(template.trim(), options)
複製程式碼
劃重點啦,通過parse
方法將我們傳入的template中的內容,轉換為AST語法樹
一起來看下src/compiler/parser/index.js
檔案中的parse
方法。
function parse (template, options){
warn = options.warn || baseWarn
platformIsPreTag = options.isPreTag || no
platformMustUseProp = options.mustUseProp || no
platformGetTagNamespace = options.getTagNamespace || no
// pluckModuleFunction:找出options.mudules中每一項中屬性含有key方法
transforms = pluckModuleFunction(options.modules, 'transformNode')
preTransforms = pluckModuleFunction(options.modules, 'preTransformNode')
postTransforms = pluckModuleFunction(options.modules, 'postTransformNode')
delimiters = options.delimiters
// 存放astNode
const stack = []
const preserveWhitespace = options.preserveWhitespace !== false
// 定義根節點
let root
// 當前處理節點的父節點
let currentParent
// 標識屬性中是否有v-pre的
// 什麼是v-pre,見 https://cn.vuejs.org/v2/api/#v-pre
let inVPre = false
// 標識是否為pre標籤
let inPre = false
// 標識是否已經觸發warn
let warned = false
function warnOnce (msg) {}
function closeElement (element) {}
// 通過迴圈的方式解析傳入html
parseHTML(params)
/**
* 處理v-pre
* @param {*} el
*/
function processPre() {}
/**
* 處理html原生屬性
* @param {*} el
*/
function processRawAttrs (el) {}
return root
}
複製程式碼
從上面部分我們可以看出,實際做轉換的是
parseHTML
方法,我們在上面省略了parseHTML
的引數,因為在parseHTML
方法內部,會用到引數中的start
、end
、chars
、comment
方法,所以為了防止大家迷惑,在這裡我會在文章的最後,拆出來專門為每個方法提供註釋,方便大家閱讀。
這裡先進入src/compiler/parser/html-parser.js
中看看parseHTML
方法吧。
function parseHTML (html, options) {
const stack = []
const expectHTML = options.expectHTML
const isUnaryTag = options.isUnaryTag || no
const canBeLeftOpenTag = options.canBeLeftOpenTag || no
// 宣告index,標識當前處理傳入html的索引
let index = 0
let last, lastTag
// 迴圈處理html
while (html) {...}
// Clean up any remaining tags
parseEndTag()
/**
* 修改當前處理標記索引,並且將html已處理部分擷取掉
* @param {*} n 位數
*/
function advance (n) {}
/**
* 處理開始標籤,將屬性放入attrs中
*/
function parseStartTag () {}
/**
* 將parseStartTag處理的結果進行處理並且通過options.start生成ast node
* @param {*} match 通過parseStartTag處理的結果
*/
function handleStartTag (match) {}
/**
* 處理結束標籤
* @param {*} tagName 標籤名
* @param {*} start 在html中起始位置
* @param {*} end 在html中結束位置
*/
function parseEndTag (tagName, start, end) {}
}
複製程式碼
這裡我們保留了部分片段,完整的註釋部分,我會放在文章的最後。
通過parse
方法,我們將整個抽象語法樹拿到了。
optimize
對當前抽象語法樹進行優化,標識出靜態節點,這部分我們下一篇關於vNode
的文章會再提到。
generate(scr/compiler/codegen/index.js)
這部分會將我們的抽象語法樹,轉換為對應的render方法的字串。有興趣的可以自行翻閱,看這部分時,你會更清晰一點
在vue instance時,為原型掛載了各種_字母
的方法的用意
沒錯 你沒看做,這裡轉換的是
with(this){...}
的字串,所以上面為什麼在編譯template會檢測是否允許使用eval
是不是有眉目了?。
小結
通過compile中的parse
、optimize
、generate
將template
轉換為了render
。
最後
如果你喜歡,我會繼續為你帶來render
時,虛擬DOM相關的文章。
附錄—原始碼+註釋
options.start
start (tag, attrs, unary) {
// check namespace.
// inherit parent ns if there is one
const ns = (currentParent && currentParent.ns) || platformGetTagNamespace(tag)
// handle IE svg bug
/* istanbul ignore if */
// 處理IE的svg的BUG
if (isIE && ns === 'svg') {
attrs = guardIESVGBug(attrs)
}
// 建立AST NODE
let element: ASTElement = createASTElement(tag, attrs, currentParent)
if (ns) {
element.ns = ns
}
// 判斷當前節點如果是<style>、<script>以及不是服務端渲染時給出警告
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.'
)
}
// apply pre-transforms
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
}
// 如果含有v-pre,則跳過編譯過程
if (inVPre) {
processRawAttrs(element)
} else if (!element.processed) {
// structural directives
// 處理v-for
processFor(element)
// 處理v-if v-else-if v-else
processIf(element)
// 處理v-once
processOnce(element)
// element-scope stuff
// 處理ast node節點,key、ref、slot、component、attrs
processElement(element, options)
}
// root節點約束檢測
function checkRootConstraints (el) {
if (process.env.NODE_ENV !== 'production') {
// slot、template不能作為root
if (el.tag === 'slot' || el.tag === 'template') {
warnOnce(
`Cannot use <${el.tag}> as component root element because it may ` +
'contain multiple nodes.'
)
}
// root節點中不能存在v-for
if (el.attrsMap.hasOwnProperty('v-for')) {
warnOnce(
'Cannot use v-for on stateful component root element because ' +
'it renders multiple elements.'
)
}
}
}
// tree management
if (!root) {
root = element
checkRootConstraints(root)
} else if (!stack.length) {
// allow root elements with v-if, v-else-if and v-else
if (root.if && (element.elseif || element.else)) {
checkRootConstraints(element)
addIfCondition(root, {
exp: element.elseif,
block: element
})
} else if (process.env.NODE_ENV !== 'production') {
warnOnce(
`Component template should contain exactly one root element. ` +
`If you are using v-if on multiple elements, ` +
`use v-else-if to chain them instead.`
)
}
}
if (currentParent && !element.forbidden) {
if (element.elseif || element.else) {
processIfConditions(element, currentParent)
} else if (element.slotScope) { // scoped slot
currentParent.plain = false
const name = element.slotTarget || '"default"'
;(currentParent.scopedSlots || (currentParent.scopedSlots = {}))[name] = element
} else {
currentParent.children.push(element)
element.parent = currentParent
}
}
if (!unary) {
currentParent = element
stack.push(element)
} else {
closeElement(element)
}
}
複製程式碼
options.end
end () {
// remove trailing whitespace
// 拿到stack中最後一個ast node
const element = stack[stack.length - 1]
// 找到最近處理的一個節點
const lastNode = element.children[element.children.length - 1]
if (lastNode && lastNode.type === 3 && lastNode.text === ' ' && !inPre) {
element.children.pop()
}
// pop stack
// 將element從stack移除
stack.length -= 1
currentParent = stack[stack.length - 1]
closeElement(element)
}
複製程式碼
options.chars
chars (text: string) {
// 文字沒有父節點處理
if (!currentParent) {
if (process.env.NODE_ENV !== 'production') {
if (text === template) {
warnOnce(
'Component template requires a root element, rather than just text.'
)
} else if ((text = text.trim())) {
warnOnce(
`text "${text}" outside root element will be ignored.`
)
}
}
return
}
// IE textarea placeholder bug
/* istanbul ignore if */
if (isIE &&
currentParent.tag === 'textarea' &&
currentParent.attrsMap.placeholder === text
) {
return
}
const children = currentParent.children
// 格式化text
text = inPre || text.trim()
? isTextTag(currentParent) ? text : decodeHTMLCached(text)
// only preserve whitespace if its not right after a starting tag
: preserveWhitespace && children.length ? ' ' : ''
if (text) {
// 處理{{text}}部分,將{{text}}轉為
// {expression: '_s(text)', token: [{'@binding': 'text'}]}
let res
if (!inVPre && text !== ' ' && (res = parseText(text, delimiters))) {
children.push({
type: 2,
expression: res.expression,
tokens: res.tokens,
text
})
} else if (text !== ' ' || !children.length || children[children.length - 1].text !== ' ') {
children.push({
type: 3,
text
})
}
}
}
複製程式碼
parseHTML
function parseHTML (html, options) {
const stack = []
const expectHTML = options.expectHTML
const isUnaryTag = options.isUnaryTag || no
const canBeLeftOpenTag = options.canBeLeftOpenTag || no
// 宣告index,標識當前處理傳入html的索引
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)) {
let textEnd = html.indexOf('<')
// 是否以<開頭
if (textEnd === 0) {
// Comment:
// 判斷是否是<!-- 開頭的註釋
if (comment.test(html)) {
const commentEnd = html.indexOf('-->')
if (commentEnd >= 0) {
if (options.shouldKeepComment) {
options.comment(html.substring(4, commentEnd))
}
advance(commentEnd + 3)
continue
}
}
// 判斷是否為相容性註釋以 <![ 開頭
// <!--[if IE 6]>
// Special instructions for IE 6 here
// <![endif]-->
// 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 開頭
// Doctype:
const doctypeMatch = html.match(doctype)
if (doctypeMatch) {
advance(doctypeMatch[0].length)
continue
}
// 判斷是否為結束標籤
// End tag:
const endTagMatch = html.match(endTag)
if (endTagMatch) {
const curIndex = index
advance(endTagMatch[0].length)
parseEndTag(endTagMatch[1], curIndex, index)
continue
}
// 通過parseStartTag後,會將html = '<div id="demo">...</div>'
// 的標籤處理為 html = '...<div>'
// 返回的match = {
// start: 0, // 開始索引
// end: 15, // 結束索引
// tagName: 'div'
// attrs: [] // 這裡的陣列為正則匹配出來標籤的attributes
// }
// Start tag:
const startTagMatch = parseStartTag()
if (startTagMatch) {
handleStartTag(startTagMatch)
if (shouldIgnoreFirstNewline(lastTag, html)) {
advance(1)
}
continue
}
}
let text, rest, next
// 這裡由於我們的html程式碼可能會有製表位 換行等不需要解析的操作
// 這裡將無用的東西移除,然後繼續迴圈html
if (textEnd >= 0) {
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)
advance(textEnd)
}
if (textEnd < 0) {
text = html
html = ''
}
// 處理字元
if (options.chars && text) {
options.chars(text)
}
} else {
let endTagLength = 0
const stackedTag = lastTag.toLowerCase()
const reStackedTag = reCache[stackedTag] || (reCache[stackedTag] = new RegExp('([\\s\\S]*?)(</' + stackedTag + '[^>]*>)', 'i'))
const rest = html.replace(reStackedTag, function (all, text, endTag) {
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 ''
})
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}"`)
}
break
}
}
// Clean up any remaining tags
parseEndTag()
/**
* 修改當前處理標記索引,並且將html已處理部分擷取掉
* @param {*} n 位數
*/
function advance (n) {
index += n
html = html.substring(n)
}
/**
* 處理開始標籤,將屬性放入attrs中
*/
function parseStartTag () {
const start = html.match(startTagOpen)
if (start) {
const match = {
tagName: start[1],
attrs: [],
start: index
}
// 處理完頭部資訊,將頭部移除掉
advance(start[0].length)
let end, attr
// 迴圈找尾部【>】,如果沒有到尾部時,就向attrs中新增當前正則匹配出的屬性。
while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) {
advance(attr[0].length)
match.attrs.push(attr)
}
// 將尾部【>】從html中移除,記錄當前處理完的索引
if (end) {
match.unarySlash = end[1]
advance(end[0].length)
match.end = index
return match
}
}
}
/**
* 將parseStartTag處理的結果進行處理並且通過options.start生成ast node
* @param {*} match 通過parseStartTag處理的結果
*/
function handleStartTag (match) {
const tagName = match.tagName
const unarySlash = match.unarySlash
if (expectHTML) {
if (lastTag === 'p' && isNonPhrasingTag(tagName)) {
parseEndTag(lastTag)
}
if (canBeLeftOpenTag(tagName) && lastTag === tagName) {
parseEndTag(tagName)
}
}
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]
// hackish work around FF bug https://bugzilla.mozilla.org/show_bug.cgi?id=369778
if (IS_REGEX_CAPTURING_BROKEN && args[0].indexOf('""') === -1) {
if (args[3] === '') { delete args[3] }
if (args[4] === '') { delete args[4] }
if (args[5] === '') { delete args[5] }
}
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 (!unary) {
stack.push({ tag: tagName, lowerCasedTag: tagName.toLowerCase(), attrs: attrs })
lastTag = tagName
}
if (options.start) {
options.start(tagName, attrs, unary, match.start, match.end)
}
}
/**
* 處理結束標籤
* @param {*} tagName 標籤名
* @param {*} start 在html中起始位置
* @param {*} end 在html中結束位置
*/
function parseEndTag (tagName, start, end) {
let pos, lowerCasedTagName
if (start == null) start = index
if (end == null) end = index
if (tagName) {
lowerCasedTagName = tagName.toLowerCase()
}
// Find the closest opened tag of the same type
// 從stack中找到與當前tag匹配的節點,這裡利用倒序,匹配最近的
if (tagName) {
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.`
)
}
if (options.end) {
options.end(stack[i].tag, start, end)
}
}
// Remove the open elements from the stack
stack.length = pos
lastTag = pos && stack[pos - 1].tag
// 處理br
} else if (lowerCasedTagName === 'br') {
if (options.start) {
options.start(tagName, [], true, start, end)
}
// 處理p標籤
} else if (lowerCasedTagName === 'p') {
if (options.start) {
options.start(tagName, [], false, start, end)
}
if (options.end) {
options.end(tagName, start, end)
}
}
}
}
複製程式碼