在整理這篇文章時,我感到了困惑,困惑的是不知道該怎麼用更好的方式來將這部分繁瑣的內容讓你很容易的看的明白。我也但願這篇文章能做爲你的在閱讀時引導,你能夠一塊兒邊看引導,邊看源碼。javascript
還記得以前的《手拉手帶你過一遍vue部分源碼》嗎?在那裏,咱們已經知道,在src/platform/web/entry-runtime-with-compiler.js
重寫原型的$mount
方法時,已經將template轉換爲render函數了,接下來,咱們從這個地方做爲入口。html
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
來建立的,因此,這裏,咱們爲了好記,暫時忽略中間的全部步驟。 先從最後一步開始看吧。vue
代碼有點多久不在這裏不全貼出來了,我說,你看着。java
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。這裏爲何要作檢測?好問題,先記住,繼續往下看,你會本身獲得答案。node
const key = options.delimiters ? String(options.delimiters) + template : template if (cache[key]) { return cache[key] } 複製代碼
又一段代碼來爲了提升效率,直接從緩存中查找是否有已經編譯好的結果,有則直接返回。web
const compiled = compile(template, options) 複製代碼
這裏的compile
方法就須要看src/compiler/create-compiler.js
文件的createCompilerCreator
中的function compile
了。express
在compile方法裏,主要作了作了3件事api
將傳入的CompilerOptions
通過處理後掛載至finalOptions
中數組
這裏finalOptions
最終會變成:緩存
template
、finalOptions
傳入src/compiler/index.js
文件的baseCompile
中。ast
轉換時的錯誤信息。這裏咱們進入到baseCompile
中,看看baseCompile
作了什麼。
// 將傳入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
方法,咱們將整個抽象語法樹拿到了。
對當前抽象語法樹進行優化,標識出靜態節點,這部分咱們下一篇關於vNode
的文章會再提到。
這部分會將咱們的抽象語法樹,轉換爲對應的render方法的字符串。有興趣的能夠自行翻閱,看這部分時,你會更清晰一點 在vue instance時,爲原型掛載了各類_字母
的方法的用意
沒錯 你沒看作,這裏轉換的是
with(this){...}
的字符串,因此上面爲何在編譯template會檢測是否容許使用eval
是否是有眉目了?。
經過compile中的parse
、optimize
、generate
將template
轉換爲了render
。
若是你喜歡,我會繼續爲你帶來render
時,虛擬DOM相關的文章。
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) } } 複製代碼
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) } 複製代碼
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 }) } } } 複製代碼
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) } } } } 複製代碼