一、组件化
-
Vue.js
另一个核心思想是组件化。所谓组件化,就是把页面拆分成多个组件(component)
,每个组件依赖的CSS、JavaScript
、模板、图片等资源放在一起开发和维护。组件是资源独立的,组件在系统内部可复用,组件和组件之间可以嵌套。 -
我们在用
Vue.js
开发实际项目的时候,就是像搭积木一样,编写一堆组件拼装生成页面。在Vue.js
的官网中,也是花了大篇幅来介绍什么是组件,如何编写组件以及组件拥有的属性和特性。我们将从源码的角度来分析Vue
的组件内部是如何工作的,只有了解了内部的工作原理,才能让我们使用它的时候更加得心应手。 -
接下来我们会用
Vue-cli
初始化的代码为例,来分析一下Vue
组件初始化的一个过程,如下所示:
import Vue from 'vue'
import App from './App.vue'
var app = new Vue({
el: '#app',
// 这里的 h 是 createElement 方法
render: h => h(App)
})
和之前相同的点也是通过
render
函数去渲染的,不同的这次通过createElement
传的参数是一个组件而不是一个原生的标签,那么接下来我们就开始分析这一过程。
二、createComponent 的理解
- 我们之前在分析
createElement
的实现的时候,它最终会调用_createElement
方法,其中有一段逻辑是对参数tag
的判断,如果是一个普通的html
标签,是一个普通的 div,则会实例化一个普通VNode
节点,否则通过createComponent
方法创建一个组件VNode
,如下所示:
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)
}
- 在这里传入的是一个
App
对象,它本质上是一个Component
类型,那么它会走到上述代码的else
逻辑,直接通过createComponent
方法来创建vnode
。所以接下来我们来看一下createComponent
方法的实现,它定义在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
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, context)
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
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
installComponentHooks(data)
// return a placeholder vnode
const name = Ctor.options.name || tag
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
}
-
从上面可以看到,
createComponent
的逻辑也会有一些复杂,但是分析源码比较推荐的是只分析核心流程,分支流程可以之后针对性的看,所以这里针对组件渲染这个 case 主要就三个关键步骤:
构造子类构造函数,安装组件钩子函数和实例化vnode
。 -
构造子类构造函数,如下所示:
const baseCtor = context.$options._base
// plain options object: turn it into a constructor
if (isObject(Ctor)) {
Ctor = baseCtor.extend(Ctor)
}
我们在编写一个组件的时候,通常都是创建一个普通对象,还是以我们的 App.vue 为例,代码如下:
import HelloWorld from './components/HelloWorld'
export default {
name: 'app',
components: {
HelloWorld
}
}
- 这里
export
的是一个对象,所以createComponent
里的代码逻辑会执行到baseCtor.extend(Ctor)
,在这里baseCtor
实际上就是Vue
,这个的定义是在最开始初始化Vue
的阶段,在src/core/global-api/index.js
中的initGlobalAPI
函数有这么一段逻辑:
// this is used to identify the "base" constructor to extend all plain-object
// components with in Weex's multi-instance scenarios.
Vue.options._base = Vue
- 这里定义的是
Vue.options
,而我们的createComponent
取的是context.$options
,实际上在src/core/instance/init.js
里Vue
原型上的_init
函数中有这么一段逻辑:
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {
},
vm
)
-
这样就把
Vue
上的一些option
扩展到了vm.$options
上,所以我们也就能通过vm.$options._base
拿到 Vue 这个构造函数了。mergeOptions
的实现我们会在后面具体分析,现在只需要理解它的功能是把Vue
构造函数的options
和用户传入的options
做一层合并,到vm.$options
上。 -
在了解了
baseCtor
指向了Vue
之后,我们来看一下Vue.extend
函数的定义,在src/core/global-api/extend.js
中,如下所示:
/**
* Class inheritance
*/
Vue.extend = function (extendOptions: Object): Function {
extendOptions = extendOptions || {
}
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)
}
const Sub = function VueComponent (options) {
this._init(options