對於我們經常使用框架,卻不知道內部具體經歷過了什麼,確實好奇到不行,接下來我們以 斷點的形式 進入,看看是怎麼將 模版 轉化爲 虛擬 Dom 進行操作的。 Go, Go,Go,玩起來
具體經歷步驟
1、從緩存中讀取 Render 函數
2、將模版轉化成 AST
3、AST 優化
4、AST 轉化爲 js 字符串
5、JS 字符串 轉化爲 匿名函數
6、Render 函數 = 匿名函數
7、Render 函數 轉化爲 Vnode
8、將 Vnode 創建成爲 真實 Dom
9、Node 標籤放入頁面中
一、 預備工作
1. Vue2.6 源碼的 clone
2. 瀏覽器,我用的是 google
3. 瀏覽器內斷點操作
二、新建 html
在 Vue2.6 源碼目錄下,找到 examples 文件夾,新建 /07.template/index.html , 以下爲 html 內部代碼
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <div id="app"> <div>a <span>b</span></div> <div>c </div> <div>{{name}} <span>e</span> </div> </div> <script src="../../dist/vue.js"></script> <script> const vm = new Vue({ el: '#app', data: { name: 'd' } }) </script> </body> </html>
三、斷點
打開 google 瀏覽器,F12 找到 sources, 根據下面圖片,找到 src/platforms/web/entry-runtime-with-compiler.js 對比的文件,並進行斷點
二. 詳細註解
一、從緩存中讀取 Render 函數
// vue2.6/src/compiler/to-function.js
// check cache const key = options.delimiters ? String(options.delimiters) + template : template if (cache[key]) { return cache[key] // 讀取緩存 }
function compile ( template: string, options?: CompilerOptions ): CompiledResult { const finalOptions = Object.create(baseOptions) const errors = [] const tips = [] let warn = (msg, range, tip) => { (tip ? tips : errors).push(msg) } if (options) { if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) { // $flow-disable-line const leadingSpaceLength = template.match(/^\s*/)[0].length warn = (msg, range, tip) => { const data: WarningMessage = { msg } if (range) { if (range.start != null) { data.start = range.start + leadingSpaceLength } if (range.end != null) { data.end = range.end + leadingSpaceLength } } (tip ? tips : errors).push(data) } } // merge custom modules if (options.modules) { finalOptions.modules = (baseOptions.modules || []).concat(options.modules) } // merge custom directives if (options.directives) { finalOptions.directives = extend( Object.create(baseOptions.directives || null), options.directives ) } // copy other options for (const key in options) { if (key !== 'modules' && key !== 'directives') { finalOptions[key] = options[key] } } } finalOptions.warn = warn const compiled = baseCompile(template.trim(), finalOptions) // 將模版轉化成 AST
if (process.env.NODE_ENV !== 'production') {
detectErrors(compiled.ast, warn)
}
compiled.errors = errors
compiled.tips = tips
return compiled
}
二、將模版轉化成 AST
export const createCompiler = createCompilerCreator(function baseCompile ( template: string, options: CompilerOptions ): CompiledResult { const ast = parse(template.trim(), options) // 模版轉化成 AST if (options.optimize !== false) { optimize(ast, options) } })
2. parse()
/** * Convert HTML string to AST. */ export function parse ( template: string, options: CompilerOptions ): ASTElement | void { warn = options.warn || baseWarn platformIsPreTag = options.isPreTag || no platformMustUseProp = options.mustUseProp || no platformGetTagNamespace = options.getTagNamespace || no const isReservedTag = options.isReservedTag || no maybeComponent = (el: ASTElement) => !!el.component || !isReservedTag(el.tag) transforms = pluckModuleFunction(options.modules, 'transformNode') preTransforms = pluckModuleFunction(options.modules, 'preTransformNode') postTransforms = pluckModuleFunction(options.modules, 'postTransformNode') delimiters = options.delimiters const stack = [] const preserveWhitespace = options.preserveWhitespace !== false const whitespaceOption = options.whitespace let root let currentParent let inVPre = false let inPre = false let warned = false function warnOnce (msg, range) { if (!warned) { warned = true warn(msg, range) } } function closeElement (element) { if (!inVPre && !element.processed) { element = processElement(element, options) } // tree management if (!stack.length && element !== root) { // allow root elements with v-if, v-else-if and v-else if (root.if && (element.elseif || element.else)) { if (process.env.NODE_ENV !== 'production') { 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.`, { start: element.start } ) } } if (currentParent && !element.forbidden) { if (element.elseif || element.else) { processIfConditions(element, currentParent) } else if (element.slotScope) { // scoped slot const name = element.slotTarget || '"default"' ;(currentParent.scopedSlots || (currentParent.scopedSlots = {}))[name] = element } else { currentParent.children.push(element) element.parent = currentParent } } // check pre state if (element.pre) { inVPre = false } if (platformIsPreTag(element.tag)) { inPre = false } // apply post-transforms for (let i = 0; i < postTransforms.length; i++) { postTransforms[i](element, options) } } function checkRootConstraints (el) { if (el.tag === 'slot' || el.tag === 'template') { warnOnce( `Cannot use <${el.tag}> as component root element because it may ` + 'contain multiple nodes.', { start: el.start } ) } if (hasOwn(el.attrsMap, 'v-for')) { warnOnce( 'Cannot use v-for on stateful component root element because ' + 'it renders multiple elements.', el.rawAttrsMap['v-for'] ) } } parseHTML(template, { warn, expectHTML: options.expectHTML, isUnaryTag: options.isUnaryTag, canBeLeftOpenTag: options.canBeLeftOpenTag, shouldDecodeNewlines: options.shouldDecodeNewlines, shouldDecodeNewlinesForHref: options.shouldDecodeNewlinesForHref, shouldKeepComment: options.comments, outputSourceRange: options.outputSourceRange, start (tag, attrs, unary, start) { // 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) } let element: ASTElement = createASTElement(tag, attrs, currentParent) if (ns) { element.ns = ns } if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) { element.start = start element.rawAttrsMap = element.attrsList.reduce((cumulated, attr) => { cumulated[attr.name] = attr return cumulated }, {}) } 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 } ) } // 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 } if (inVPre) { processRawAttrs(element) } else if (!element.processed) { // structural directives processFor(element) processIf(element) processOnce(element) } if (!root) { root = element if (process.env.NODE_ENV !== 'production') { checkRootConstraints(root) } } if (!unary) { currentParent = element stack.push(element) } else { closeElement(element) } }, end (tag, start, end) { const element = stack[stack.length - 1] if (!inPre) { // remove trailing whitespace node const lastNode = element.children[element.children.length - 1] if (lastNode && lastNode.type === 3 && lastNode.text === ' ') { element.children.pop() } } // pop stack stack.length -= 1 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 (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) { 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 }
三、AST 優化
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
}
})
2. AST 新增 靜態節點後的顯示
四、AST 轉化爲 js 字符串
export function generate ( ast: ASTElement | void, options: CompilerOptions ): CodegenResult { const state = new CodegenState(options) const code = ast ? genElement(ast, state) : '_c("div")' // AST 轉化爲 js 字符串,內部包含 _c,_v..... return { render: `with(this){return ${code}}`, staticRenderFns: state.staticRenderFns } }
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) // AST 轉化爲 js 字符串,加 with(this){return ${code}} return { ast, render: code.render, staticRenderFns: code.staticRenderFns } })