重学Vue源码,根据黄轶大佬的vue技术揭秘,逐个过一遍,巩固一下vue源码知识点,毕竟嚼碎了才是自己的,所有文章都同步在 公众号(道道里的前端栈) 和 github 上。
正文
在开发过程中,自定义组件必须先注册才可以使用,如果直接使用的话,会报一个错:未知的自定义元素,就像下面这样:
'Unknown custom element: <xxx> - did you register the component correctly?
For recursive components, make sure to provide the "name" option.'
在vue中提供了2种组件注册的方式:全局注册 和 局部注册,下面来把它们分析一下。
全局注册
全局注册一个组件,一般会在 main.js 中这样写:
Vue.component("comp-name", {
// options
})
使用了一个 Vue.component 函数来注册,这个函数的定义过程是在最开始初始化Vue的全局函数的时候,代码在 src/core/global-api/assets.js 中:
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 上。
由于在Vue初始化的时候会调用一个 _createElement 方法(它在render函数渲染和创建元素的时候分析过,可以看这篇createElement做了什么),在这个方法里有这样一段代码:
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:
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:
// 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
)
}
所以可以全局使用。
我的公众号:道道里的前端栈,每一天一篇前端文章,嚼碎的感觉真奇妙~