简述编译流程
总的来说,在beforeMount之前
执行编译过程,第一步通过html-parser将template解析成ast抽象语法树
,第二步通过optimize优化
ast并标记静态节点和静态根节点,第三步通过generate将ast抽象语法树编译成render字符串
并将静态部分放到staticRenderFns中,最后通过new Function(render)生成render函数。在beforeMount和mounted之间执行render函数
生成VNode,然后通过patch(VNode)生成dom树并挂载,调用mounted。
编译入口
//entry-runtime-with-compiler.js
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function ( //挂载前先做一些预处理
el?: string | Element,
hydrating?: boolean
): Component {
const { render, staticRenderFns } = compileToFunctions(template, {
shouldDecodeNewlines,
delimiters: options.delimiters,
comments: options.comments
}, this)
options.render = render
options.staticRenderFns = staticRenderFns
return mount.call(this, el, hydrating)
}
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
callHook(vm, 'beforeMount')
let updateComponent = () => {
vm._update(vm._render(), hydrating)//调用render生成VNode,然后patch渲染到页面
}
vm._watcher = new Watcher(vm, updateComponent, noop)
hydrating = false
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}
由此可知,在beforeMount之前执行编译过程,然后调用beforeMount,然后执行render函数,然后调用mounted
编译过程
//src/compiler/index.js
export const createCompiler = createCompilerCreator(function baseCompile (
template: string,
options: CompilerOptions
): CompiledResult {
const ast = parse(template.trim(), options) //生成抽象语法树
optimize(ast, options) //优化抽象语法树,标记静态节点和静态根节点
const code = generate(ast, options) //生成render函数字符串
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns //静态无需重新渲染的部分
}
})
export function createCompilerCreator (baseCompile: Function): Function {
return function createCompiler (baseOptions: CompilerOptions) {
function compile (
template: string,
options?: CompilerOptions
): CompiledResult {
const finalOptions = Object.create(baseOptions)
const compiled = baseCompile(template, finalOptions)
return compiled
}
return {
compile,
compileToFunctions: createCompileToFunctionFn(compile)
}
}
}
export function createCompileToFunctionFn (compile: Function): Function {
const cache: {
[key: string]: CompiledFunctionResult;
} = Object.create(null)
return function compileToFunctions (
template: string,
options?: CompilerOptions,
vm?: Component
): CompiledFunctionResult {
options = extend({}, options)
const warn = options.warn || baseWarn
delete options.warn
const key = options.delimiters
? String(options.delimiters) + template
: template
if (cache[key]) {
return cache[key]
}
const compiled = compile(template, options)
const res = {}
const fnGenErrors = []
res.render = createFunction(compiled.render, fnGenErrors) // 将render字符串转换成函数
res.staticRenderFns = compiled.staticRenderFns.map(code => {
return createFunction(code, fnGenErrors) //将静态render字符串转换成函数
})
return (cache[key] = res) //缓存结果,当template不变时直接返回缓存
}
}
function createFunction (code, errors) {
try {
return new Function(code)
} catch (err) {
errors.push({ err, code })
return noop
}
}
由此可知,编译过程为:第一步通过html-parser将template解析成ast抽象语法树
,第二步通过optimize优化
ast并标记静态节点和静态根节点,第三步通过generate将ast抽象语法树编译成render字符串
并将静态部分放到staticRenderFns中,最后通过new Function(render)生成render函数。
将template解析成ast抽象语法树
src/compiler/index.js
src/compiler/parser/html-parser.js
export function parse (
template: string,
options: CompilerOptions
): ASTElement | void {
const stack = []
const preserveWhitespace = options.preserveWhitespace !== false
let root
let currentParent
let inVPre = false
let inPre = false
let warned = false
parseHTML(template, {
warn,
expectHTML: options.expectHTML,
isUnaryTag: options.isUnaryTag,
canBeLeftOpenTag: options.canBeLeftOpenTag,
shouldDecodeNewlines: options.shouldDecodeNewlines,
shouldKeepComment: options.comments,
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 */
if (isIE && ns === 'svg') {
attrs = guardIESVGBug(attrs)
}
let element: ASTElement = createASTElement(tag, attrs, currentParent)
if (ns) {
element.ns = ns
}
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
}
if (inVPre) {
processRawAttrs(element)
} else if (!element.processed) {
// structural directives
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) {
// 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 {
endPre(element)
}
// apply post-transforms
for (let i = 0; i < postTransforms.length; i++) {
postTransforms[i](element, options)
}
},
end () {
// remove trailing whitespace
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
stack.length -= 1
currentParent = stack[stack.length - 1]
endPre(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 = inPre || text.trim()
? isTextTag(currentParent) ? text : decodeHTMLCached(text)
// only preserve whitespace if its not right after a starting tag
: preserveWhitespace && children.length ? ' ' : ''
if (text) {
let expression
if (!inVPre && text !== ' ' && (expression = parseText(text, delimiters))) {
children.push({
type: 2,
expression,
text
})
} else if (text !== ' ' || !children.length || children[children.length - 1].text !== ' ') {
children.push({
type: 3,
text
})
}
}
},
comment (text: string) {
currentParent.children.push({
type: 3,
text,
isComment: true
})
}
})
return root
}
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) {
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
}
}
// 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
}
// End tag:
const endTagMatch = html.match(endTag)
if (endTagMatch) {
const curIndex = index
advance(endTagMatch[0].length)
parseEndTag(endTagMatch[1], curIndex, index)
continue
}
// Start tag:
const startTagMatch = parseStartTag()
if (startTagMatch) {
handleStartTag(startTagMatch)
if (shouldIgnoreFirstNewline(lastTag, html)) {
advance(1)
}
continue
}
}
let text, rest, next
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')
.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()
function advance (n) {
index += n
html = html.substring(n)
}
function parseStartTag () {
const start = html.match(startTagOpen)
if (start) {
const match = {
tagName: start[1],
attrs: [],
start: index
}
advance(start[0].length)
let end, attr
while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) {
advance(attr[0].length)
match.attrs.push(attr)
}
if (end) {
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) {
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] || ''
attrs[i] = {
name: args[1],
value: decodeAttr(
value,
options.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)
}
}
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
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
} else if (lowerCasedTagName === 'br') {
if (options.start) {
options.start(tagName, [], true, start, end)
}
} else if (lowerCasedTagName === 'p') {
if (options.start) {
options.start(tagName, [], false, start, end)
}
if (options.end) {
options.end(tagName, start, end)
}
}
}
}
假设有如下代码:
new Vue({
el: '#app',
template: `
<div id="app">
<div class="aa" :class="{'bb':bb}" v-if="cc" @click="meClick">
<input type="text" v-model="dd">
<div v-for="item in arr">{{item}}</div>
<div><span>我是静态内容</span></div>
</div>
</div>
`,
data() {
return {
bb:true,
cc:true,
dd:'input',
arr:[1,2,3],
}
},
methods:{
meClick(){}
},
})
生成的ast树如下:
let ast = {
type: 1,// type= 1普通元素 2表达式(动态) 3文本节点(静态)
tag: 'div',
attrsMap: {id: 'app'},
parent: undefined, //没有父元素
static: false,
staticRoot: false,
children: [
{
type: 1,
tag: "div",
attrsMap: {class: "aa", ':class': "{'bb':bb}", 'v-if': "cc", '@click': "meClick"},
if: "cc",
ifConditions: [{
exp: "cc"
}],
staticClass: "aa",
classBinding: "{'bb':bb}",
hasBindings: true,
events: {click: {value: "meClick", dynamic: false}},
static: false,
staticRoot: false,
ifProcessed: true,
children: [
{
type: 1,
tag: "input",
attrsMap: {type: "text", 'v-model': "dd"},
directives:[{name: "model", rawName: "v-model", value: "dd", arg: null, isDynamicArg: false,modifiers: undefined,}],
static: false,
staticRoot: false,
props:[{name: "value", value: "(dd)"}],
events:[{input:{value: "if($event.target.composing)return;dd=$event.target.value"}}]
},
{
type: 1,
tag: "div",
attrsMap: {'v-for': "item in arr"},
for: "arr",
alias: "item",
plain: true,
static: false,
staticRoot: false,
children:[{type: 2, expression: "_s(item)",tokens:[{'@binding': "item"}],text: "{{item}}"}]
},
{
type: 1,
tag: "div",
static: true,//第二步中被optimize标记的静态节点
staticInFor: false,
staticRoot: true,//第二步中被optimize标记的静态根节点
staticProcessed: true,
children:[
{
type: 1,
tag: "span",
static: true,//第二步中被optimize标记的静态节点
children:[{type: 3, text: "我是静态内容", static: true}]//文本类型,第二步中被optimize标记的静态节点
}
]
}
]
}
]
}
通过optimize标记ast的静态节点和静态根节点
/src/compiler/optimizer.js
function markStatic (node: ASTNode) {
node.static = isStatic(node)
if (node.type === 1) {
// 不要使组件槽内容静态。这就避免了
// 组件不能改变插槽节点
// 静态插槽内容热加载失败
if (
!isPlatformReservedTag(node.tag) &&
node.tag !== 'slot' &&
node.attrsMap['inline-template'] == null
) {
return
}
for (let i = 0, l = node.children.length; i < l; i++) {
const child = node.children[i]
markStatic(child)
if (!child.static) {//只要某一个子元素不是静态,则其不是静态
node.static = false
}
}
if (node.ifConditions) {
for (let i = 1, l = node.ifConditions.length; i < l; i++) {
const block = node.ifConditions[i].block
markStatic(block)
if (!block.static) {//只要某个if条件为真,则不是静态
node.static = false
}
}
}
}
}
function isStatic (node: ASTNode): boolean {
if (node.type === 2) { // 表达式类型节点
return false
}
if (node.type === 3) { // 文本类型节点
return true
}
return !!(node.pre || (
!node.hasBindings && // 不是动态绑定
!node.if && !node.for && // 不是 v-if 或 v-for 或 v-else
!isBuiltInTag(node.tag) && // 不是内置标签slot,component
isPlatformReservedTag(node.tag) && // 不是容器组件
!isDirectChildOfTemplateFor(node) && // 不是template或v-for的直接子元素
Object.keys(node).every(isStaticKey) // 当前节点所有属性key都是静态的 (比如说type,tag,attrsList,attrsMap,plain,parent,children,attrs)
))
}
function markStaticRoots (node: ASTNode, isInFor: boolean) {
// 当前元素是静态的且有子元素,则是静态根元素
if (node.type === 1) {
if (node.static || node.once) {
node.staticInFor = isInFor
}
if (node.static && node.children.length && !(
node.children.length === 1 &&
node.children[0].type === 3
)) {
node.staticRoot = true
return
} else {
node.staticRoot = false
}
if (node.children) {
for (let i = 0, l = node.children.length; i < l; i++) {
markStaticRoots(node.children[i], isInFor || !!node.for)
}
}
if (node.ifConditions) {
for (let i = 1, l = node.ifConditions.length; i < l; i++) {
markStaticRoots(node.ifConditions[i].block, isInFor)
}
}
}
}
第一步中的<div><span>我是静态内容</span></div>
会被标记为静态节点,该节点对象为
{
type: 1,
tag: "div",
static: true,//被optimize标记的静态节点
staticInFor: false,
staticRoot: true,//被optimize标记的静态根节点
staticProcessed: true,
children:[
{
type: 1,
tag: "span",
static: true,//被optimize标记的静态节点
children:[{type: 3, text: "我是静态内容", static: true}]//文本类型,被optimize标记的静态节点
}
]
}
通过generate将ast抽象语法树编译成render字符串
源码在src/compiler/codegen/index.js中
将第二步中的ast编译后如下:
//render
with(this){return _c('div',{attrs:{"id":"app"}},[(cc)?_c('div',{staticClass:"aa",class:{'bb':bb},on:{"click":meClick}},[_c('input',{directives:[{name:"model",rawName:"v-model",value:(dd),expression:"dd"}],attrs:{"type":"text"},domProps:{"value":(dd)},on:{"input":function($event){if($event.target.composing)return;dd=$event.target.value}}}),_v(" "),_l((arr),function(item){return _c('div',[_v(_s(item))])}),_v(" "),_m(0)],2):_e()])}
//staticRenderFns
["with(this){return _c('div',[_c('span',[_v("我是静态内容")])])}"]
//render中函数简称原名
export function installRenderHelpers (target: any) {
target._o = markOnce
target._n = toNumber
target._s = toString
target._l = renderList
target._t = renderSlot
target._q = looseEqual
target._i = looseIndexOf
target._m = renderStatic //注意看静态的部分被替换了
target._f = resolveFilter
target._k = checkKeyCodes
target._b = bindObjectProps
target._v = createTextVNode
target._e = createEmptyVNode
target._u = resolveScopedSlots
target._g = bindObjectListeners
}
最后通过options.render= new Function(render)
,options.staticRenderFns = new Function(staticRenderFns)
获得渲染函数和静态渲染函数数组并赋值到options上
至此,编译过程告一段落,后面的虚拟dom和patch请听下回分解。。。
ps:今天看到一句诗我很喜欢,"醉里不知天在水,满船清梦压星河"
,什么时候我才能过上这样的日子啊+_+。。。
关注下再走呗 +_+