赞
踩
在调用$mount
方法时,会判断传参中是否存在render
或者template
(同时存在以render
为准)如果没有render
,而只有template
的话需要先使用编译器把模板编译成render
函数,之后进行渲染。
使用render
方式生成节点:
new Vue({
render: (h) => {
return h('div', {}, [h('h1', 'use render')])
},
})
内部直接调用mountComponent
方法组成updateComponent
方法进行渲染。
使用template
的方式
new Vue({
template: '<div><h1>use render</h1></div>'
})
在entry-runtime-with-compiler.js
文件中覆写了$mount
方法,先将template
进行解析。
template
可以有三种类型:
id
标识,通过id
在文档中查找对应的模板<div id="app">
<div id="temp">
<h1>use id to get template</h1>
</div>
</div>
new Vue({
app: '#app',
template: '#temp'
})
vue
内部通过id
去查找节点,而是直接提供节点。<div id="app">
<div>
<h1>use id to get template</h1>
</div>
</div>
new Vue({
app: '#app',
template: document.getElementsByTagName('div')[1],
})
new Vue({
app: '#app',
template: '<div><h1>use template string</h1></div>'
})
先看看$mount
对传入的template
进行处理逻辑:
// $mount方法逻辑 if (template) { if (typeof template === 'string') { // 处理传入id的情况: if (template.charAt(0) === '#') { template = idToTemplate(template) } // 传入真实节点情况: } else if (template.nodeType) { console.log(template) template = template.innerHTML // 非法格式 } else { if (process.env.NODE_ENV !== 'production') { warn('invalid template option:' + template, this) } return this } // 没有template默认渲染#app节点 } else if (el) { template = getOuterHTML(el) }
在确保了template
存在后,就是生成render
函数流程了,这也是本文章需要详细讲解的部分。
来看看$mount
中剩余逻辑:
if (template) {
const { render, staticRenderFns } = compileToFunctions(template, { // 编译模板
outputSourceRange: process.env.NODE_ENV !== 'production',
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this)
options.render = render
options.staticRenderFns = staticRenderFns
}
// 渲染
return mount.call(this, el, hydrating)
$mount
方法通过compileToFunctions
函数获取了render
函数,而compileToFunctions
这个方法用于通过createCompiler
生成的,为什么要设计的这么嵌套呢?这是因为Vue
内部需要兼容web
、weex
两个平台,这两个平台对于DOM
的操作是不同的,模板必然存在差异(比如说保留标签名,weex
存在slider
这个标签名,web
不存在,那么web
肯定处理不了)。
因此createCompiler
接受了一个对象(不同平台配置,确保能正确编译),并返回一个包含了compileToFunctions
方法的对象。本文章只讲解web
端。
// src/platforms/web/compiler/index.js
const { compile, compileToFunctions } = createCompiler(baseOptions)
createCompiler
是createCompilerCreator
工厂函数的返回值,可能大家会觉得很绕,不过vue
这样设计是为了用户可以自定义编译器的行为,我们看看vue
内部的编译器:
// src/compiler/index.js 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 } }
可以看到vue
的基础编译器非常容易理解:先解析,然后优化AST
树,最后生成render
函数。
根据createCompiler
方法传入的平台配置并与用户传入的options
合并后传作为最终的编译配置传入编译器中:
// src/compiler/create-compiler.js createCompilerCreator方法中compile逻辑 // 合并配置 if (options) { // 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] } } }
前面提过,createCompilerCreator
可以让用户自定义编译器,createCompileToFunctionFn
也是同理,上面的代码都是基于vue
内部配置合并后传入baseCompiler
中,而对于一些自定义的编译器不想使用到内部定义的配置,那么可以直接通过给createCompileToFunctionFn
工厂函数传入一个编译器,那么render
函数就可以按照传入的编译器进行生成。
说了这么多vue
的自定义编译器的设计,其实就是为了能让大家更好的理解这嵌套的函数,让我们把重点放回vue
的编译器,看看是怎么解析模板的。
将上面提到的处理用户定义编译器相关的代码全部移除,单独把vue
的编译器的核心代码提取出来:
// src/compiler/to-function.js createCompileToFunctionFn方法的返回值 function compileToFunctions ( template: string, options?: CompilerOptions, vm?: Component ): CompiledFunctionResult { const compiled = compile(template, options) const res = {} res.render = createFunction(compiled.render, fnGenErrors) res.staticRenderFns = compiled.staticRenderFns.map(code => { return createFunction(code, fnGenErrors) }) ... } // 将字符串转化成函数 function createFunction (code, errors) { return new Function(code) } // src/compiler/create-compiler.js createCompilerCreator中的返回值中定义的compile方法 function compile ( template: string, options?: CompilerOptions ): CompiledResult { ... const compiled = baseCompile(template.trim(), finalOptions) ... } // src/compiler/index.js 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 } }
可以看到vue
在处理template
的时候最终是通过baseCompile
方法。
这个方法把html
转化成ast
function parse (
template: string,
options: CompilerOptions
): ASTElement | void {
...
parseHTML(template, {
...
// 编译器配置
})
}
parseHTML
是将template
字符串转换成ast
的核心,通过词法分析和语法分析,将template
解析为一个树形结构的ast
。
解析template
,根据<
、>
字符来作为标识进行解析(HTML
里边的标签都是包裹在<
、>
内的)。
先看看parseHTML
的设计,还记得前面提到的vue
支持自定义解析器吗,parseHTML
也是这样的设计,parseHTML
方法里通过在解析出标签及属性后,会调用传入parseHTML
中的start
、charts
、end
等方法并把解析出的标签传入其中,由其中的解析器对解析出的标签等进行更一步的处理。
我们梳理一遍parseHTML
方法就能更理解这种设计了。
// src/compiler/parser/html-parser.js parseHTML 逻辑 while (html) { last = html // 不处理script、style、textarea标签 if (!lastTag || !isPlainTextElement(lastTag)) { let textEnd = html.indexOf('<') // 第一个是<字符 if (textEnd === 0) { ... } // 文本 if (textEnd >= 0) {} } else { // 单独处理script、style、textarea标签 ... } }
首先我们要知道为什么解析template
,因为template
中存在一些动态的属性(比如指令、表达式、响应式数据等等,还有组件标签<comp></comp>
)普通HTML
是无法处理这些东西的,因此需要解析出这些动态数据,之后通过document.createElement
的方式把解析出的数据放入创建的节点中。
而style
、script
没有这样的设计,因此不需要这样的处理。
来看看vue
是怎么处理style
跟script
的
// 单独处理script、style、textarea标签
// 把标签内的内容当作文本处理,不解析
if (options.chars) {
options.chars(text)
}
index += html.length - rest.length
html = rest
// 处理结束标签
parseEndTag(stackedTag, index - endTagLength, index)
非style
跟script
标签:
<
也就是if (textEnd === 0)
这个条件内的逻辑,我们能想到,这种情况就是遇到标签了,开始标签(例如<div>
)、结束标签(</div>
)、注释(<!--xxxxx-->
)
if (comment.test(html)) {
const commentEnd = html.indexOf('-->')
if (commentEnd >= 0) {
// 是否要生成注释节点。
if (options.shouldKeepComment) {
options.comment(html.substring(4, commentEnd), index, index + commentEnd + 3)
}
// 移动指针
advance(commentEnd + 3)
continue
}
}
vue
支持配置shouldKeepComment
属性(默认undefined
)用来选择要不要生成注释节点,调用options.comment
方法并把通过正则获取到的注释节点交给编译器中的comment
方法处理。
编译器中直接生成一个ast
对象,用type
来区分不同标签类型。
// src/compiler/parser/index.js parse 方法中传入parseHTML中的配置
comment(text, start, end) {
if (currentParent) {
const child = {
type: 3,
text,
isComment: true
}
currentParent.children.push(child)
}
}
虽然不至于有开发者会在模板中添加<!DOCTYPE html>
文档类型,但是vue
也进行了校验,做法就是直接掠过不处理。
const endTagMatch = html.match(endTag)
if (endTagMatch) {
const curIndex = index
advance(endTagMatch[0].length)
parseEndTag(endTagMatch[1], curIndex, index)
continue
}
function parseEndTag(tagName, start, end) { if (tagName) { // 匹配最近的开始标签 lowerCasedTagName = tagName.toLowerCase() for (pos = stack.length - 1; pos >= 0; pos--) { if (stack[pos].lowerCasedTag === lowerCasedTagName) { break } } } else { pos = 0 } if (pos >= 0) { for (let i = stack.length - 1; i >= pos; i--) { // 关闭标签 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) } } }
parseEndTag
用于解析结束标签,首先是需要在数组中找到当前结束标签对应的开始标签,然后调用options.end
去处理。
不过也有几种情况要处理:
p
,这种情况是p
标签内存在其他标签,vue
的处理先关闭开始的p
标签(详情看下面开始标签解析中的p
),这样就导致对应的结束标签p
没有处理,因此vue
的处理是直接调用start
后调用end
,生成一个空p
标签br
,没有结束标签,在开始标签处理部分会直接调用parseEndTag
处理。 end(tag, start, end) {
const element = stack[stack.length - 1]
stack.length -= 1
currentParent = stack[stack.length - 1]
closeElement(element)
}
function closeElement(element) {
if (!inVPre && !element.processed) {
// 处理标签内的属性
element = processElement(element, options)
}
...
}
closeElement
方法用于处理ref
属性及slot
、template
标签,并最后对attrs
属性进行处理(添加dynamicAttrs
、attrs
属性用于保存动态和静态属性)。
const startTagMatch = parseStartTag()
if (startTagMatch) {
handleStartTag(startTagMatch)
if (shouldIgnoreFirstNewline(startTagMatch.tagName, html)) {
advance(1)
}
continue
}
// src/compiler/parser/html-parser.js parseHTML/parseStartTag方法 function parseStartTag() { const start = html.match(startTagOpen) if (start) { const match = { tagName: start[1], attrs: [], start: index } ... // 遍历标签中的属性,放入attrs数组中 while (!(end = html.match(startTagClose)) && (attr = html.match(dynamicArgAttribute) || html.match(attribute))) { attr.start = index advance(attr[0].length) attr.end = index match.attrs.push(attr) } ... } }
parseStartTag
方法就是把开始标签中的属性全部解析出来并放入一个数组中,通过使用一个对象来记录这个开始标签(tagName
、attrs
属性、unarySlash
(用于判断是否是自关闭标签)等)
在通过parseStartTag
生成了一个标签对象后,还需要调用handleStartTag
方法进行处理:
这个方法内容比较多:
p
标签,可能存在嵌套其他标签的情况:<p><h1>1</h1></p>
浏览器的处理是:
<p></p>
<h1>1</h1>
<p></p>
因此vue
也严格按照浏览器的解析,将p
标签关闭后在处理其中的内容。
if (lastTag === 'p' && isNonPhrasingTag(tagName)) {
parseEndTag(lastTag)
}
th
,thead
等),也直接将标签关闭if (canBeLeftOpenTag(tagName) && lastTag === tagName) {
parseEndTag(tagName)
}
遍历处理标签对象中的attrs
,生成[{name, value}]
格式便于后续的处理。
把处理的好的标签放入一个队列中,用于后续处理关闭标签时能匹配到对应的开始标签,确保模板中的标签是对应的。
stack.push({ tag: tagName, lowerCasedTag: tagName.toLowerCase(), attrs: attrs, start: match.start, end: match.end })
options.start
方法把解析出的标签放入解析器中进行更进一步的处理。// src/compiler/parser/index.js parse 方法中传入parseHTML中的配置 start (tag, attrs, unary, start, end) { let element = createASTElement(tag, attrs, currentParent); // 处理指令 processFor(element) processIf(element) processOnce(element) if (!unary) { currentParent = element stack.push(element) } else { closeElement(element) } }
生成一个对象用于保存解析出来的标签,处理v-for
、v-if
、v-once
指令。
vue
还对ie6
中的条件注释进行了处理,这里不做介绍。
<
这时候说明是标签中的内容,直接截取后调用options.chars
生成文本。
// src/compiler/parser/index.js parse 方法中传入parseHTML中的配置 chars (text: string, start: number, end: number) { ... 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 } } }
这里主要考虑了文本中存在{{xxx}}
的情况,也就是动态值的情况,使用parseText
方法进行解析:
function parseText (text, delimiters) { ... while ((match = tagRE.exec(text))) { index = match.index if (index > lastIndex) { rawTokens.push(tokenValue = text.slice(lastIndex, index)) tokens.push(JSON.stringify(tokenValue)) } // tag token // 解析动态字符 const exp = parseFilters(match[1].trim()) // 用_s标记 tokens.push(`_s(${exp})`) rawTokens.push({ '@binding': exp }) lastIndex = index + match[0].length } return { expression: tokens.join('+'), tokens: rawTokens } }
通过判断文本中是否存在模板分隔符,然后使用_s
方法包裹表达式。
对静态节点进行标记,减少重新渲染时重复生成静态真实节点的耗时,从而进一步提高渲染性能。
function optimize (root, options) {
if (!root) return
isStaticKey = genStaticKeysCached(options.staticKeys || '')
isPlatformReservedTag = options.isReservedTag || no
markStatic(root)
markStaticRoots(root, false)
}
markStatic
的方法是用于给当前标签添加一个static
属性。
static
值:
true
, 只有当节点为不存在v-if
、v-bind
、属性都是在modules
中定义了过的静态属性、非组件标签、非v-for
指令生成、非slot
标签,且子节点也通过满足以上条件false
里边的逻辑非常简单,就是遍历节点及其子节点,只要不满足上面提到的几种情况,那么节点的static
属性就是true
,否则哪怕只有一个子节点存在这种情况都是所有的父节点都是false
。
可以在
src/core/vdom/patch.js
文件中的createPatchFunction
中的片段可以看到关于对static
属性的使用:if (isTrue(vnode.isStatic) && isTrue(oldVnode.isStatic) && vnode.key === oldVnode.key && (isTrue(vnode.isCloned) || isTrue(vnode.isOnce)) ) { vnode.componentInstance = oldVnode.componentInstance // 直接return,不执行下边的新旧节点对比逻辑 return }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
markStaticRoots
则是用来标记一组静态节点的根节点,当一个节点被标记成静态节点的根节点后,在渲染的时候就可以跳过这个节点的子树。
前面的parse
方法将template
解析成一个AST
对象,而optimize
方法则是对静态节点进行标记,generate
函数是用来解析元素节点(AST
对象)并生成渲染函数代码的。
function generate (
ast: ASTElement | void,
options: CompilerOptions
): CodegenResult {
const state = new CodegenState(options)
const code = ast ? genElement(ast, state) : '_c("div")'
return {
render: `with(this){return ${code}}`,
staticRenderFns: state.staticRenderFns
}
}
genElement
函数会根据AST
对象节点的标签名、属性、子节点等信息,生成对应的创建元素和设置属性的代码,并递归处理子节点。
function genElement (el: ASTElement, state: CodegenState): string { // 如果元素节点是静态节点且未被处理过,我们调用genStatic函数处理并返回结果 if (el.staticRoot && !el.staticProcessed) { return genStatic(el, state) } else if (el.once && !el.onceProcessed) { return genOnce(el, state) } else if (el.for && !el.forProcessed) { return genFor(el, state) } else if (el.if && !el.ifProcessed) { return genIf(el, state) } else if (el.tag === 'template' && !el.slotTarget && !state.pre) { return genChildren(el, state) || 'void 0' } else if (el.tag === 'slot') { return genSlot(el, state) } else { ... } return code } }
在Vue
中,每个AST
都有一个type
属性,用于表示该节点的类型。type
属性的取值如下:
genElement
函数的作用是根据一个元素节点的AST
描述对象生成该元素在模板中对应的渲染函数代码。在这个函数中,首先判断元素节点是否是静态节点、插槽节点、循环节点、条件节点或组件节点。如果是,则分别调用对应的函数处理;否则,根据ASTElement
节点的tag
属性生成创建元素的代码,并根据元素的静态属性、动态属性和子节点生成对应的代码片段,并将它们拼接成一个完整的创建元素的代码。
由于篇幅原因就不一一讲解是怎么处理。我们就重点就来看看render
、staticRenderFns
函数。
render
属性把生成的代码包裹在with(this){return ${code}}
中,这是因为在genElement
方法是把解析的节点包裹在渲染帮助方法中(src/core/instance/render-helpers/index.js
中定义的方法)由于这些被添加到Vue.prototype
中,通过with(this)
帮上下文绑定到Vue
实例中,这就可以调用_c
这些方法了。
render
函数会在每次重新渲染时调用,因此它可以响应动态数据的变化并更新视图。
staticRenderFns
是静态渲染函数的数组,这些函数也用于渲染组件模板。与render
不同的是,staticRenderFns
生成的结果只在组件初始化时被使用一次,并且之后不会再更新。这意味着静态渲染函数可以在性能上提供一些优化,因为它们不需要在每次重新渲染时重新计算。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。