本篇文章从最简单的情况入手,不考虑 prop 和组件间通信。
vue 文档告诉我们可以使用
注册一个组件
- Vue.component(tagName, options)
- Vue.component('my-component', {
- // 选项
- })
毫无疑问这是一个全局 API,我们顺着代码最终可以找到 Vue.component 是这样的
- Vue.component = function(id, definition) {
- definition.name = definition.name || id
- definition = Vue.extend(definition)
- this.options[type + 's'][id] = definition
- return definition
- }
Vue.component 实际上是 Vue.extend 的封装,Vue.extend 如下:
- Vue.extend = function (extendOptions: Object): Function {
- extendOptions = extendOptions || {}
- const Super = this
- const isFirstExtend = Super.cid === 0
- if (isFirstExtend && extendOptions._Ctor) {
- return extendOptions._Ctor
- }
- let name = extendOptions.name || Super.options.name
- 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
- )
- Sub['super'] = Super
- // allow further extension
- Sub.extend = Super.extend
- // create asset registers, so extended classes
- // can have their private assets too.
- config._assetTypes.forEach(function (type) {
- Sub[type] = Super[type]
- })
- // enable recursive self-lookup
- 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.
- Sub.superOptions = Super.options
- Sub.extendOptions = extendOptions
- // cache constructor
- if (isFirstExtend) {
- extendOptions._Ctor = Sub
- }
- return Sub
- }
可以看到 Vue.extend 返回的实际上是一个构造函数 Sub,并且此构造函数继承自 Vue。里面有这么几行代码
- Sub.options = mergeOptions(
- Super.options,
- extendOptions
- )
那么 Super.options(即 Vue.options)是什么呢?
- Vue.options = Object.create(null)
- // 包含components directives filters
- config._assetTypes.forEach(type = >{
- Vue.options[type + 's'] = Object.create(null)
- })
- util.extend(Vue.options.components, builtInComponents)
Vue.options 事实上存放了系统以及用户定义的 component、directive、filter,builtInComponents 为 Vue 内置的组件(如 keep-alive),打印看下:
所以 Sub 构造函数的 options 不仅包含 components、directives、filters,还包含预先定义的实例化时所需的选项。定义一个组件如下:
- let MyComponent = Vue.extend({
- data() {
- return {
- msg: "this's compoennt"
- }
- },
- render(h) {
- return h('p', this.msg)
- }
- })
打印 MyComponent.options 如下:
再回过头看 Vue.component,可以发现他做的工作就是扩展一个 Vue 构造函数(VueComponent),并将这个构造函数(VueComponent)添加到 Vue.options.components
现在我们已经可以回答最开始的问题 ---vue 的组件是什么?vue 的组件其实就是扩展的 Vue 构造函数,并且在适当的时候实例化为 Vue 实例。
组件对应的 vnode 是什么样子?从一个简单的例子入手:
- let MyComponent = Vue.component('my-component', {
- data() {
- return {
- msg: "this's component"
- }
- },
- render(h) {
- return h('p', this.msg)
- }
- })
- window.app = new Vue({
- render(h) {
- return h('my-component')
- }
- }).$mount('#root')
上篇文章已经说道在 initRender 的时候会初始一个系统 watcher,如下:
- vm._watcher = new Watcher(vm, () => {
- vm._update(vm._render(), hydrating)
- }, noop)
上篇文章提到 vm._render() 返回的是一个虚拟 dom(vnode),具体到本篇,那么组件标签会被解析成什么样的虚拟节点呢?
事实上 render 的时候会首先调用 createElement,根据传入的 tag(html 标签或者组件标签)不同,vnode 可以分为以下两种:
- else if ((Ctor = resolveAsset(context.$options, 'components', tag))) {
- // component
- return createComponent(Ctor, data, context, children, tag)
- }
这里的 Ctor 就是我们扩展的组件构造函数,createComponent 最终返回的 vnode 如下:
- const vnode = new VNode(
- `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
- data, undefined, undefined, undefined, undefined, context,
- { Ctor, propsData, listeners, tag, children }
- )
需要注意的是 data 有一个操作:
- // merge component management hooks onto the placeholder node
- mergeHooks(data)
merge 之后 data.hook 会添加四个方法:
前文看到组件构造函数实际上是存在组件对应 vnode 的 componentOptions 中,那么究竟是什么时候实例化组件呢?
顺着 vm._update(vm._render(), hydrating) 往下看发现最终调用的是 patch 操作,而对于组件实例化而言并不存在与之对应的 oldVnode(因为 oldVnode 是在组件更新后产生的),所以最终的逻辑归到
,即
- 根据组件对应的vnode创建真实dom节点
- createElm(vnode, insertedVnodeQueue)
我们还记得组件的构造函数是
,其实最终调用的也是这个构造函数。
- vnode.componentOptions.Ctor
createElm 函数中与组件初始化相关的关键代码如下:
- const data = vnode.data
- if (isDef(data)) {
- if (isDef(i = data.hook) && isDef(i = i.init)) i(vnode)
- if (isDef(i = vnode.child)) {
- initComponent(vnode, insertedVnodeQueue)
- return vnode.elm
- }
- }
init 的代码如下:
- function init (vnode: VNodeWithData, hydrating: boolean) {
- if (!vnode.child || vnode.child._isDestroyed) {
- const child = vnode.child = createComponentInstanceForVnode(vnode, activeInstance)
- child.$mount(hydrating ? vnode.elm : undefined, hydrating)
- }
- }
- export function createComponentInstanceForVnode (
- vnode: any, // we know it's MountedComponentVNode but flow doesn't
- parent: any // activeInstance in lifecycle state
- ): Component {
- const vnodeComponentOptions = vnode.componentOptions
- const options: InternalComponentOptions = {
- _isComponent: true,
- parent,
- propsData: vnodeComponentOptions.propsData,
- _componentTag: vnodeComponentOptions.tag,
- _parentVnode: vnode,
- _parentListeners: vnodeComponentOptions.listeners,
- _renderChildren: vnodeComponentOptions.children
- }
- // check inline-template render functions
- const inlineTemplate = vnode.data.inlineTemplate
- if (inlineTemplate) {
- options.render = inlineTemplate.render
- options.staticRenderFns = inlineTemplate.staticRenderFns
- }
- return new vnodeComponentOptions.Ctor(options)
- }
经过 init 之后可以看到组件 vnode.child 对应的就是组件的实例,且 child.$el 即为组件对应的真实 dom,但是实际上 createElm 返回的是 vnode.elm,怎么回事?事实上 initComponent 中做了处理
- vnode.elm = vnode.child.$el
综上,组件实例化是在由虚拟 dom 映射为真实 dom 时完成的。
写到这里已经对组件机制有了初步的认识,数据的传递、父子组件通信本文并没有涉及,留到以后再看。
来源: https://juejin.im/post/5a3f8706f265da432a7bcc3a