mount是手動加載的過程,接下來讓咱們看看具體是怎麼實現的:html
src/platforms/web/entry-runtime-with-compiler.js
複製代碼
/*把本來不帶編譯的$mount方法保存下來,在最後會調用。*/
const mount = Vue.prototype.$mount
/*掛載組件,帶模板編譯*/
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && query(el)
/* istanbul ignore if */
if (el === document.body || el === document.documentElement) {
process.env.NODE_ENV !== 'production' && warn(
`Do not mount Vue to <html> or <body> - mount to normal elements instead.`
)
return this
}
const options = this.$options
// resolve template/el and convert to render function
/*處理模板templete,編譯成render函數,render不存在的時候纔會編譯template,不然優先使用render*/
if (!options.render) {
let template = options.template
/*template存在的時候取template,不存在的時候取el的outerHTML*/
if (template) {
/*當template的類型是字符串時*/
if (typeof template === 'string') {
/*檢索到template的首字母是#時判斷爲id*/
if (template.charAt(0) === '#') {
template = idToTemplate(template)
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && !template) {
warn(
`Template element not found or is empty: ${options.template}`,
this
)
}
}
} else if (template.nodeType) {
/*當template爲DOM節點的時候*/
template = template.innerHTML
} else {
/*報錯*/
if (process.env.NODE_ENV !== 'production') {
warn('invalid template option:' + template, this)
}
return this
}
} else if (el) {
/*獲取element的outerHTML*/
template = getOuterHTML(el)
}
if (template) {
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile')
}
/*將template編譯成render函數,這裏會有render以及staticRenderFns兩個返回,這是vue的編譯時優化,static靜態不須要在VNode更新時進行patch,優化性能*/
const { render, staticRenderFns } = compileToFunctions(template, {
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this)
options.render = render
options.staticRenderFns = staticRenderFns
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile end')
measure(`vue ${this._name} compile`, 'compile', 'compile end')
}
}
}
/*調用const mount = Vue.prototype.$mount保存下來的不帶編譯的mount*/
return mount.call(this, el, hydrating)
}
複製代碼
經過mount編譯代碼咱們清晰的瞭解到,在mount的過程當中,若是render函數不存在(render函數存在會優先使用render)會將template進行compileToFunctions獲得render以及staticRenderFns。譬如說手寫組件時加入了template的狀況都會在運行時進行編譯。而render function在運行後會返回VNode節點,供頁面的渲染以及在update的時候patch。接下來咱們來看一下template是如何編譯的。vue
compile 函數(src/compiler/index.js)就是將 template 編譯成 render function 的字符串形式。接下來就詳細講解這個函數:node
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 {
const ast = parse(template.trim(), options)
if (options.optimize !== false) {
optimize(ast, options)
}
const code = generate(ast, options)
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
})
複製代碼
createCompiler 函數主要經過3個步驟:parse、optimize、generate來生成一個包含ast、render、staticRenderFns的對象。git
在說parse函數以前,咱們先來了解一個概念:AST(Abstract Syntax Tree)抽象語法樹: 在計算機科學中,抽象語法樹(abstract syntax tree或者縮寫爲AST),或者語法樹(syntax tree),是源代碼的抽象語法結構的樹狀表現形式,這裏特指編程語言的源代碼。具體能夠查看抽象語法樹。github
AST會通過generate獲得render函數,render的返回值是VNode,VNode是Vue的虛擬DOM節點,具體定義以下:web
export default class VNode {
tag: string | void;
data: VNodeData | void;
children: ?Array<VNode>;
text: string | void;
elm: Node | void;
ns: string | void;
context: Component | void; // rendered in this component's scope key: string | number | void; componentOptions: VNodeComponentOptions | void; componentInstance: Component | void; // component instance parent: VNode | void; // component placeholder node // strictly internal raw: boolean; // contains raw HTML? (server only) isStatic: boolean; // hoisted static node isRootInsert: boolean; // necessary for enter transition check isComment: boolean; // empty comment placeholder? isCloned: boolean; // is a cloned node? isOnce: boolean; // is a v-once node? asyncFactory: Function | void; // async component factory function asyncMeta: Object | void; isAsyncPlaceholder: boolean; ssrContext: Object | void; fnContext: Component | void; // real context vm for functional nodes fnOptions: ?ComponentOptions; // for SSR caching fnScopeId: ?string; // functional scope id support constructor ( tag?: string, data?: VNodeData, children?: ?Array<VNode>, text?: string, elm?: Node, context?: Component, componentOptions?: VNodeComponentOptions, asyncFactory?: Function ) { /*當前節點的標籤名*/ this.tag = tag /*當前節點對應的對象,包含了具體的一些數據信息,是一個VNodeData類型,能夠參考VNodeData類型中的數據信息*/ this.data = data /*當前節點的子節點,是一個數組*/ this.children = children /*當前節點的文本*/ this.text = text /*當前虛擬節點對應的真實dom節點*/ this.elm = elm /*當前節點的名字空間*/ this.ns = undefined /*編譯做用域*/ this.context = context /*函數化組件做用域*/ this.functionalContext = undefined /*節點的key屬性,被看成節點的標誌,用以優化*/ this.key = data && data.key /*組件的option選項*/ this.componentOptions = componentOptions /*當前節點對應的組件的實例*/ this.componentInstance = undefined /*當前節點的父節點*/ this.parent = undefined /*簡而言之就是是否爲原生HTML或只是普通文本,innerHTML的時候爲true,textContent的時候爲false*/ this.raw = false /*靜態節點標誌*/ this.isStatic = false /*是否做爲跟節點插入*/ this.isRootInsert = true /*是否爲註釋節點*/ this.isComment = false /*是否爲克隆節點*/ this.isCloned = false /*是否有v-once指令*/ this.isOnce = false /*異步組件的工廠方法*/ this.asyncFactory = asyncFactory /*異步源*/ this.asyncMeta = undefined /*是否異步的預賦值*/ this.isAsyncPlaceholder = false } // DEPRECATED: alias for componentInstance for backwards compat. /* istanbul ignore next */ get child (): Component | void { return this.componentInstance } } 複製代碼
接下來咱們來看看parse的源碼:正則表達式
src/compiler/parser/index.js
複製代碼
function parse(template) {
...
const stack = [];
let currentParent; //當前父節點
let root; //最終返回出去的AST樹根節點
...
parseHTML(template, {
start: function start(tag, attrs, unary) {
......
},
end: function end() {
......
},
chars: function chars(text) {
......
}
})
return root
}
複製代碼
這個方法太長啦,就省略了parse的相關內容,只看一下大致的功能,其主要的功能函數應該是parseHTML方法。接受了2個參數,一個使咱們的模板template,另外一個是包含start、end、chars的方法。 在看parseHTML以前,咱們須要先了解一下下面這幾個正則:編程
// 該正則式可匹配到 <div id="index"> 的 id="index" 屬性部分
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
const ncname = '[a-zA-Z_][\\w\\-\\.]*'
const qnameCapture = `((?:${ncname}\\:)?${ncname})`
// 匹配起始標籤
const startTagOpen = new RegExp(`^<${qnameCapture}`)
const startTagClose = /^\s*(\/?)>/
// 匹配結束標籤
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`)
// 匹配DOCTYPE、註釋等特殊標籤
const doctype = /^<!DOCTYPE [^>]+>/i
const comment = /^<!\--/
const conditionalComment = /^<!\[/
複製代碼
Vue 經過上面幾個正則表達式去匹配開始結束標籤、標籤名、屬性等等。有了上面這些基礎,咱們再來看看parseHtml的內部實行:數組
export function parseHTML (html, options) {
const stack = []
const expectHTML = options.expectHTML
const isUnaryTag = options.isUnaryTag || no
const canBeLeftOpenTag = options.canBeLeftOpenTag || no
let index = 0
let last, lastTag
while (html) {
// 保留 html 副本
last = html
// 若是沒有lastTag,並確保咱們不是在一個純文本內容元素中:script、style、textarea
if (!lastTag || !isPlainTextElement(lastTag)) {
let textEnd = html.indexOf('<')
if (textEnd === 0) {
// Comment:
if (comment.test(html)) {
...
}
if (conditionalComment.test(html)) {
...
}
// Doctype:
const doctypeMatch = html.match(doctype)
if (doctypeMatch) {
...
}
// End tag:
const endTagMatch = html.match(endTag)
if (endTagMatch) {
...
}
// Start tag:
const startTagMatch = parseStartTag()
if (startTagMatch) {
...
}
}
let text, rest, next
if (textEnd >= 0) {
...
}
if (textEnd < 0) {
text = html
html = ''
}
// 繪製文本內容,使用 options.char 方法。
if (options.chars && text) {
options.chars(text)
}
} else {
...
}
...
}
複製代碼
上面只看一下代碼的大概意思:bash
1.首先經過while (html)去循環判斷html內容是否存在。
2.再判斷文本內容是否在script/style標籤中。
3.上述條件都知足的話,開始解析html字符串。
這裏面有parseStartTag 和 handleStartTag兩個方法值得關注一下:
function parseStartTag () {
//判斷html中是否存在開始標籤
const start = html.match(startTagOpen)
if (start) {
// 定義 match 結構
const match = {
tagName: start[1], // 標籤名
attrs: [], // 屬性名
start: index // 起點位置
}
/**
* 經過傳入變量n來截取字符串,這也是Vue解析的重要方法,經過不斷地蠶食掉html字符串,一步步完成對他的解析過程
*/
advance(start[0].length)
let end, attr
// 若是尚未到結束標籤的位置
// 存入屬性
while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) {
advance(attr[0].length)
match.attrs.push(attr)
}
// 返回處理後的標籤match結構
if (end) {
match.unarySlash = end[1]
advance(end[0].length)
match.end = index
return match
}
}
}
複製代碼
假設咱們設置一個html字符串
function handleStartTag (match) {
// match 是上面調用方法的時候傳遞過來的數據結構
const tagName = match.tagName
const unarySlash = match.unarySlash
...
const unary = isUnaryTag(tagName) || !!unarySlash
// 備份屬性數組的長度
const l = match.attrs.length
// 構建長度爲1的空數組
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] || ''
// 改變attr的格式爲 [{name: 'id', value: 'demo'}]
attrs[i] = {
name: args[1],
value: decodeAttr(
value,
options.shouldDecodeNewlines
)
}
}
// stack中記錄當前解析的標籤
// 若是不是自閉和標籤
// 這裏的stack這個變量在parseHTML中定義,做用是爲了存放標籤名 爲了和結束標籤進行匹配的做用。
if (!unary) {
stack.push({ tag: tagName, lowerCasedTag: tagName.toLowerCase(), attrs: attrs })
lastTag = tagName
}
// parse 函數傳入的 start 方法
options.start(tagName, attrs, unary, match.start, match.end)
}
複製代碼
到這裏彷佛一切明朗了許多,parseHTML主要用來解析html字符串,解析出字符串中的tagName,attrs,match等元素,傳入start方法:
start (tag, attrs, unary) {
...
// 建立基礎的 ASTElement
let element: ASTElement = createASTElement(tag, attrs, currentParent)
if (ns) {
element.ns = ns
}
...
if (!inVPre) {
// 判斷有沒有 v-pre 指令的元素。若是有的話 element.pre = true
// 官網有介紹:<span v-pre>{{ this will not be compiled }}</span>
// 跳過這個元素和它的子元素的編譯過程。能夠用來顯示原始 Mustache 標籤。跳過大量沒有指令的節點會加快編譯。
processPre(element)
if (element.pre) {
inVPre = true
}
}
if (platformIsPreTag(element.tag)) {
inPre = true
}
if (inVPre) {
// 處理原始屬性
processRawAttrs(element)
} else if (!element.processed) {
// structural directives
// v-for v-if v-once
processFor(element)
processIf(element)
processOnce(element)
// element-scope stuff
processElement(element, options)
}
// 檢查根節點約束
function checkRootConstraints (el) {
if (process.env.NODE_ENV !== 'production') {
if (el.tag === 'slot' || el.tag === 'template') {
warnOnce(
`Cannot use <${el.tag}> as component root element because it may ` +
'contain multiple nodes.'
)
}
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) {
// 容許有 v-if, v-else-if 和 v-else 的根元素
...
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 {
// 將元素插入 children 數組中
currentParent.children.push(element)
element.parent = currentParent
}
}
if (!unary) {
currentParent = element
stack.push(element)
} else {
endPre(element)
}
// apply post-transforms
for (let i = 0; i < postTransforms.length; i++) {
postTransforms[i](element, options)
}
}
複製代碼
其實start方法就是處理 element 元素的過程。肯定命名空間;建立AST元素 element;執行預處理;定義root;處理各種 v- 標籤的邏輯;最後更新 root、currentParent、stack 的結果。 最終經過 createASTElement 方法定義了一個新的 AST 對象。
下面咱們來屢一下parse總體的過程:
1.經過parseHtml來一步步解析傳入html字符串的標籤、元素、文本、註釋..。
2.parseHtml解析過程當中,調用傳入的start,end,chars方法來生成AST語法樹
咱們看一下最終生成的AST語法樹對象:
要是喜歡的話給我個star, github
感謝muwoo提供的解析思路。