「这是我参与2022首次更文挑战的第40天,活动详情查看:2022首次更文挑战」
一、前情回顾 & 背景
上一篇小作文讨论了 Vue 中最复杂的渲染函数的运行时帮助函数 vm._c,我们强调了这个方法与其他的不同,其他的帮助函数如 _l/_t/_s... 都是挂在 Vue.prototype 对象上的公有方法,而 vm._c 是 vm 实例私有的方法;
另外还介绍了 _createElement、resolveAssets 等方法;
这篇小作文就 Vue 在渲染过程中创建自定义组件 VNode 的 createComponent 方法展开讨论;
二、createComponent
方法位置:src/core/vdom/create-component.js -> function createComponent
方法参数:
Ctor,Vue子类,还可以是构造函数或者选项对象;data,组件的data对象,包含props等数据;children,子节点列表;tag,标签名;
方法作用:
- 和并组件
options和根实例options(Vue.options),基于options扩展出用于创建组件的Vue子类构造函数; - 处理异步组件,这里先忽略吧;
- 处理
v-model,将v-model绑定的值添加到data.attrs,把实现双向绑定的运行时事件添加到data.on;data.attrs就是将来真实DOM上的行内属性,例如在<input v-model="inputValue" />会在data.attrs['value'] = inputVlue,而data.on上的事件就是编译时生成的运行时事件代码,监听input的oninput事件获取新值并更新数据; - 提取
propsData,数据源有两个地方,data.attrs和data.props,其中props就是声明组件时props选项; installComponentHooks,为组件的data增加hook对象,包含init、prepatch、insert、destory四个钩子方法- 创建标签名字为
vue-component-${cid}-${name}的VNode,并返回
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
}
// baseCtor 就是 Vue 构造函数本身
const baseCtor = context.$options._base
// 当 Ctor 为配置对象时,通过 Vue.extend 将其转换为构造函数
// 所以能看出来,子组件的构造函数是 Vue 的子类
// plain options object: turn it into a constructor
if (isObject(Ctor)) {
Ctor = baseCtor.extend(Ctor) // Vue.extend()
}
// 到此为止,如果 Ctor 仍不是一个函数,则表示这是个无效的组件定义
if (typeof Ctor !== 'function') {
return
}
// 异步组件
// async component
let asyncFactory
if (isUndef(Ctor.cid)) {
asyncFactory = Ctor
Ctor = resolveAsyncComponent(asyncFactory, baseCtor)
if (Ctor === undefined) {
// 异步组件返回一个占位符节点,组件被渲染为注释节点,原始信息,
// 这些信息将用于异步渲染和合成
// 先管同步的吧,异步的先放放
return createAsyncPlaceholder(
asyncFactory,
data,
context,
children,
tag
)
}
}
// 节点属性 JSON 字符串
data = data || {}
// 这里其实就是组件做选项合并的地方,即编译器将组件编译为渲染函数,
// 渲染时执行 render 函数如果遇到自定义组件,然后执行其中的 _c,就会走到这里了
// 解析构造函数选项,合并基类选项,合并过程中会比对两次基类的 options 和 缓存的 options 比较,
// 以防止在组件构造函数创建后引用全局混入丢失掉新混入的信息
resolveConstructorOptions(Ctor)
// 将组件的 v-model 的信息(值和回调)转换为 data.attrs 对象的属性
// data.on 对象上的事件、回调
if (isDef(data.model)) {
transformModel(Ctor.options, data)
}
// 提取 props 数据,得到 propsData 对象,propsData[key] = val
// 声明组件时 props 配置中的属性为 key,父组件对应的数据为 value
const propsData = extractPropsFromVNodeData(data, Ctor, tag)
// 函数式组件
if (isTrue(Ctor.options.functional)) {
return createFunctionalComponent(Ctor, propsData, data, context, children)
}
// 获取事件监听器 data.on,因为这些监听器需要作为子组件的自定义事件监听器处理,
// 而不是 DOM 监听器
const listeners = data.on
// 将带有 .native 修饰符的事件对象赋值给 data.on,这样就可以再父组件 patch 的时候被处理
data.on = data.nativeOn
// 如果是抽象组件,则保留 props、listeners 和 slot
if (isTrue(Ctor.options.abstract)) {
const slot = data.slot
data = {}
if (slot) {
data.slot = slot
}
}
// 在组件的 data 对象上设置组件管理的 hook 对象
// hook 对象增加四个属性:
// init: 组件创建
// prepatch:组件更新
// insert: 组件被挂载到父组件
// destroy: 组件销毁
// 这些方法在组件 patch 阶段会被调用
installComponentHooks(data)
// 返回占位符节点
// return a placeholder vnode
const name = Ctor.options.name || tag
// 实例化组件的 VNode,
// 对于普通组件标签名: vue-component-${cid}-${name}
const vnode = new VNode(
`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
data,
undefined,
undefined,
undefined,
context,
{ Ctor, propsData, listeners, tag, children },
asyncFactory
)
return vnode
}
2.1 baseCtor.extend
baseCtor 就是 Vue 构造函数自己;
方法位置:src/core/global-api/extend.js -> function initExtend -> Vue.extend
方法参数:Vue.extend 的参数,extendOptions,需要扩展的选项对象;
方法作用:基于 Vue 自身根据 extendOptions 选项生成 Vue 的一个子类,当然子类还可以继续 extend 出孙子类。
子类的 options 是 Vue.options 和 extendOptions 通过 mergeOptions 合并后的结果;所以,这样一来子类就复用了父类的能力,最简单的就是全局的组件、指令、过滤器就是通过这种方式实现每个子组件都能够访问到的。
export function initExtend (Vue: GlobalAPI) {
Vue.cid = 0
let cid = 1
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]
}
// extendOptions 就是我们声明组件时创建的选项对象
const name = extendOptions.name || Super.options.name
if (process.env.NODE_ENV !== 'production' && name) {
validateComponentName(name)
}
// 定义 Sub 构造函数,和 Vue 构造函数一样
const Sub = function VueComponent (options) {
this._init(options)
}
// 通过原型继承的方式继承 Vue,所以 this._init 就有了
Sub.prototype = Object.create(Super.prototype)
Sub.prototype.constructor = Sub
Sub.cid = cid++
// 选项合并,合并 Vue 的配置项到自己的配置项,实现复用 Vue 类的能力
Sub.options = mergeOptions(
Super.options,
extendOptions
)
// 记录自己的基类
Sub['super'] = Super
// 下面的将 props、computed 属性代理到扩展原型对象上,
// 这能避免在实例创建时 Object.defineProperty 调用
// 初始化 props, 将 props 配置代理到 Sub.prototype._props 对象上
if (Sub.options.props) {
initProps(Sub)
}
// 初始化 computed,将 computed 配置代理到 Sub.prototype 对象上
if (Sub.options.computed) {
initComputed(Sub)
}
// 定义 extend、mixin、use 这三个静态方法,允许在 Sub 基础上进一步构造子类
Sub.extend = Super.extend
Sub.mixin = Super.mixin
Sub.use = Super.use
// 定义 component/filter/directive 三个静态方法
// 子类就可以有自己的 component、filter、directive 方法了;
// 给子类加三个声明全局组件、过滤器;、指令的方法
ASSET_TYPES.forEach(function (type) {
Sub[type] = Super[type]
})
// enable recursive self-lookup
// 递归组件的原理,如果组件设置了 name 属性,则将自己注册到自己的 components 选项中
if (name) {
Sub.options.components[name] = Sub
}
// 在扩展时保留对基类选项的引用
// 稍后在实例化时,我们可以检查 Super 的选项是否具有更新,如果有更新要取用最新的选项
Sub.superOptions = Super.options
Sub.extendOptions = extendOptions
Sub.sealedOptions = extend({}, Sub.options)
// 缓存
cachedCtors[SuperId] = Sub
return Sub
}
}
我们的每一个子组件都会有自己的子类,当他被渲染的时候,就会初始这个子类的实例来完成各种初始化、数据响应式、编译模板并挂载。如果子组件还有子组件,那么这个过程是个递归的,直到最深的一级完成挂载;
2.2 transformModel
方法位置:src/core/vdom/create-component.js -> function transformModel
方法参数:
options,Ctor.options是经过合并后的选项对象,如果是子组件,这个options还包含了父类的optionsdata, 使用v-model指令的元素的data对象;
方法作用:将组件的 v-model 绑定的属性信息变成 data.attrs 上的属性;运行时的事件监听器变成 data.on 上的属性;v-model 默认绑定的是 value 属性,事件默认是 input 事件;
function transformModel (options, data: any) {
const prop = (options.model && options.model.prop) || 'value'
const event = (options.model && options.model.event) || 'input'
;(data.attrs || (data.attrs = {}))[prop] = data.model.value
const on = data.on || (data.on = {})
const existing = on[event]
const callback = data.model.callback
if (isDef(existing)) {
if (
Array.isArray(existing)
? existing.indexOf(callback) === -1
: existing !== callback
) {
on[event] = [callback].concat(existing)
}
} else {
on[event] = callback
}
}
2.3 extractPropsFromVNodeData
方法位置:src/core/vdom/helpers/extract-props.js -> function extractPropsFromVNodeData
方法参数:
data, 虚拟DOM的data对象,就是前面generate得到的data,在运行时里就是对象了;Ctor,构造函数;tag:标签名;
方法作用:从 data 的 attrs 和 props 中提取出最终的 props 对象,key 是 props 中的 key,值有可能来自 attrs 也可能来自 props;
export function extractPropsFromVNodeData (
data: VNodeData,
Ctor: Class<Component>,
tag?: string
): ?Object {
// 组件的 props 选项,{ props: { someKey: { type: Object, default () { return { a: 'ddddd'} } } }
// 这里只提取原始值,验证默认值和子组件中处理
const propOptions = Ctor.options.props
if (isUndef(propOptions)) {
return
}
// 以 props 配置中的属性为 key,父组件传递下来的值为 value
// 当父组件中数据更新时,触发响应式更新,重新执行 render,生成新的 vnode,又走到这里
// 这样子子组件中相应的数据就会被更新
const res = {}
const { attrs, props } = data
if (isDef(attrs) || isDef(props)) {
// 遍历 propsOptions
for (const key in propOptions) {
// 将小驼峰形式 key 转成 连字符
const altKey = hyphenate(key)
if (process.env.NODE_ENV !== 'production') {
// 提示,props 为小驼峰,html 不区分大小写,要用连字符替换驼峰
}
// 从 props 和 attrs 中取值,props 要保留,attrs 不用保留
checkProp(res, props, key, altKey, true) ||
checkProp(res, attrs, key, altKey, false)
}
}
return res
}
- checkProp 方法,尝试从 attrs 或者 props 中取值,如果 preserve 为 false 的时候从对象中删除掉;
function checkProp (
res: Object,
hash: ?Object,
key: string,
altKey: string,
preserve: boolean
): boolean {
if (isDef(hash)) {
// 判断 hash (props、attrs)对象中是否存在 key 或者 altKey
// 存在则设置给 res => res[key] = hash[key]
if (hasOwn(hash, key)) {
res[key] = hash[key]
if (!preserve) {
delete hash[key]
}
return true
} else if (hasOwn(hash, altKey)) {
res[key] = hash[altKey]
if (!preserve) {
delete hash[altKey]
}
return true
}
}
return false
}
2.4 installComponentHooks
方法位置:src/core/vdom/create-component.js -> function installComponent
方法参数:data, 组件的 data 对象
方法作用:为组件的 data 对象增加 hook 对象,hook 中有四方法:
init,组件初始化时调用,这个方法将在Vue.prototype._render方法中调用,相当于创建Vue实例prepatch,更新VNode时调用insert,执行组件的mounted生命周期钩子destroy,销毁组件
function installComponentHooks (data: VNodeData) {
const hooks = data.hook || (data.hook = {})
// 遍历 hooksToMerge = ['init', 'prepatch', 'insert', 'destroy']
for (let i = 0; i < hooksToMerge.length; i++) {
// 比如 key = init
const key = hooksToMerge[i]
// 从 data.hook 对象中获取 key 对应的方法
const existing = hooks[key]
// 合并用户传递的 hook 方法和框架自带的 hook 方法,支持多个 hook 方法
const toMerge = componentVNodeHooks[key]
if (existing !== toMerge && !(existing && existing._merged)) {
hooks[key] = existing ? mergeHook(toMerge, existing) : toMerge
}
}
}
2.5 componentVNodeHooks
在组件的 patch 期间会调用这些钩子,实现组件的初始化、更新、挂载和销毁,在稍晚的小作文中就会看到 init 的执行,init 执行就是让子组件重新走一遍 Vue 实例的初始化过程;
const componentVNodeHooks = {
// 初始化
init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
if (
vnode.componentInstance &&
!vnode.componentInstance._isDestroyed &&
vnode.data.keepAlive
) {
// 被 keep-alive 包裹的组件
// kept-alive components, treat as a patch
const mountedNode: any = vnode // work around flow
componentVNodeHooks.prepatch(mountedNode, mountedNode)
} else {
// 创建组件实例,即 new vnode.componentOptions.Ctor(options) => 得到 Vue 组件实例
const child = vnode.componentInstance = createComponentInstanceForVnode(
vnode,
activeInstance
)
// 执行组件的 $mount 方法,进入挂载阶段,接下来就是通过编译器得到 render 函数,接着走挂载、patch 这条路直到组件渲染到页面
child.$mount(hydrating ? vnode.elm : undefined, hydrating)
}
},
// 更新 VNode,用新的 VNode 配置更新就的 VNode
prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
// 新的 VNode,用新的 VNode 配置更新就的 VNode 上的各种属性
const options = vnode.componentOptions
// 老的 VNode 组件的组件实例
const child = vnode.componentInstance = oldVnode.componentInstance
// 用 vnode 上的属性更新 child 上的各种属性
updateChildComponent(
child,
options.propsData, // updated props
options.listeners, // updated listeners
vnode, // new parent vnode
options.children // new children
)
},
// 执行组件的 mounted 生命周期钩子
insert (vnode: MountedComponentVNode) {
const { context, componentInstance } = vnode
// 如果组件未挂载,则调用组件的 mounted 生命周期钩子
if (!componentInstance._isMounted) {
componentInstance._isMounted = true
callHook(componentInstance, 'mounted')
}
// 处理 keep-alive 组件的异常情况
if (vnode.data.keepAlive) {
if (context._isMounted) {
queueActivatedComponent(componentInstance)
} else {
activateChildComponent(componentInstance, true /* direct */)
}
}
},
// 销毁组件:
// 1. 如果组件被 keep-alive 组件包裹,则组件失活,并不销毁组件实例,从而缓存组件状态
// 2. 如果组件未被 keep-alive 组件包裹,则直接调用实例的 $destroy 方法销毁组件实例
destroy (vnode: MountedComponentVNode) {
const { componentInstance } = vnode
if (!componentInstance._isDestroyed) {
if (!vnode.data.keepAlive) {
componentInstance.$destroy()
} else {
deactivateChildComponent(componentInstance, true /* direct */)
}
}
}
}
2.6 VNode
类的位置:src/core/vdom/vnode.js -> class VNode
构造函数参数:
tag, 标签名,自定义组件的标签为vue-component-${cid}-${name};data,data对象;children,子节点列表;elm:元素节点;context:上下文,Vue实例或者组件实例;componentOptions:组件选项;asyncFactory:异步组件工厂函数;
类的作用:创建 VNode 实例,VNode 实例中包含了元素节点所需的全部信息,并绑定了对应的组件实例;
export default class VNode {
tag: string | void;
data: VNodeData | void;
children: ?Array<VNode>;
text: string | void;
elm: Node | void;
ns: string | void;
context: Component | void; // rendered in this component's scope
key: string | number | void;
componentOptions: VNodeComponentOptions | void;
componentInstance: Component | void; // component instance
parent: VNode | void; // component placeholder node
// strictly internal
raw: boolean; // contains raw HTML? (server only)
isStatic: boolean; // hoisted static node
isRootInsert: boolean; // necessary for enter transition check
isComment: boolean; // empty comment placeholder?
isCloned: boolean; // is a cloned node?
isOnce: boolean; // is a v-once node?
asyncFactory: Function | void; // async component factory function
asyncMeta: Object | void;
isAsyncPlaceholder: boolean;
ssrContext: Object | void;
fnContext: Component | void; // real context vm for functional nodes
fnOptions: ?ComponentOptions; // for SSR caching
devtoolsMeta: ?Object; // used to store functional render context for devtools
fnScopeId: ?string; // functional scope id support
constructor (
tag?: string,
data?: VNodeData,
children?: ?Array<VNode>,
text?: string,
elm?: Node,
context?: Component,
componentOptions?: VNodeComponentOptions,
asyncFactory?: Function
) {
this.tag = tag
this.data = data
this.children = children
this.text = text
this.elm = elm
this.ns = undefined
this.context = context
this.fnContext = undefined
this.fnOptions = undefined
this.fnScopeId = undefined
this.key = data && data.key
this.componentOptions = componentOptions
this.componentInstance = undefined
this.parent = undefined
this.raw = false
this.isStatic = false
this.isRootInsert = true
this.isComment = false
this.isCloned = false
this.isOnce = false
this.asyncFactory = asyncFactory
this.asyncMeta = undefined
this.isAsyncPlaceholder = false
}
get child (): Component | void {
return this.componentInstance
}
}
三、总结
本篇小作文讲述了 Vue 处理自定义组件产生 VNode 的方法 createComponent;这里最需要理解的就是子组件不是由 Vue 直接创建的,而是由 Vue 的子类创建的;
我们日常的开发,以写 .vue 文件为例,在 script 部分,我们导出的其实就是一个选项对象,但是即便写多个组件,他们之间互不影响,这就是因为多个组件是多个实例,实例之间是天然隔离的。既然说是实例,那么就是由构造函数创建,这个构造函数就是通过咱们写的这个选项对象扩展得来的子类;
还有一个重点就是每个组件都会有自己的 hook 对象,这个对象后四个方法 init、prepatch、insert、destroy 四个钩子方法,其中 init 负责子组件实例的初始化,这个过程相当于 new Vue 的全流程,包含子组件的模板编译、渲染函数的创建过程;