Vue.js 源码剖析 - 模板编译和组件化
模板编译简介
模板编译主要目的是将模板(template)转换为渲染函数(render)
模板编译的作用
- Vue2.x使用VNode描述视图及各种交互,用户自己编写VNode比较复杂
- 用户通过编写类似HTML的代码,即Vue.js的template模板,通过编译器将模板转换为返回VNode的render函数
- .vue文件会被webpack在构建的过程中转换成render函数
Vue Template Explorer
Vue Template Explorer是Vue官方提供的一款在线将template模板转换成render函数的工具
使用示例
<div id="app"><div>{{ msg }}</div><div>hello</div><comp/></div>
转成render函数
function render() {
with(this) {
return _c('div', {
attrs: {
"id": "app"
}
}, [_c('div', [_v(_s(msg))]), _c('div', [_v("hello")]), _c('comp')],
1)
}
}
通过查看生成的render函数内容,可以了解到,render'函数的核心是_c()
这个实例方法,用于创建并返回VNode
vm._c()
与vm.$createElement()
类似,在相同的位置定义,都调用createElement()
函数,差别只是最后一个处理children内容的参数值不同
tips:Vue2.x中,元素节点内容中的空格、换行等会被全部保留到render函数中,因此不必要的空格与换行会增加处理成本,而Vue3.x中,空格与换行不再保留到render函数中
模板编译的入口
在包含编译器版本的入口文件entry-runtime-with-compiler.js中,通过调用
compileToFunctions()
将template转换成render函数
-
compileToFunctions()
- 定义位置:
src/platforms/web/compiler/index.js
- 通过
createCompiler(baseOptions)
函数调用返回
- 定义位置:
-
createCompiler(baseOptions)
定义位置:
src/compiler/index.js
-
参数
baseOptions
// web平台相关的选项以及一些辅助函数 export const baseOptions: CompilerOptions = { expectHTML: true, modules, //模块 处理class、style、model directives, // 指令 处理v-model、v-text、v-html isPreTag, isUnaryTag, mustUseProp, canBeLeftOpenTag, isReservedTag, getTagNamespace, staticKeys: genStaticKeys(modules) }
返回
compile
与compileToFunctions:createCompileToFunctionFn(compile)
通过
createCompilerCreator()
工厂函数调用返回
-
createCompilerCreator(baseCompile)
- 定义位置:
src/compiler/create-compiler.js
- 参数
baseCompile
:编译模板的核心函数- 解析
- 优化
- 生成
- 返回
createCompiler
函数
- 定义位置:
-
createCompileToFunctionFn(compile)
- 定义位置:
src/compiler/to-function.js
- 参数
compile
- 返回
compileToFunctions()
- 定义位置:
模板编译过程
compileToFunctions
将模板字符串编译为render函数
export function createCompileToFunctionFn (compile: Function): Function {
// 创建缓存
const cache = Object.create(null)
return function compileToFunctions (
template: string,
options?: CompilerOptions,
vm?: Component
): CompiledFunctionResult {
// 备份options避免污染
options = extend({}, options)
const warn = options.warn || baseWarn
delete options.warn
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production') {
// detect possible CSP restriction
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.'
)
}
}
}
// check cache
// 检查缓存中是否存在CompileFunctionResult,有则直接返回
const key = options.delimiters
? String(options.delimiters) + template
: template
if (cache[key]) {
return cache[key]
}
// compile
// 把模板编译为编译对象{render, staticRenderFns, errors, tips},字符串形式的js代码
const compiled = compile(template, options)
// check compilation errors/tips
if (process.env.NODE_ENV !== 'production') {
if (compiled.errors && compiled.errors.length) {
if (options.outputSourceRange) {
compiled.errors.forEach(e => {
warn(
`Error compiling template:\n\n${e.msg}\n\n` +
generateCodeFrame(template, e.start, e.end),
vm
)
})
} else {
warn(
`Error compiling template:\n\n${template}\n\n` +
compiled.errors.map(e => `- ${e}`).join('\n') + '\n',
vm
)
}
}
if (compiled.tips && compiled.tips.length) {
if (options.outputSourceRange) {
compiled.tips.forEach(e => tip(e.msg, vm))
} else {
compiled.tips.forEach(msg => tip(msg, vm))
}
}
}
// turn code into functions
// 通过createFunction将js字符串转换成函数 new Function(code)
const res = {}
const fnGenErrors = []
res.render = createFunction(compiled.render, fnGenErrors)
res.staticRenderFns = compiled.staticRenderFns.map(code => {
return createFunction(code, fnGenErrors)
})
// check function generation errors.
// this should only happen if there is a bug in the compiler itself.
// mostly for codegen development use
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production') {
if ((!compiled.errors || !compiled.errors.length) && fnGenErrors.length) {
warn(
`Failed to generate render function:\n\n` +
fnGenErrors.map(({ err, code }) => `${err.toString()} in\n\n${code}\n`).join('\n'),
vm
)
}
}
// 缓存并返回编译结果res
return (cache[key] = res)
}
}
compile
模板字符串编译为编译对象,返回render的js函数字符串
核心作用是合并
baseOptions
与options
选项,并调用baseCompile
编译,记录错误,返回编译后的对象
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)
}
// 处理合并baseOptions与options为finalOptions
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
// 调用baseCompile编译template模板
const compiled = baseCompile(template.trim(), finalOptions)
if (process.env.NODE_ENV !== 'production') {
detectErrors(compiled.ast, warn)
}
compiled.errors = errors
compiled.tips = tips
return compiled
}
baseCompile
解析模板字符串,生成抽象语法树ast,优化、生成字符串形式js代码的渲染函数
function baseCompile (
template: string,
options: CompilerOptions
): CompiledResult {
// 调用parse将模板字符串转换成ast抽象语法树
// 抽象语法树: 以树的形式描述代码结构
const ast = parse(template.trim(), options)
// 优化抽象语法树
if (options.optimize !== false) {
optimize(ast, options)
}
// 把抽象语法树生成字符串形式js代码
const code = generate(ast, options)
return {
ast,
render: code.render, // 渲染函数
staticRenderFns: code.staticRenderFns // 静态渲染函数,生成静态VNode树
}
}
AST
AST(Abstract Syntax Tree) 抽象语法树,以树的形式描述代码结构
此处抽象语法树用来描述树形结构的HTML字符串
为什么要使用抽象语法树
- 模板字符串转换成AST后,可以通过AST对模板做优化处理
- 标记模板中的静态内容,在patch的时候直接跳过静态内容
- 在patch过程中静态内容不需要比对和重新渲染,从而提高性能
https://astexplorer.net/ 是一个在线的ast转换工具,可以查看和转换不同语言的ast生成结果
// AST对象
export function createASTElement (
tag: string,
attrs: Array<ASTAttr>,
parent: ASTElement | void
): ASTElement {
return {
type: 1,
tag,
attrsList: attrs,
attrsMap: makeAttrsMap(attrs),
rawAttrsMap: {},
parent,
children: []
}
}
parse
把HTML模板字符串转换成ast对象
对HTML模板字符串的解析借鉴了第三方库
simplehtmlparser
通过正则表达式对各种标签进行解析,并处理各种属性与指令
parse的整体代码较为复杂,这里不做深入分析
/**
* Convert HTML string to AST.
*/
export function parse (
template: string,
options: CompilerOptions
): ASTElement | void {
// 处理options
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
// 一些变量与方法定义
...
...
// 解析模板字符串
// 借鉴了simplehtmlparser.js
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, end) {
// 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)
}
// 创建AST对象
let element: ASTElement = createASTElement(tag, attrs, currentParent)
if (ns) {
element.ns = ns
}
if (process.env.NODE_ENV !== 'production') {
if (options.outputSourceRange) {
element.start = start
element.end = end
element.rawAttrsMap = element.attrsList.reduce((cumulated, attr) => {
cumulated[attr.name] = attr
return cumulated
}, {})
}
attrs.forEach(attr => {
if (invalidAttributeRE.test(attr.name)) {
warn(
`Invalid dynamic argument expression: attribute names cannot contain ` +
`spaces, quotes, <, >, / or =.`,
{
start: attr.start + attr.name.indexOf(`[`),
end: attr.start + attr.name.length
}
)
}
})
}
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
}
// 处理v-pre指令
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
// 处理结构化指令 v-for v-if v-once
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]
// 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 (!inPre && 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) {
// adding anything as a sibling to the root node is forbidden
// comments should still be allowed, but ignored
if (currentParent) {
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
}
optimize
优化的目标是寻找并标记静态子树,静态内容不会改变,不需要重新渲染,在patch过程中可以直接跳过
/**
* Goal of the optimizer: walk the generated template AST tree
* and detect sub-trees that are purely static, i.e. parts of
* the DOM that never needs to change.
*
* Once we detect these sub-trees, we can:
*
* 1. Hoist them into constants, so that we no longer need to
* create fresh nodes for them on each re-render;
* 2. Completely skip them in the patching process.
*/
export function optimize (root: ?ASTElement, options: CompilerOptions) {
if (!root) return
isStaticKey = genStaticKeysCached(options.staticKeys || '')
isPlatformReservedTag = options.isReservedTag || no
// first pass: mark all non-static nodes.
// 标记所有静态节点
markStatic(root)
// second pass: mark static roots.
// 标记所有静态根节点
markStaticRoots(root, false)
}
function markStatic (node: ASTNode) {
// 判断是否静态astNode
node.static = isStatic(node)
// 元素节点
if (node.type === 1) {
// do not make component slot content static. this avoids
// 1. components not able to mutate slot nodes
// 2. static slot content fails for hot-reloading
// 不将组件中的slot内容标记成静态节点
if (
!isPlatformReservedTag(node.tag) &&
node.tag !== 'slot' &&
node.attrsMap['inline-template'] == null
) {
return
}
// 遍历children
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) {
// 如果有一个子节点不是静态的,则当前节点不是静态的
node.static = false
}
}
}
}
}
function markStaticRoots (node: ASTNode, isInFor: boolean) {
if (node.type === 1) {
if (node.static || node.once) {
node.staticInFor = isInFor
}
// For a node to qualify as a static root, it should have children that
// are not just static text. Otherwise the cost of hoisting out will
// outweigh the benefits and it's better off to just always render it fresh.
// 一个静态根节点应当包含子节点,且子节点不是静态文本节点
// 否则优化成本大于收益,不如重新渲染
if (node.static && node.children.length && !(
node.children.length === 1 &&
node.children[0].type === 3
)) {
node.staticRoot = true
return
} else {
node.staticRoot = false
}
// 遍历所有子节点,递归调用markStaticRoots
if (node.children) {
for (let i = 0, l = node.children.length; i < l; i++) {
markStaticRoots(node.children[i], isInFor || !!node.for)
}
}
// 遍历所有条件渲染子节点,递归调用markStaticRoots
if (node.ifConditions) {
for (let i = 1, l = node.ifConditions.length; i < l; i++) {
markStaticRoots(node.ifConditions[i].block, isInFor)
}
}
}
}
generate
将AST对象转换成字符串形式js代码
export function generate (
ast: ASTElement | void,
options: CompilerOptions
): CodegenResult {
const state = new CodegenState(options)
// 如果传入的ast对象有值
// 调用核心方法genElement生成js字符串代码code
// 否则返回'_c("div")',即使用渲染函数创建空div节点
const code = ast ? genElement(ast, state) : '_c("div")'
return {
render: `with(this){return ${code}}`,
staticRenderFns: state.staticRenderFns
}
}
export function genElement (el: ASTElement, state: CodegenState): string {
if (el.parent) {
el.pre = el.pre || el.parent.pre
}
if (el.staticRoot && !el.staticProcessed) {
// 处理静态根节点
return genStatic(el, state)
} else if (el.once && !el.onceProcessed) {
// 处理v-once指令
return genOnce(el, state)
} else if (el.for && !el.forProcessed) {
// 处理v-for指令
return genFor(el, state)
} else if (el.if && !el.ifProcessed) {
// 处理v-if指令
return genIf(el, state)
} else if (el.tag === 'template' && !el.slotTarget && !state.pre) {
// 处理template子节点内容,如果非有效内容,返回'void 0'
return genChildren(el, state) || 'void 0'
} else if (el.tag === 'slot') {
// 处理slot标签
return genSlot(el, state)
} else {
// component or element
let code
if (el.component) {
// 处理组件
code = genComponent(el.component, el, state)
} else {
// 处理内置标签
let data
if (!el.plain || (el.pre && state.maybeComponent(el))) {
// 字符串拼接生成元素的各种属性、指令、事件等
// 处理各种指令,包括genDirectives (v-model/v-text/v-html)
data = genData(el, state)
}
// 遍历el.children子节点ast数组
// 调用genNode处理相应类型的节点并转换成字符串js拼接返回
// 对单个子节点的v-for使用genElement进行优化处理
const children = el.inlineTemplate ? null : genChildren(el, state, true)
// 拼接_c()渲染函数调用字符串代码
code = `_c('${el.tag}'${
data ? `,${data}` : '' // data
}${
children ? `,${children}` : '' // children
})`
}
// module transforms
for (let i = 0; i < state.transforms.length; i++) {
code = state.transforms[i](el, code)
}
return code
}
}
// 处理静态根节点
// hoist static sub-trees out
function genStatic (el: ASTElement, state: CodegenState): string {
// 标记静态根节点已处理,避免重复执行
el.staticProcessed = true
// Some elements (templates) need to behave differently inside of a v-pre
// node. All pre nodes are static roots, so we can use this as a location to
// wrap a state change and reset it upon exiting the pre node.
// 备份state.pre
const originalPreState = state.pre
if (el.pre) {
state.pre = el.pre
}
// 调用genElement并将生成的代码放入staticRenderFns数组
// 使用数组是因为可能存在多个静态根节点
state.staticRenderFns.push(`with(this){return ${genElement(el, state)}}`)
state.pre = originalPreState
// 拼接返回_m()函数调用的字符串代码
// _m函数是定义在原型上的实例方法,指向renderStatic方法实现
// 主要用处是调用staticRenderFns中的代码渲染静态节点
return `_m(${
state.staticRenderFns.length - 1
}${
el.staticInFor ? ',true' : ''
})`
}
模板编译过程总结
- 模板编译的过程是将模板字符串转换成
render
渲染函数的过程 -
render
函数通过compileToFunctions()
函数创建- 优先返回缓存中已编译好的
render
函数 - 调用
compile
函数创建
- 优先返回缓存中已编译好的
-
compile
函数主要合并了options
,主要的编译逻辑通过baseCompile
函数来完成 -
baseCompile
函数实现了模板的解析、优化、生成三个核心步骤- parse:解析,将模板字符串template转换成AST抽象语法树
- optimize:优化,标记AST中的静态子树,避免不必要的重新渲染以及patch过程
- generate:将AST生成js字符串代码
- 最终字符串代码通过
createFunction
函数,使用new Function(code)
的方式转换成实际执行的渲染函数
组件化简介
组件化可以让我们方便的将一个页面拆分成多个可重用的组件
使用组件可以使我们重用页面中的内容
组件之间也可以进行嵌套
什么是Vue组件?
- 一个Vue组件就是一个拥有预定义选项的Vue实例
- 一个组件可以组成页面上功能完备的区域,组件可以包含脚本(script)、样式(style)、模板(template)
组件注册
全局组件
全局组件使用Vue静态方法component来完成注册
-
使用示例
Vue.component('component-a', { } )
-
定义位置
src\core\global-api\assets.js
-
源码解析
export function initAssetRegisters (Vue: GlobalAPI) { /** * Create asset registration methods. */ // 初始化全局component、directive、filter的注册 ASSET_TYPES.forEach(type => { Vue[type] = function ( id: string, definition: Function | Object ): Function | Object | void { if (!definition) { return this.options[type + 's'][id] } else { /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && type === 'component') { validateComponentName(id) } // Vue.component('comp', { template: '' }) if (type === 'component' && isPlainObject(definition)) { // 没有指定name选项,使用id作为组件名 definition.name = definition.name || id // 把组件配置转换成组件的构造函数 definition = this.options._base.extend(definition) } if (type === 'directive' && typeof definition === 'function') { definition = { bind: definition, update: definition } } // 全局注册,存储资源并赋值 // this.options['components']['comp'] = definition this.options[type + 's'][id] = definition return definition } } }) }
Vue.extend
将组件配置转换成组件的构造函数
组件的构造函数继承自Vue的构造函数,因此组件对象拥有与Vue实例同样的成员(组件本身就是Vue实例)
Vue.extend = function (extendOptions: Object): Function {
extendOptions = extendOptions || {}
// Vue构造函数
const Super = this
const SuperId = Super.cid
// 从缓存中加载组件的构造函数 有就直接返回
const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
if (cachedCtors[SuperId]) {
return cachedCtors[SuperId]
}
const name = extendOptions.name || Super.options.name
if (process.env.NODE_ENV !== 'production' && name) {
// 验证组件名称
validateComponentName(name)
}
// 组件Sub构造函数
const Sub = function VueComponent (options) {
// 调用_init()初始化
this._init(options)
}
// 为Sub继承Vue原型
Sub.prototype = Object.create(Super.prototype)
Sub.prototype.constructor = Sub
Sub.cid = cid++
// 合并options
Sub.options = mergeOptions(
Super.options,
extendOptions
)
Sub['super'] = Super
// For props and computed properties, we define the proxy getters on
// the Vue instances at extension time, on the extended prototype. This
// avoids Object.defineProperty calls for each instance created.
// 一系列初始化工作
if (Sub.options.props) {
initProps(Sub)
}
if (Sub.options.computed) {
initComputed(Sub)
}
// allow further extension/mixin/plugin usage
// 从Vue继承静态方法
Sub.extend = Super.extend
Sub.mixin = Super.mixin
Sub.use = Super.use
// create asset registers, so extended classes
// can have their private assets too.
// 初始化组件的component、directive、filter的注册
ASSET_TYPES.forEach(function (type) {
Sub[type] = Super[type]
})
// enable recursive self-lookup
// 保存组件构造函数到Ctor.options.components.comp = Ctor
if (name) {
Sub.options.components[name] = Sub
}
// keep a reference to the super options at extension time.
// later at instantiation we can check if Super's options have
// been updated.
// 保存options引用
Sub.superOptions = Super.options
Sub.extendOptions = extendOptions
Sub.sealedOptions = extend({}, Sub.options)
// cache constructor
// 缓存组件的构造函数
cachedCtors[SuperId] = Sub
// 返回组件构造函数
return Sub
}
局部组件
局部组件使用components选项注册
-
使用示例
new Vue({ el: '#app', components: { 'component-a': ComponentA, 'component-b': ComponentB } })
组件的创建过程
组件本身也是Vue的实例,组件的创建过程也会执行Vue的一系列创建过程,并调用
createElement
创建VNode当判断要创建的VNode是自定义组件时,执行
createComponent
创建并返回自定义组件的VNode
// src\core\vdom\create-component.js
export function createComponent (
Ctor: Class<Component> | Function | Object | void,
data: ?VNodeData,
context: Component,
children: ?Array<VNode>,
tag?: string
): VNode | Array<VNode> | void {
if (isUndef(Ctor)) {
return
}
const baseCtor = context.$options._base
// plain options object: turn it into a constructor
// 如果Ctor是对象而不是构造函数
// 使用Vue.extend()创建组件的构造函数
if (isObject(Ctor)) {
Ctor = baseCtor.extend(Ctor)
}
// if at this stage it's not a constructor or an async component factory,
// reject.
if (typeof Ctor !== 'function') {
if (process.env.NODE_ENV !== 'production') {
warn(`Invalid Component definition: ${String(Ctor)}`, context)
}
return
}
// async component
// 异步组件处理
let asyncFactory
if (isUndef(Ctor.cid)) {
asyncFactory = Ctor
Ctor = resolveAsyncComponent(asyncFactory, baseCtor)
if (Ctor === undefined) {
// return a placeholder node for async component, which is rendered
// as a comment node but preserves all the raw information for the node.
// the information will be used for async server-rendering and hydration.
return createAsyncPlaceholder(
asyncFactory,
data,
context,
children,
tag
)
}
}
data = data || {}
// resolve constructor options in case global mixins are applied after
// component constructor creation
// 处理组件选项
resolveConstructorOptions(Ctor)
// transform component v-model data into props & events
// 处理组件v-model指令
if (isDef(data.model)) {
transformModel(Ctor.options, data)
}
// extract props
const propsData = extractPropsFromVNodeData(data, Ctor, tag)
// functional component
if (isTrue(Ctor.options.functional)) {
return createFunctionalComponent(Ctor, propsData, data, context, children)
}
// extract listeners, since these needs to be treated as
// child component listeners instead of DOM listeners
const listeners = data.on
// replace with listeners with .native modifier
// so it gets processed during parent component patch.
data.on = data.nativeOn
if (isTrue(Ctor.options.abstract)) {
// abstract components do not keep anything
// other than props & listeners & slot
// work around flow
const slot = data.slot
data = {}
if (slot) {
data.slot = slot
}
}
// install component management hooks onto the placeholder node
// 安装组件的钩子函数 init/prepatch/insert/destroy
// 组件实例在init钩子函数中调用createComponentInstanceForVnode创建
installComponentHooks(data)
// return a placeholder vnode
// 获取组件名称
const name = Ctor.options.name || tag
// 创建组件对应VNode对象
const vnode = new VNode(
`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
data, undefined, undefined, undefined, context,
{ Ctor, propsData, listeners, tag, children },
asyncFactory
)
// Weex specific: invoke recycle-list optimized @render function for
// extracting cell-slot template.
// https://github.com/Hanks10100/weex-native-directive/tree/master/component
/* istanbul ignore if */
if (__WEEX__ && isRecyclableComponent(vnode)) {
return renderRecyclableComponentTemplate(vnode)
}
return vnode
}
组件的patch过程
组件实例在init钩子函数中调用createComponentInstanceForVnode创建的
init钩子函数在patch过程中被调用
// src\core\vdom\patch.js
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
let i = vnode.data
if (isDef(i)) {
const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
if (isDef(i = i.hook) && isDef(i = i.init)) {
// 调用init钩子函数,创建和挂载组件实例
// init()的过程中创建了组件的真实DOM,挂载到了vnode.elm
i(vnode, false /* hydrating */)
}
// after calling the init hook, if the vnode is a child component
// it should've created a child instance and mounted it. the child
// component also has set the placeholder vnode's elm.
// in that case we can just return the element and be done.
if (isDef(vnode.componentInstance)) {
// 调用钩子函数
// 1. VNode钩子函数初始化属性/事件/样式等
// 2. 组件的钩子函数
initComponent(vnode, insertedVnodeQueue)
// 将组件对应的DOM插入到父元素中
// 先创建父组件 后创建子组件
// 先挂载子组件 后挂载父组件
insert(parentElm, vnode.elm, refElm)
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
}
return true
}
}
}
组件总结
- Vue组件是Vue实例,选项不同
- 父子组件创建顺序是先创建父组件,后创建子组件
- 父子组件挂载顺序是先挂载子组件,后挂载父组件
- 通过查看源码我们知道,每个组件的创建过程都会执行一系列Vue实例初始化过程,因此组件化的粒度并不是越细越好,合理的抽象组件结构与组件嵌套,可以避免不必要的性能消耗