重学Vue源码,根据黄轶大佬的vue技术揭秘,逐个过一遍,巩固一下vue源码知识点,毕竟嚼碎了才是本身的,全部文章都同步在 公众号(道道里的前端栈) 和 github 上。前端
在开发过程当中,自定义组件必须先注册才可使用,若是直接使用的话,会报一个错:未知的自定义元素,就像下面这样:vue
'Unknown custom element: <xxx> - did you register the component correctly?
For recursive components, make sure to provide the "name" option.'
复制代码
在vue中提供了2种组件注册的方式:全局注册
和 局部注册
,下面来把它们分析一下。node
全局注册一个组件,通常会在 main.js
中这样写:git
Vue.component("comp-name", {
// options
})
复制代码
使用了一个 Vue.component
函数来注册,这个函数的定义过程是在最开始初始化Vue的全局函数的时候,代码在 src/core/global-api/assets.js
中:github
export const ASSET_TYPES = [
'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)
}
if (type === 'component' && isPlainObject(definition)) {
definition.name = definition.name || id
definition = this.options._base.extend(definition)
}
if (type === 'directive' && typeof definition === 'function') {
definition = { bind: definition, update: definition }
}
this.options[type + 's'][id] = definition
return definition
}
}
})
复制代码
能够看出来经过遍历 ASSET_TYPES
,往Vue上扩展了几个方法,每一个方法都有两个参数,一个id,一个自定义函数或对象,若是没有 definitioin
,那就不日后走了,不然就继续。在后面的逻辑里,对组件名作了一层校验,后面若是 type
是一个组件,而且它的定义是一个普通对象,就把 name
赋值,接着用 this.options._base.extend()
,把第二个参数转换成一个构造器, this.options._base
其实就是大Vue(以前分析过,经过 Vue.options._base = Vue
得知的),而后使用 Vue.extend()
把参数转化为构造器,最后把这个构造器赋值给 this.options[type + 's'][id]
,也就是给大Vue扩展定义了一个 components
构造器,最终挂载到了 Vue.options.components
上。api
因为在Vue初始化的时候会调用一个 _createElement
方法(它在render函数渲染和建立元素的时候分析过,能够看这篇createElement作了什么),在这个方法里有这样一段代码:markdown
if (typeof tag === 'string') {
let Ctor
ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
if (config.isReservedTag(tag)) {
// platform built-in elements
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,
undefined, undefined, context
)
} else if (isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
// component
vnode = createComponent(Ctor, data, context, children, tag)
} else {
// unknown or unlisted namespaced elements
// check at runtime because it may get assigned a namespace when its
// parent normalizes children
vnode = new VNode(
tag, data, children,
undefined, undefined, context
)
}
} else {
// direct component options / constructor
vnode = createComponent(tag, data, context, children)
}
复制代码
注册组件会走到 vnode = createComponent(Ctor, data, context, children, tag)
逻辑中,就会建立一个组件vnode,能够看到调用了一个 resolveAssets
方法,传入了 vm.$options
, components
和 tag
,这个方法定义在 src/core/util/options.js
:ide
export function resolveAsset ( options: Object, type: string, id: string, warnMissing?: boolean ): any {
/* istanbul ignore if */
if (typeof id !== 'string') {
return
}
const assets = options[type]
// check local registration variations first
if (hasOwn(assets, id)) return assets[id]
const camelizedId = camelize(id)
if (hasOwn(assets, camelizedId)) return assets[camelizedId]
const PascalCaseId = capitalize(camelizedId)
if (hasOwn(assets, PascalCaseId)) return assets[PascalCaseId]
// fallback to prototype chain
const res = assets[id] || assets[camelizedId] || assets[PascalCaseId]
if (process.env.NODE_ENV !== 'production' && warnMissing && !res) {
warn(
'Failed to resolve ' + type.slice(0, -1) + ': ' + id,
options
)
}
return res
}
复制代码
这里注意一下第一个参数 options
,以前在分析合并配置的时候(能够看这篇Vue的合并配置过程)有提到: vm.$options
实际上是自定义配置和大 Vue.options 一块儿合并出来的,因此在 asset.js
中的最后,给大Vue扩展一个 components
,在 resolveAsset
第一个参数 options
上就能够去找,也就是下面的一些 if
判断了。函数
继续看,type
传入的是 components
,赋值给 assets
,而后判断 assets
自身有 id
属性的话,就返回它,不然就把 id
转化为驼峰,后面一样的逻辑,根据驼峰去找,若是驼峰找不到,就找首字母大写,若是仍是找不到,那么注释上写的,去原型上找,原型上找的顺序也是先 id
,再驼峰,再首字母大写,若是还找不到,那这个vnode就是个空(或者说不认识这个vnode),也就会走到 _createElement
方法的后面判断,也就是经过 new 来建立一个VNode:oop
// unknown or unlisted namespaced elements
// check at runtime because it may get assigned a namespace when its
// parent normalizes children
vnode = new VNode(
tag, data, children,
undefined, undefined, context
)
复制代码
这也就是为何自定义全局组件的时候能够把 id
写成 驼峰,或者 首字母大写 的方式使用。
捋一下,若是是全局自定义组件,就会在大 Vue.options.components 里扩展了一个构造器,接着在初始化建立元素(_createElement)的时候,经过 resolveAsset
传入的 tag
,解析出来一个有关组件标签的定义,而后返回这个构造器,把它传入到 createComponent
里去建立组件的vnode,而后走patch和update过程最终变成一个真实DOM。
局部注册通常会在某个vue文件这样写:
<template>
<Comp />
</template>
<script>
import Comp from "Comp.vue";
export default {
components:{
Comp
}
}
</script>
复制代码
这样就引入了一个局部组件,如今来分析一下它的过程。
回顾一下 Vue.extend
是如何合并 options
的:
const Sub = function VueComponent (options) {
this._init(options)
}
Sub.prototype = Object.create(Super.prototype)
Sub.prototype.constructor = Sub
Sub.cid = cid++
Sub.options = mergeOptions(
Super.options,
extendOptions
)
复制代码
Super.options
是大 Vue.$options,后面的 extendOptions
就是上面例子中的这一块:
export default {
components:{
Comp
}
}
复制代码
把这两个合并到了子组件构造器的 options
上,就是 Sub.options
上,接着在这个Sub初始化的时候,会调用一个 initInternalComponent
方法(也就是调用 _init
里面的方法,代码在 /src/core/instance/init.js
):
if (options && options._isComponent) {
// optimize internal component instantiation
// since dynamic options merging is pretty slow, and none of the
// internal component options needs special treatment.
initInternalComponent(vm, options)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
复制代码
接着看下 initInternalComponent
这个方法:
export function initInternalComponent (vm: Component, options: InternalComponentOptions) {
const opts = vm.$options = Object.create(vm.constructor.options)
// doing this because it's faster than dynamic enumeration.
const parentVnode = options._parentVnode
opts.parent = options.parent
opts._parentVnode = parentVnode
const vnodeComponentOptions = parentVnode.componentOptions
opts.propsData = vnodeComponentOptions.propsData
opts._parentListeners = vnodeComponentOptions.listeners
opts._renderChildren = vnodeComponentOptions.children
opts._componentTag = vnodeComponentOptions.tag
if (options.render) {
opts.render = options.render
opts.staticRenderFns = options.staticRenderFns
}
}
复制代码
里面的 vm.constructor
就是上面说的 Sub
,这样 Sub.options
就能够拿到咱们在页面里写的组件配置,而后赋值给 vm.$options
,因此能够经过 vm.$options.components
拿到页面里定义的组件配置,那在全局注册里的提到的 assets
就能够拿到这个局部注册的组件配置。
注意:因为局部组件的合并配置是扩展到 Sub.options
的,因此引入的这个局部组件只能在当前组件下使用(或者说当前vue页面),而全局注册是扩展到大 Vue.options 下的,也就是会走到 _init
方法的 vm.$options = mergeOptions()
这里:
if (options && options._isComponent) {
// optimize internal component instantiation
// since dynamic options merging is pretty slow, and none of the
// internal component options needs special treatment.
initInternalComponent(vm, options)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
复制代码
因此能够全局使用。
个人公众号:道道里的前端栈,每一天一篇前端文章,嚼碎的感受真奇妙~