组件系统是Vue整个框架中非常重要的一环,涉及的内容也比较多。这里笔者还是老样子,梳理组件系统的主流程,了解Vue当中的组件系统是如何运转的。
在模版编译以及挂载中我们知道,最终的模板都会解析编译成render函数。如果你是手动写render函数时,你一定少不了使用createElement函数。createElement函数,对应render中的参数h,用来手动创建VNode(最终都是形成VNode,然后再前后两个VNode进行diff对比并更新DOM)。举个应用案例:
import Vue from 'vue'
import App from './App.vue'
// 带有el参数会最终执行render渲染。src/core/instance/render.js
var app = new Vue({
el: '#app',
// 这里的 h 是 createElement方法
// App是对应的组件
render: h => h(App)
})
tag 做判断,如果是 string 类型。
src/core/vdom/create-element.js
如果是非 string 类型
,createComponent同上。
if (isObject(Ctor)) {
Ctor = Vue.extend(Ctor)
}
// Vue.extend 的作用就是构造一个 Vue 的子类
// 把一个纯对象转换一个继承于 Vue 的构造器 Sub 并返回
Vue.extend = function (extendOptions: Object): Function {
const Super = this // this === Vue
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
}
// 往 data.hook中注入钩子函数,在vnode patch阶段会调用
installComponentHooks(data)
const name = Ctor.options.name || tag
// 注意:组件的VNode没有children字段,有的是componentOptions.Ctor
const vnode = new VNode(
`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
data, undefined, undefined, undefined, context,
{ Ctor, propsData, listeners, tag, children },
asyncFactory
)
return vnode
以上可以知道,在编译阶段遇到是组件时,最终也是编译成VNode节点,只不过它会额外做一些处理,比如拿到组件子类Ctor、传递propsData、设置listeners、设置data.hook等,并把这些内容作为参数,传入进 VNode.componentOptions
,等待patch阶段生成真正的DOM组件。
在Vue2.x源码分析 - 模版编译以及挂载讲到,patch阶段会把VNode转换成真正的 DOM 节点。patch(oldVNode, vnode)
的过程(如果只考虑组件初始化渲染的话),会调用 createElm(vnode)
,根据vnode类型创建真实DOM元素。'src/core/vdom/patch.js'
function patch (oldVnode, vnode, hydrating, removeOnly) {
// 1. 新vnode为空,销毁老vnode
if (isUndef(vnode)) {
if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
return
}
if (isUndef(oldVnode)) {
// 2. 新vnode,没有老vnode,直接根据新的VNode创建真实DOM(初始化渲染都走这流程)
createElm(vnode, insertedVnodeQueue)
} else {
// 新老vnode都有
if (sameVnode(oldVnode, vnode)) {
// 3. 有相同的type和key,patch对比更新
patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly)
} else {
// 4. vnode数据结构不同,直接创建新vnode,销毁老vnode
// 根据新vnode,创建真实dom
createElm(vnode)
// 根据老vnode,销毁dom
if (isDef(parentElm)) {
removeVnodes(parentElm, [oldVnode], 0, 0)
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode)
}
}
}
return vnode.elm
}
function createElm (
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
) {
// 1. 如果是组件vnode,初始化组件new SubVue(options)
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
// 以下都是非组件,包括:dom vnode/文本 vnode/注释 vnode
const data = vnode.data
const children = vnode.children
const tag = vnode.tag
// 2. 如果有tag标签,表明是dom vnode
if (isDef(tag)) {
vnode.elm = nodeOps.createElement(tag, vnode)
// 处理children,childVNode执行createElm,递归。
createChildren(vnode, children, insertedVnodeQueue)
insert(parentElm, vnode.elm, refElm)
} else if (isTrue(vnode.isComment)) {
// 3. 注释类型
vnode.elm = nodeOps.createComment(vnode.text)
insert(parentElm, vnode.elm, refElm)
} else {
// 4. 文本类型
vnode.elm = nodeOps.createTextNode(vnode.text)
insert(parentElm, vnode.elm, refElm)
}
}
后面还有createChildren/updateChildren等流程,核心还是依据前后两个VNode进行diff算法,再更新真实DOM。以下贴出笔者vue patch流程的思维导图:
// 调用钩子,即找到vm.$options[hook]执行
export function callHook (vm: Component, hook: string) {
const handlers = vm.$options[hook]
if (handlers) {
for (let i = 0, j = handlers.length; i < j; i++) {
handlers[i].call(vm)
}
}
// 程序化的事件侦听器:https://cn.vuejs.org/v2/guide/components-edge-cases.html#%E7%A8%8B%E5%BA%8F%E5%8C%96%E7%9A%84%E4%BA%8B%E4%BB%B6%E4%BE%A6%E5%90%AC%E5%99%A8
if (vm._hasHookEvent) { // Vue.prototype.$on:vm._hasHookEvent = /^hook:/.test(event)
vm.$emit('hook:' + hook)
}
}
// new Vue({options})时执行this._init()
Vue.prototype._init = function (options?: Object) {
// ...
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')
// ...
}
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
// ...
callHook(vm, 'beforeMount')
let updateComponent = () => {
// vm._render得到VNode
// vm._update真正去更新DOM
vm._update(vm._render(), hydrating)
}
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
hydrating = false
// vm.$vnode 如果为 null,则表明这不是一次组件的初始化过程,而是通过外部 new Vue 初始化过程。
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}
component
:使用Vue.extend拿到Vue子类,挂载到 Vue.options.components上。当有组件注册后,即可根据注册的名字,找到组件定义(组件可理解为一个Object对象)。directive/filter
,挂载到Vue.options.directives/filters上注册组件,重点是把 components 合并到 vm.$options.components 上
,这样我们就可以在 resolveAsset 的时候拿到这个组件的构造函数,方便后续创建。// src/core/global-api/assets.js
import { ASSET_TYPES } from 'shared/constants'
import { isPlainObject, validateComponentName } from '../util/index'
export function initAssetRegisters (Vue: GlobalAPI) {
/**
* Create asset registration methods.
*/
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)
}
// component处理,使用Vue.extend扩展,使得继承Vue类
if (type === 'component' && isPlainObject(definition)) {
definition.name = definition.name || id
definition = this.options._base.extend(definition)
}
// directive处理,如果是function,绑定为对象
// 官方API:https://cn.vuejs.org/v2/guide/custom-directive.html
if (type === 'directive' && typeof definition === 'function') {
definition = { bind: definition, update: definition }
}
// 资源型的componet、directive、filter都是挂载到vm.options上
this.options[type + 's'][id] = definition
return definition
}
}
})
}