如果您是刚开始准备阅读Vue.js的源码,建议先看一下本系列的Vue.js源码学习 —— 起步,相信会对您后面的阅读有很大帮助。
如果您才开始准备看Vue.js中虚拟DOM相关的内容,建议先看一下本系列的Vue.js源码学习 —— VNode/createElement,再来看今天这篇会轻松很多。
今天这篇文章会主要看Vue.js中组件是如何创建的,以及异步组件是如何实现的。要看的代码主要位于文件core/vdom/create-component.js
中和文件core/vdom/resolve-async-component.js
中。
我没有在文章中大量的贴源码,一定要将源码clone到本地哦。
createComponent
在Vue.js源码学习 —— VNode/createElement中讲createElement
的实现时,当tag
是自定义的组件名称时,或是其他情况时会调用createComponent
创建一个组件返回。比如下面这样:
1 | // render vnode with component |
这也是为什么在Vue中可以使用自定义的元素,像使用HTML内置的元素(div
、span
这些)一样,因为你在创建的节点是虚拟节点,Vue还会对其进行一次解析,最后通过patch
才转换成最终的真实节点。
创建组件的过程必然要比创建一个简单的元素节点处理的事情多,因为还要管理组件的生命周期,Vue还支持异步创建组件等等。下面就来看看createElement
的实现,在阅读代码时遇到不理解的地方,记得去看一下对它的测试用例,主要位于文件create-component.spec.js
中。
参数
1 | export function createComponent ( |
Ctor
:可以看成是组件的构造器,Ctor
中存了与组件相关的信息。从它的类型注解上可以看出,该参数允许传Class<Component>
或Function
或Object
,或者是空的。Class<Component>
:这是在使用Vue Class Component这个库时用到的。这个库允许开发者用Class
的形式类定义组件,具体的时候可以查阅官方文档。Function
:这里又有两种情况,一种是直接传的Vue的子类构造器,一种是创建异步组件时传的异步函数。先看一下传Vue的子类构造器的情况:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15const VueChild = Vue.extend({
name: 'my-component',
props: ['msg'],
render(h) {
return h('p', `${this.msg}`)
}
}) // Ctor
const vm = new Vue({
el: '#app',
data: { message: 'hello world' },
render (h) {
return h(VueChild, { props: { msg: this.message } }) // data
}
})还有一种情况是传异步函数,这个等说到异步函数那部分时再细说。
Object
: 也可以直接传组件的选项对象。如下例子所示:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15const child = {
name: 'child',
props: ['msg'],
render (h) {
return h('p', `${this.msg}`)
}
}
const vm = new Vue({
el: '#app',
data: { message: 'hello world' },
render (h) {
return h(child, { props: { msg: this.message } }) // data
}
})
data
:节点的数据对象,这个在Vue.js源码学习 —— VNode/createElement中详细说过,这个对象就是描述当前节点的相关信息的。context
:节点所属的实例对象。children
:节点下的所有子节点。tag
:节点的标签名称。
返回
返回值是VNode
类型的值,或是一个VNode
数组。
返回
VNode
数组的情况是函数式组件时,暂时先不讨论这种情况。
创建这个要返回的vnode
时的传参如下:
1 | // return a placeholder vnode |
可以看到createComponent
返回的是组件的虚拟节点vnode
,而并不是通过new Ctor()
创建的组件的实例对象,但是将Ctor
放入到了componentOptions
中,被vnode
携带着,真正执行new Ctor()
其实是在将vnode
转成真实DOM节点,也就是patch
的过程中。
获取组件构造器
1 | // Ctor 是 undefined 或 null 时,直接返回 |
这块要注意的是,在Ctor
是对象时,实际上相当于以Ctor
为选项对象,用Vue.extend
创建了一个Vue的子类构造器:
1 | Ctor = Vue.extend(Ctor) |
最后确保在后面的处理过程中使用的Ctor
都是函数。
异步组件
1 | // async component |
我们上面说了Ctor
是函数的情况有两种,这里依据Ctor.cid
是否有值来判断是哪一种情况。
为什么
Ctor.cid
有值时传的是构造器呢?
不论是传入的构造器还是上面才创建的,都是使用的Vue.extend
,打开文件core/global-api/extend.js
中的代码可以发现下面的代码:
1 | /** |
所以继承Vue的构造函数(包括Vue本身)都有cid
属性。
接下来就是resolveAsyncComponent
函数内部的实现了,这块涉及到了异步组件的实现过程,内容比较多但是却相对独立,我新开了一篇文章来写。
获取最新选项
调用resolveConstructorOptions
函数,以便在Ctor
的父类构造器的选项发生改变时重新获取选项。这个函数在Vue.js源码学习 —— 合并选项前的预处理中有详细说过。
transformModel
如果开发者在data
中传了model
字段,就调用transformModel
函数将组件的v-model
信息(value
和 callback
) 转成props
和 events
。这是什么意思呢?为什么要这么做呢?
还是通过例子来看:
1 | const checkbox = { |
上面的例子中通过model
选项定义了checkbox
组件的v-model
指令,model.prop
是v-model
指令得到的值,model.event
是v-model
指令要触发的事件名称。on.click
在input
元素被点击时会被触发,这个方法调用了$emit
来触发model.event
事件。这样就把值传出去了。
看一下如何使用checkbox
组件的v-model
指令:
1 | new Vue({ |
如果上面的Vue
实例中用render
而不用template
渲染组件时,该如何使用checkbox
的v-model
指令呢?这时就需要用到data.model
了,如下所示:
1 | new Vue({ |
上面Vue
实例的checked
的值可以传递给checkbox
组件,在checkbox
组件选中状态改变时,上面的callback
也会被触发。
会有这样的效果就是因为在transformModel
函数中,对data.model
进行了如下转换:
1 | data: { |
如果原来的节点数据对象中就有
on.change
,会将callback
与之合并为一个数组。
attrs
中的各项是普通的 HTML attribute,所以这里相当于是给input
元素的checked
属性赋值。
on
中是事件监听器,这里有一个change
事件的监听,所以能够收到$emit('change', event.target.checked)
发出的信息。
所以上面的例子可以直接改成使用attrs
和on
:
1 | new Vue({ |
上面分析的其实是如何自定义组件v-model
行为,那如果在组件中没有明确给出model
选项,默认使用的model
选项如下,prop
为value
,event
为input
。
1 | model: { |
因为在transformModel
函数中有这样一段代码:
1 | const prop = (options.model && options.model.prop) || 'value' |
对于checkbox
组件只能明确给出model
选项,如果是普通的输入框直接用默认的model
选项就可以了,举个例子看一下:
1 | const inputText = { |
extractPropsFromVNodeData
extractPropsFromVNodeData
这个函数主要是用来从节点的数据对象(vnodedata
)中取props
和attrs
中的值,然后赋值给组件的props
选项中对应的属性。看个例子就明白了:
1 | const child = { |
经过extractPropsFromVNodeData
函数后,得到propsData
如下:
1 | { |
checkProp
函数用key
找了一次,没找到又用altKey
找了一次,目的就是如果组件中的props
的key
是驼峰形式,但是在给组件传值时可以用以-
连接的形式,像例子中的my-blog
一样。
这样就实现了给组件传值的目的。得到的propsData
会作为componentOptions
中的一项。
函数式组件
如果组件构造器选项中设置了functional = true
,就会调用createFunctionalComponent
函数创建一个函数式组件返回。
关于createFunctionalComponent
函数的实现暂时先放下了,后面找时间再补上。
listeners
接着到下面这段代码:
1 | // extract listeners, since these needs to be treated as |
listeners
也会作为componentOptions
中的一项,最后在初始化组件实例时会用到。关于data.on
和data.nativeOn
可以看一下本系列的Vue.js源码学习 —— 事件机制,详细介绍了它们的使用场景。
抽象组件
官方文档中没有找到与抽象组件有关的介绍,但是从Ctor.options.abstract
可以看出,abstract
这个选项是可以公开给开发者使用的。在Vue内置的组件<transition>
、<keep-alive>
等等都会看到abstract: true
,也就是说这些组件是抽象的。
抽象组件跟正常的组件很像,除了它们不会渲染内容到DOM中。抽象组件的作用是给已有组件添加一些行为。
所以下面这段代码先清空了data
,如果有slot
,再重新设置会data
中:
1 | if (isTrue(Ctor.options.abstract)) { |
但目前看到的是只保留了
slot
在data
中,props
和listeners
可能就是最后放在componentOptions
中的,后面看代码时看看能否找到答案。
installComponentHooks
installComponentHooks
这个函数做的事情就是把开发者放在data
中的hook
与内部的hook
进行合并,最后放回到data.hook
中。合并操作主要是在mergeHook
函数中完成的:
1 | // f1 toMerge |
可以看到,是返回了一个新函数,在这个函数中先调用了内部的钩子函数,再调用用户传进来的钩子函数。
这里的钩子函数是谁提供的呢?会在什么时候被调用呢?
在componentVNodeHooks
中给了一行注释,并且定义了所有钩子函数:
1 | // inline hooks to be invoked on component VNodes during patch |
从注释上可知,这些钩子函数在给组件的vnode
打补丁(patch
)期间会被调用,后面讲到patch.js
时会看到的。
当然,开发者也有机会在打补丁的各个时期执行一些事情,通过在data
中加上hook
就可以,如下所示:
1 | let child = { |
在浏览器中跑一下会发现,init
先被调用了,然后insert
被调用了,其他钩子函数还没有得到执行。
接下来就看看内部的钩子函数中都做了什么。
init
1 | init (vnode: VNodeWithData, hydrating: boolean): ?boolean { |
从上面的if
判断可以出,如果组件实例已经存在并且没有被销毁,并且是<keep-alive>
组件时,会调用prepatch
函数更新页面。如果不是上面的情况会使用createComponentInstanceForVnode
函数重新创建组件实例。
再看一下createComponentInstanceForVnode
函数的实现:
1 | export function createComponentInstanceForVnode ( |
这里用于创建实例对象的构造器Ctor
,就是在createComponent
函数中最后返回vnode
时放在componentOptions
中的。createComponentInstanceForVnode
函数中收集了要传入Ctor
的选项,同时_parentVnode.componentOptions
中存放着propsData
、listeners
等内容,最后使用new vnode.componentOptions.Ctor(options)
创建了组件的实例对象,在Vue.prototype._init
函数中就可以使用到propsData
、listeners
等这些值了。
这里出现了inlineTemplate
,这个是在实现内联模板时用到的。我们现在给上面例子中的Vue
实例的render
中加上inlineTemplate
字段:
1 | new Vue({ |
在浏览器中运行后,你会发现Hello World
被渲染出来了,child
中原来的内容没有渲染。这肯定是优先使用了inlineTemplate.render
,而没有使用child
中的render
,这是怎么做到的呢?那child
中用户传的这些选项现在又放在哪了呢?
答案就在这个组件实例的初始化过程中。你可能已经注意到了createComponentInstanceForVnode
中出现了_isComponent: true
了,我们在看选项合并那部分时也遇到了_isComponent
,现在就能跟前面的接上了。
现在打开core/instance/init.js
文件,还记得下面这段代码吗?
1 | if (options && options._isComponent) { |
在初始化内部创建的组件实例时,选项的处理有所不同,上面也看到了options
是createComponentInstanceForVnode
函数中收集到传过来的,不存在初始化一般实例对象
时的那些问题。
在initInternalComponent
函数中,有下面这样一行代码:
1 | const opts = vm.$options = Object.create(vm.constructor.options) |
child
中用户传的选项一开始是与Vue.options
进行了合并后,放在了Ctor.options
中的(也就是vm.constructor.options
)。现在通过上面这样代码把构造器中的选项直接放在了实例选项的原型上。然后把从createComponentInstanceForVnode
函数中传过来的options
直接放在了vm.$options
中,同时componentOptions
中的各项也会放到vm.$options
中。这就解答了刚刚问题,vm.$options.render
会比vm.$options.__proto__.render
被优先使用。
至此,文件core/instance/init.js
中的代码也全部看完了。
其他钩子函数
prepatch
:主要用来更新组件的。insert
:主要是给组件实例对象比较上_isMounted
为true
,同时去执行用户设置的mounted
钩子选项。对于<keep-alive>
组件会有特殊处理。destroy
:如果不是<keep-alive>
组件就调用$destroy()
进行销毁操作,否则就调用deactivateChildComponent
函数将组件变为非活跃状态。
这几个钩子中用到的主要函数几乎都来自文件core/instance/lifecycle.js
中,所以等到分析这个文件的时候再讲他们到底干了什么。这几个函数如下:
1 | import { |
还有一个queueActivatedComponent
函数,来自core/observer/scheduler
,等看到observer
部分时再说。
创建组件的过程就说完了,相信你会对Vue公开给开发者使用的实例方法$createElement
有更深的理解,尤其是对vnodedata
的理解。最后在创建vnode
时传了很多值,我们目前还没有看到这些参数到底有什么用,接一下看patch
部分的代码应该会得到答案。
在创建组件的过程中,涉及到了两种特殊组件,一种是异步组件,对于异步组件实现的分析在Vue.js源码学习 —— 异步组件的实现这篇文章中。还有一种是函数式组件,这个找时间再看吧,跟我们后面要分析的内容关联不是很大。
本文作者:意林
本文链接:http://shinancao.cn/2020/01/15/Vue-Source-Code-04/
版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-SA 3.0 许可协议。转载请注明出处!