Vue 组件化
Vue 组件化
createComponent : 创建组件 VNode
在 Vue.prototype._render
进行 Virtual DOM 渲染时,执行 vnode = render.call(vm._renderProxy, vm.$createElement)
,通过 vm.$createElement
最终调用 createElement
(最终调用 _createElement
) 创建 Virtual DOM 。
_createElement(context, tag, data, children, normalizationType)
方法中,会对参数 tag
进行判断,如果是一个普通的 html 标签,则会实例化一个普通的 VNode 节点,否则,会通过 createComponent
方法创建一个组件 VNode 。
【render】过程:调用的 _createElement 方法
// src\core\vdom\create-element.ts
export function _createElement(
context: Component,
tag?: string | Component | Function | Object,
data?: VNodeData,
children?: any,
normalizationType?: number
): VNode | Array<VNode> {
if (isDef(data) && isDef((data as any).__ob__)) {
__DEV__ &&
warn(
`Avoid using observed data object as vnode data: ${JSON.stringify(
data
)}\n` + 'Always create fresh vnode data objects in each render!',
context
)
return createEmptyVNode()
}
// object syntax in v-bind
if (isDef(data) && isDef(data.is)) {
tag = data.is
}
if (!tag) {
// in case of component :is set to falsy value
return createEmptyVNode()
}
// warn against non-primitive key
if (__DEV__ && isDef(data) && isDef(data.key) && !isPrimitive(data.key)) {
warn(
'Avoid using non-primitive value as key, ' +
'use string/number value instead.',
context
)
}
// support single function children as default scoped slot
if (isArray(children) && isFunction(children[0])) {
data = data || {}
data.scopedSlots = { default: children[0] }
children.length = 0
}
if (normalizationType === ALWAYS_NORMALIZE) {
children = normalizeChildren(children)
} else if (normalizationType === SIMPLE_NORMALIZE) {
children = simpleNormalizeChildren(children)
}
let vnode, ns
if (typeof tag === 'string') {
let Ctor
ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
if (config.isReservedTag(tag)) {
// platform built-in elements
if (
__DEV__ &&
isDef(data) &&
isDef(data.nativeOn) &&
data.tag !== 'component'
) {
warn(
`The .native modifier for v-on is only valid on components but it was used on <${tag}>.`,
context
)
}
vnode = new VNode(
config.parsePlatformTagName(tag),
data,
children,
undefined,
undefined,
context
)
} else if (
(!data || !data.pre) &&
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 as any, data, context, children)
}
if (isArray(vnode)) {
return vnode
} else if (isDef(vnode)) {
if (isDef(ns)) applyNS(vnode, ns)
if (isDef(data)) registerDeepBindings(data)
return vnode
} else {
return createEmptyVNode()
}
}
【render】过程:通过的 createComponent 方法创建 VNode
// src\core\vdom\create-component.ts
export function createComponent(
Ctor: typeof Component | Function | ComponentOptions | void,
data: VNodeData | undefined,
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 as typeof Component)
}
// if at this stage it's not a constructor or an async component factory,
// reject.
if (typeof Ctor !== 'function') {
if (__DEV__) {
warn(`Invalid Component definition: ${String(Ctor)}`, context)
}
return
}
// async component
let asyncFactory
// @ts-expect-error
if (isUndef(Ctor.cid)) {
asyncFactory = Ctor
Ctor = resolveAsyncComponent(asyncFactory, baseCtor)
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 as typeof Component)
// transform component v-model data into props & events
if (isDef(data.model)) {
// @ts-expect-error
transformModel(Ctor.options, data)
}
// extract props
// @ts-expect-error
const propsData = extractPropsFromVNodeData(data, Ctor, tag)
// functional component
// @ts-expect-error
if (isTrue(Ctor.options.functional)) {
return createFunctionalComponent(
Ctor as typeof Component,
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
// @ts-expect-error
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
// @ts-expect-error
const name = getComponentName(Ctor.options) || tag
const vnode = new VNode(
// @ts-expect-error
`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
data,
undefined,
undefined,
undefined,
context,
// @ts-expect-error
{ Ctor, propsData, listeners, tag, children },
asyncFactory
)
return vnode
}
构造子类构造函数
在开发过程中,编写 Vue 组件通常会创建一个普通对象, export
是一个对象。示例如下:
// App.vue
import HelloWorld from './components/HelloWorld'
export default {
name: 'app',
components: {
HelloWorld
}
}
在 createComponent
方法中,执行 const baseCtor = context.$options._base
时,其中:
baseCtor
: Vue 构造函数。通过context.$options._base
获取 Vue 构造函数,在执行initGlobalAPI
方法时,通过Vue.options._base = Vue
定义。context.$options
:Vue 实例的初始化选项。在 Vue 构造函数中,执行this._init
方法,进而调用Vue.prototype._init
,执行mergeOptions
进行options
合并和挂载。
// src\core\vdom\create-component.ts
// createComponent 方法
const baseCtor = context.$options._base
// plain options object: turn it into a constructor
if (isObject(Ctor)) {
Ctor = baseCtor.extend(Ctor as typeof Component)
}
在 createComponent
方法中,执行 Ctor = baseCtor.extend(Ctor as typeof Component)
时, baseCtor
指向 Vue, 则 baseCtor.extend
实际执行 Vue.extend
。
Vue.extend
的作用就是构造一个 Vue 的子类,使用原型继承的方式把一个纯对象转换一个继承于 Vue 的构造器 Sub
并返回。实例化 Sub
时,会执行 this.init
进行 Vue 实例初始化逻辑。同时,会 Sub
子类对扩展、初始化等操作:
- 对
Sub
这个对象本身扩展了一些属性:扩展 options 、 添加全局 API 等。 - 对配置中的
props
和computed
进行初始化工作 - 对于
Sub
构造函数进行缓存,避免多次执行Vue.extend
的时候对同一个子组件重复构造
Vue.extend
// src\core\global-api\extend.ts
export function initExtend(Vue: GlobalAPI) {
/**
* Each instance constructor, including Vue, has a unique
* cid. This enables us to create wrapped "child
* constructors" for prototypal inheritance and cache them.
*/
Vue.cid = 0
let cid = 1
/**
* Class inheritance
*/
Vue.extend = function (extendOptions: any): typeof Component {
extendOptions = extendOptions || {}
const Super = this
const SuperId = Super.cid
const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
if (cachedCtors[SuperId]) {
return cachedCtors[SuperId]
}
const name =
getComponentName(extendOptions) || getComponentName(Super.options)
if (__DEV__ && name) {
validateComponentName(name)
}
const Sub = function VueComponent(this: any, options: any) {
this._init(options)
} as unknown as typeof Component
Sub.prototype = Object.create(Super.prototype)
Sub.prototype.constructor = Sub
Sub.cid = cid++
Sub.options = mergeOptions(Super.options, extendOptions)
Sub['super'] = Super
// For props and computed properties, we define the proxy getters on
// the Vue instances at extension time, on the extended prototype. This
// avoids Object.defineProperty calls for each instance created.
if (Sub.options.props) {
initProps(Sub)
}
if (Sub.options.computed) {
initComputed(Sub)
}
// allow further extension/mixin/plugin usage
Sub.extend = Super.extend
Sub.mixin = Super.mixin
Sub.use = Super.use
// create asset registers, so extended classes
// can have their private assets too.
ASSET_TYPES.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
Sub.sealedOptions = extend({}, Sub.options)
// cache constructor
cachedCtors[SuperId] = Sub
return Sub
}
}
function initProps(Comp: typeof Component) {
const props = Comp.options.props
for (const key in props) {
proxy(Comp.prototype, `_props`, key)
}
}
function initComputed(Comp: typeof Component) {
const computed = Comp.options.computed
for (const key in computed) {
defineComputed(Comp.prototype, key, computed[key])
}
}
安装组件钩子函数
在 createComponent
方法中,执行 installComponentHooks
安装组件钩子函数。
installComponentHooks
的过程就是将 componentVNodeHooks
的钩子函数合并到 data.hook
中,在 VNode 执行 patch
的过程中执行相关的钩子函数。在合并过程中,如果某个时机的钩子已存在 data.hook
中,那么通过执行 mergeHook
函数合并,在最终执行的时候,依次执行这两个钩子函数。
installComponentHooks
// src\core\vdom\create-component.ts
// inline hooks to be invoked on component VNodes during patch
const componentVNodeHooks = {
init(vnode: VNodeWithData, hydrating: boolean): boolean | void {
if (
vnode.componentInstance &&
!vnode.componentInstance._isDestroyed &&
vnode.data.keepAlive
) {
// kept-alive components, treat as a patch
const mountedNode: any = vnode // work around flow
componentVNodeHooks.prepatch(mountedNode, mountedNode)
} else {
const child = (vnode.componentInstance = createComponentInstanceForVnode(
vnode,
activeInstance
))
child.$mount(hydrating ? vnode.elm : undefined, hydrating)
}
},
prepatch(oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
const options = vnode.componentOptions
const child = (vnode.componentInstance = oldVnode.componentInstance)
updateChildComponent(
child,
options.propsData, // updated props
options.listeners, // updated listeners
vnode, // new parent vnode
options.children // new children
)
},
insert(vnode: MountedComponentVNode) {
const { context, componentInstance } = vnode
if (!componentInstance._isMounted) {
componentInstance._isMounted = true
callHook(componentInstance, 'mounted')
}
if (vnode.data.keepAlive) {
if (context._isMounted) {
// vue-router#1212
// During updates, a kept-alive component's child components may
// change, so directly walking the tree here may call activated hooks
// on incorrect children. Instead we push them into a queue which will
// be processed after the whole patch process ended.
queueActivatedComponent(componentInstance)
} else {
activateChildComponent(componentInstance, true /* direct */)
}
}
},
destroy(vnode: MountedComponentVNode) {
const { componentInstance } = vnode
if (!componentInstance._isDestroyed) {
if (!vnode.data.keepAlive) {
componentInstance.$destroy()
} else {
deactivateChildComponent(componentInstance, true /* direct */)
}
}
}
}
const hooksToMerge = Object.keys(componentVNodeHooks)
function installComponentHooks(data: VNodeData) {
const hooks = data.hook || (data.hook = {})
for (let i = 0; i < hooksToMerge.length; i++) {
const key = hooksToMerge[i]
const existing = hooks[key]
const toMerge = componentVNodeHooks[key]
// @ts-expect-error
if (existing !== toMerge && !(existing && existing._merged)) {
hooks[key] = existing ? mergeHook(toMerge, existing) : toMerge
}
}
}
function mergeHook(f1: any, f2: any): Function {
const merged = (a, b) => {
// flow complains about extra args which is why we use any
f1(a, b)
f2(a, b)
}
merged._merged = true
return merged
}
实例化 VNode
在 createComponent
中,安装组件钩子函数后,会通过 new VNode
实例化一个 vnode
返回。与普通元素节点的 vnode
不同,组件的 vnode
是没有 children
的。
// return a placeholder vnode
// @ts-expect-error
const name = getComponentName(Ctor.options) || tag
const vnode = new VNode(
// @ts-expect-error
`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
data,
undefined,
undefined,
undefined,
context,
// @ts-expect-error
{ Ctor, propsData, listeners, tag, children },
asyncFactory
)
return vnode
patch : 将组件 VNode 渲染真实 DOM
在 _render
过程中,通过 vm.$createElement
调用 createElement
,使用 createComponent
创建了组件 VNode 后,会调用 vm._update
执行 vm.__patch__
将 VNode 转换成真实 DOM 节点。
在 patch
过程中,会调用 createElm
创建元素节点,其中,会判断 patch
的辅助函数 createComponent(vnode, insertedVnodeQueue, parentElm, refElm)
的返回值。
在 patch
的辅助函数 createComponent
函数中,对 vnode.data
进行了判断。如果 vnode
是一个组件 VNode,条件满足,并得到 i
为 init
钩子函数,对组件进行初始化。
【patch】 过程: 调用的 patch 辅助函数 createComponent
function createComponent(vnode, insertedVnodeQueue, parentElm, refElm) {
let i = vnode.data
if (isDef(i)) {
const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
if (isDef((i = i.hook)) && isDef((i = i.init))) {
i(vnode, false /* hydrating */)
}
// after calling the init hook, if the vnode is a child component
// it should've created a child instance and mounted it. the child
// component also has set the placeholder vnode's elm.
// in that case we can just return the element and be done.
if (isDef(vnode.componentInstance)) {
initComponent(vnode, insertedVnodeQueue)
insert(parentElm, vnode.elm, refElm)
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
}
return true
}
}
}
在执行 _render
过程中,通过 _createElement
,调用 createComponent
方法,执行 installComponentHooks
安装组件钩子函数,包含 init
钩子函数。在不考虑 keep-alive
的情况下,通过 createComponentInstanceForVnode
创建一个 Vue 的实例,然后调用 $mount
方法挂载子组件。
createComponentInstanceForVnode
函数构造一个内部组件的参数,然后执行 new vnode.componentOptions.Ctor(options)
进行子组件实例化,并执行实例的 this._init
方法。
vnode.componentOptions.Ctor
对应的是子组件的构造函数,实际上是继承于 Vue 的一个构造器Sub
,相当于new Sub(options)
options
参数options._isComponent
: 为true
表示为一个组件options.parent
: 表示当前激活的组件实例
【_render】 过程:调用 installComponentHooks 安装组件的 init 钩子函数
// src\core\vdom\create-component.ts
// inline hooks to be invoked on component VNodes during patch
const componentVNodeHooks = {
init(vnode: VNodeWithData, hydrating: boolean): boolean | void {
if (
vnode.componentInstance &&
!vnode.componentInstance._isDestroyed &&
vnode.data.keepAlive
) {
// kept-alive components, treat as a patch
const mountedNode: any = vnode // work around flow
componentVNodeHooks.prepatch(mountedNode, mountedNode)
} else {
const child = (vnode.componentInstance = createComponentInstanceForVnode(
vnode,
activeInstance
))
child.$mount(hydrating ? vnode.elm : undefined, hydrating)
}
},
// ... 省略其他钩子函数定义 : prepatch、insert、destroy
}
export function createComponentInstanceForVnode(
// we know it's MountedComponentVNode but flow doesn't
vnode: any,
// activeInstance in lifecycle state
parent?: any
): Component {
const options: InternalComponentOptions = {
_isComponent: true,
_parentVnode: vnode,
parent
}
// check inline-template render functions
const inlineTemplate = vnode.data.inlineTemplate
if (isDef(inlineTemplate)) {
options.render = inlineTemplate.render
options.staticRenderFns = inlineTemplate.staticRenderFns
}
return new vnode.componentOptions.Ctor(options)
}
子组件实例化的过程中,调用 this._init
执行相关逻辑会有些不同:
合并
options
: 在Vue.prototype._init
方法中,判断options._isComponent
为true
会执行initInternalComponent(vm, options)
。其中,opts.parent = options.parent
opts._parentVnode = parentVnode
是通过
createComponentInstanceForVnode(vnode, parent)
函数传入的参数,合并到内部的选项$options
中。子组件接管
$mount
: 子组件初始化的时候不传el
参数,则,回到组件init
钩子函数,完成实例化的_init
后,执行child.$mount(hydrating ? vnode.elm : undefined, hydrating)
。hydrating
: 为true
一般表示服务端渲染的情况child.$mount(hydrating ? vnode.elm : undefined, hydrating)
对于客户端渲染,相当于child.$mount(undefined, false)
,最终会调用mountComponent
方法,进而执行vm._render()
方法
【子组件实例化过程】:调用的 Vue.prototype._init 进行初始化
// src\core\instance\init.ts
export function initMixin(Vue: typeof Component) {
Vue.prototype._init = function (options?: Record<string, any>) {
const vm: Component = this
// a uid
vm._uid = uid++
let startTag, endTag
/* istanbul ignore if */
if (__DEV__ && config.performance && mark) {
startTag = `vue-perf-start:${vm._uid}`
endTag = `vue-perf-end:${vm._uid}`
mark(startTag)
}
// a flag to mark this as a Vue instance without having to do instanceof
// check
vm._isVue = true
// avoid instances from being observed
vm.__v_skip = true
// effect scope
vm._scope = new EffectScope(true /* detached */)
vm._scope._vm = true
// merge options
if (options && options._isComponent) {
// optimize internal component instantiation
// since dynamic options merging is pretty slow, and none of the
// internal component options needs special treatment.
initInternalComponent(vm, options as any)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor as any),
options || {},
vm
)
}
/* istanbul ignore else */
if (__DEV__) {
initProxy(vm)
} else {
vm._renderProxy = vm
}
// expose real self
vm._self = vm
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate', undefined, false /* setContext */)
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')
/* istanbul ignore if */
if (__DEV__ && config.performance && mark) {
vm._name = formatComponentName(vm, false)
mark(endTag)
measure(`vue ${vm._name} init`, startTag, endTag)
}
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
}
子组件进行 _render
渲染 Virtual DOM 过程的时候,其中
_parentVnode
: 当前组件的父 VNodevm.$vnode
: 当前 vnode 的 parentvnode
: 通过render
函数生成的当前组件的渲染 vnode
【_render】 过程:调用 Vue.prototype._render 渲染 Virtual DOM
// src\core\instance\render.ts
Vue.prototype._render = function (): VNode {
const vm: Component = this
const { render, _parentVnode } = vm.$options
if (_parentVnode && vm._isMounted) {
vm.$scopedSlots = normalizeScopedSlots(
vm.$parent!,
_parentVnode.data!.scopedSlots,
vm.$slots,
vm.$scopedSlots
)
if (vm._slotsProxy) {
syncSetupSlots(vm._slotsProxy, vm.$scopedSlots)
}
}
// set parent vnode. this allows render functions to have access
// to the data on the placeholder node.
vm.$vnode = _parentVnode!
// render self
let vnode
try {
// There's no need to maintain a stack because all render fns are called
// separately from one another. Nested component's render fns are called
// when parent component is patched.
setCurrentInstance(vm)
currentRenderingInstance = vm
vnode = render.call(vm._renderProxy, vm.$createElement)
} catch (e: any) {
handleError(e, vm, `render`)
// return error render result,
// or previous vnode to prevent render error causing blank component
/* istanbul ignore else */
if (__DEV__ && vm.$options.renderError) {
try {
vnode = vm.$options.renderError.call(
vm._renderProxy,
vm.$createElement,
e
)
} catch (e: any) {
handleError(e, vm, `renderError`)
vnode = vm._vnode
}
} else {
vnode = vm._vnode
}
} finally {
currentRenderingInstance = null
setCurrentInstance()
}
// if the returned array contains only a single node, allow it
if (isArray(vnode) && vnode.length === 1) {
vnode = vnode[0]
}
// return empty vnode in case the render function errored out
if (!(vnode instanceof VNode)) {
if (__DEV__ && isArray(vnode)) {
warn(
'Multiple root nodes returned from render function. Render function ' +
'should return a single root node.',
vm
)
}
vnode = createEmptyVNode()
}
// set parent
vnode.parent = _parentVnode
return vnode
}
子组件通过 vm._render
生成 VNode 后,执行 vm._update
调用 __patch__
进行 VNode 渲染。其中:
vm._vnode = vnode
:vnode
是通过vm._render()
返回的组件渲染 VNodevm._vnode
和vm.$vnode
的关系就是一种父子关系,通过代码标识为vm._vnode.parent === vm.$vnode
activeInstance
: 作用是保存当前上下文的 Vue 实例,是lifecycle
模块的全局变量。- 在调用
createComponentInstanceForVnode(vnode, parent)
方法创建组件 Vue 实例时,会从lifecycle
模块中获取activeInstance
,并作为parent
参数传入。实际上,JavaScript 是一个单线程,Vue 整个初始化是一个深度遍历的过程,在实例化子组件的过程中,需要知道当前上下文的 Vue 实例,并把它作为子组件的父 Vue 实例。 - 子组件实例化的过程中,会调用
this._init
进行初始化。- 执行
initInternalComponent(vm, options)
。 合并options
,将parent
存储在vm.$options
中。 - 执行
initLifecycle(vm)
。使用vm.$parent
保留当前vm
的父实例,并通过parent.$children.push(vm)
把当前的vm
存储到父实例的$children
中。
- 执行
- 在执行
vm._update
过程中,会调用setActiveInstance
将当前的vm
赋值给activeInstance
,同时,通过const prevActiveInstance = activeInstance
使用prevActiveInstance
保留上一次的activeInstance
。实际上,prevActiveInstance
和当前的vm
是一个父子关系,当一个vm
实例完成它的所有子数的patch
或者update
过程后,activeInstance
会回到它的父实例,这样就能保证在createComponentInstanceForVnode(vnode, parent)
整个深度遍历过程中,在实例子组件的时候能传入当前子组件的父 Vue 实例,并在this._init
过程中,通过vm.$parent
将这个父子关系保留。
- 在调用
【patch】过程:通过 vm._update 进行 VNode 渲染
// src\core\instance\lifecycle.ts
export let activeInstance: any = null
export function setActiveInstance(vm: Component) {
const prevActiveInstance = activeInstance
activeInstance = vm
return () => {
activeInstance = prevActiveInstance
}
}
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
const vm: Component = this
const prevEl = vm.$el
const prevVnode = vm._vnode
const restoreActiveInstance = setActiveInstance(vm)
vm._vnode = vnode
// Vue.prototype.__patch__ is injected in entry points
// based on the rendering backend used.
if (!prevVnode) {
// initial render
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
} else {
// updates
vm.$el = vm.__patch__(prevVnode, vnode)
}
restoreActiveInstance()
// update __vue__ reference
if (prevEl) {
prevEl.__vue__ = null
}
if (vm.$el) {
vm.$el.__vue__ = vm
}
// if parent is an HOC, update its $el as well
if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
vm.$parent.$el = vm.$el
}
// updated hook is called by the scheduler to ensure that children are
// updated in a parent's updated hook.
}
在 __patch__
过程中,实际调用辅助函数 createElm(vnode, insertedVnodeQueue, parentElm, refElm, nested, ownerArray, index)
渲染 VNode 为真实 DOM 。
在 patch
函数中,初始化的时候,通过 createElm(vnode, insertedVnodeQueue)
调用。在 createElm
函数中,vnode
: 组件渲染的 vnode ,即: vm._vnode
:
- 如果组件的根节点是普通元素,则
vm._vnode
为普通vnode
。在createElm
中createComponent(vnode, insertedVnodeQueue, parentElm, refElm)
返回为false
。先创建一个父节点占位符,然后再遍历所有的子 VNode 递归调用createElm
。 - 在遍历过程中,如果遇到子 VNode 是一个组件 VNode,则调用
createComponent
创建组件 VNode 重新执行相关逻辑。这样通过一个递归的方式就可以完整地构建了整个组件树。
完成组件的整个 patch
过程后,最后执行 insert(parentElm, vnode.elm, refElm)
完成组件的 DOM 插入,如果组件 patch
过程中又创建了子组件,那么 DOM 的插入顺序是 先子后父 。
【patch】过程:调用的 patch 函数
export function createPatchFunction(backend) {
return function patch(oldVnode, vnode, hydrating, removeOnly) {
if (isUndef(vnode)) {
if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
return
}
let isInitialPatch = false
const insertedVnodeQueue: any[] = []
if (isUndef(oldVnode)) {
// empty mount (likely as component), create new root element
isInitialPatch = true
createElm(vnode, insertedVnodeQueue)
} else {
const isRealElement = isDef(oldVnode.nodeType)
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// patch existing root node
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
} else {
if (isRealElement) {
// mounting to a real element
// check if this is server-rendered content and if we can perform
// a successful hydration.
if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
oldVnode.removeAttribute(SSR_ATTR)
hydrating = true
}
if (isTrue(hydrating)) {
if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
invokeInsertHook(vnode, insertedVnodeQueue, true)
return oldVnode
} else if (__DEV__) {
warn(
'The client-side rendered virtual DOM tree is not matching ' +
'server-rendered content. This is likely caused by incorrect ' +
'HTML markup, for example nesting block-level elements inside ' +
'<p>, or missing <tbody>. Bailing hydration and performing ' +
'full client-side render.'
)
}
}
// either not server-rendered, or hydration failed.
// create an empty node and replace it
oldVnode = emptyNodeAt(oldVnode)
}
// replacing existing element
const oldElm = oldVnode.elm
const parentElm = nodeOps.parentNode(oldElm)
// create new node
createElm(
vnode,
insertedVnodeQueue,
// extremely rare edge case: do not insert if old element is in a
// leaving transition. Only happens when combining transition +
// keep-alive + HOCs. (#4590)
oldElm._leaveCb ? null : parentElm,
nodeOps.nextSibling(oldElm)
)
// update parent placeholder node element, recursively
if (isDef(vnode.parent)) {
let ancestor = vnode.parent
const patchable = isPatchable(vnode)
while (ancestor) {
for (let i = 0; i < cbs.destroy.length; ++i) {
cbs.destroy[i](ancestor)
}
ancestor.elm = vnode.elm
if (patchable) {
for (let i = 0; i < cbs.create.length; ++i) {
cbs.create[i](emptyNode, ancestor)
}
// #6513
// invoke insert hooks that may have been merged by create hooks.
// e.g. for directives that uses the "inserted" hook.
const insert = ancestor.data.hook.insert
if (insert.merged) {
// start at index 1 to avoid re-invoking component mounted hook
for (let i = 1; i < insert.fns.length; i++) {
insert.fns[i]()
}
}
} else {
registerRef(ancestor)
}
ancestor = ancestor.parent
}
}
// destroy old node
if (isDef(parentElm)) {
removeVnodes([oldVnode], 0, 0)
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode)
}
}
}
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
return vnode.elm
}
}
【patch】过程:调用的 patch 辅助函数 createElm
function createElm(
vnode,
insertedVnodeQueue,
parentElm?: any,
refElm?: any,
nested?: any,
ownerArray?: any,
index?: any
) {
if (isDef(vnode.elm) && isDef(ownerArray)) {
// This vnode was used in a previous render!
// now it's used as a new node, overwriting its elm would cause
// potential patch errors down the road when it's used as an insertion
// reference node. Instead, we clone the node on-demand before creating
// associated DOM element for it.
vnode = ownerArray[index] = cloneVNode(vnode)
}
vnode.isRootInsert = !nested // for transition enter check
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
const data = vnode.data
const children = vnode.children
const tag = vnode.tag
if (isDef(tag)) {
if (__DEV__) {
if (data && data.pre) {
creatingElmInVPre++
}
if (isUnknownElement(vnode, creatingElmInVPre)) {
warn(
'Unknown custom element: <' +
tag +
'> - did you ' +
'register the component correctly? For recursive components, ' +
'make sure to provide the "name" option.',
vnode.context
)
}
}
vnode.elm = vnode.ns
? nodeOps.createElementNS(vnode.ns, tag)
: nodeOps.createElement(tag, vnode)
setScope(vnode)
createChildren(vnode, children, insertedVnodeQueue)
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue)
}
insert(parentElm, vnode.elm, refElm)
if (__DEV__ && data && data.pre) {
creatingElmInVPre--
}
} else if (isTrue(vnode.isComment)) {
vnode.elm = nodeOps.createComment(vnode.text)
insert(parentElm, vnode.elm, refElm)
} else {
vnode.elm = nodeOps.createTextNode(vnode.text)
insert(parentElm, vnode.elm, refElm)
}
}
merge options : 合并配置
在进行 Vue 实例化的过程中,会执行 this._init
(即:Vue.prototype._init
)。该过程中会进行 merge options
合并配置。 new Vue
的过程通常有 2 种场景:
- 外部主动调用
new Vue(options)
实例化 Vue 对象 - 组件内部调用
new Vue(options)
实例化子组件
【Vue 实例初始化】过程:调用 Vue.prototype._init 合并配置
// src\core\instance\init.ts
export function initMixin(Vue: typeof Component) {
Vue.prototype._init = function (options?: Record<string, any>) {
const vm: Component = this
// a uid
vm._uid = uid++
let startTag, endTag
/* istanbul ignore if */
if (__DEV__ && config.performance && mark) {
startTag = `vue-perf-start:${vm._uid}`
endTag = `vue-perf-end:${vm._uid}`
mark(startTag)
}
// a flag to mark this as a Vue instance without having to do instanceof
// check
vm._isVue = true
// avoid instances from being observed
vm.__v_skip = true
// effect scope
vm._scope = new EffectScope(true /* detached */)
vm._scope._vm = true
// merge options
if (options && options._isComponent) {
// optimize internal component instantiation
// since dynamic options merging is pretty slow, and none of the
// internal component options needs special treatment.
initInternalComponent(vm, options as any)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor as any),
options || {},
vm
)
}
/* istanbul ignore else */
if (__DEV__) {
initProxy(vm)
} else {
vm._renderProxy = vm
}
// expose real self
vm._self = vm
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate', undefined, false /* setContext */)
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')
/* istanbul ignore if */
if (__DEV__ && config.performance && mark) {
vm._name = formatComponentName(vm, false)
mark(endTag)
measure(`vue ${vm._name} init`, startTag, endTag)
}
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
}
外部主动调用合并配置
外部主动调用 new Vue(options)
的时候,会执行 this._init
(即:Vue.prototype._init
)中如下逻辑合并 options
。实际上是把 resolveConstructorOptions(vm.constructor)
的返回值和 options
合并。
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
resolveConstructorOptions(vm.constructor)
返回为 vm.constructor.options
,相当于 Vue.options
。
Vue.options
是在初始化 Vue 过程中,执行 initGlobalAPI(Vue)
的时候定义,其中:
通过
Vue.options = Object.create(null)
创建空对象遍历
ASSET_TYPES
在Vue.options
上扩展component
、directive
、filter
,相当于Vue.options.components = {} Vue.options.directives = {} Vue.options.filters = {}
通过
Vue.options._base = Vue
获取 Vue 的构造函数通过
extend(Vue.options.components, builtInComponents)
将一些内置组件扩展到Vue.options.components
上。目前 Vue 的内置组件包括<keep-alive>
、<transition>
和<transition-group>
组件,这也就是在其他组件中使用<keep-alive>
不需要注册的原因。
initGlobalAPI 函数
// src\shared\constants.ts
export const ASSET_TYPES = ['component', 'directive', 'filter'] as const
// src\core\global-api\index.ts
export function initGlobalAPI(Vue: GlobalAPI) {
// config
const configDef: Record<string, any> = {}
configDef.get = () => config
if (__DEV__) {
configDef.set = () => {
warn(
'Do not replace the Vue.config object, set individual fields instead.'
)
}
}
Object.defineProperty(Vue, 'config', configDef)
// exposed util methods.
// NOTE: these are not considered part of the public API - avoid relying on
// them unless you are aware of the risk.
Vue.util = {
warn,
extend,
mergeOptions,
defineReactive
}
Vue.set = set
Vue.delete = del
Vue.nextTick = nextTick
// 2.6 explicit observable API
Vue.observable = <T>(obj: T): T => {
observe(obj)
return obj
}
Vue.options = Object.create(null)
ASSET_TYPES.forEach(type => {
Vue.options[type + 's'] = Object.create(null)
})
// 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
extend(Vue.options.components, builtInComponents)
initUse(Vue)
initMixin(Vue)
initExtend(Vue)
initAssetRegisters(Vue)
}
mergeOptions
函数主要功能是将 parent
和 child
这两个对象根据合并策略,合并成一个新对象并返回。其中,核心逻辑为:先递归将 extend
和 mixins
合并到 parent
上,然后遍历 parent
,调用 mergeField
,然后再遍历 child
,如果 key
不在 parent
的自身属性上,则调用 mergeField
函数。
对于 mergeField
函数,不同的 Key
有着不同的合并策略。以生命周期为例:
- 如果不存在
childVal
,就返回parentVal
- 否则,再判断是否存在
parentVal
,如果存在就把childVal
添加到parentVal
后返回新数组 - 否则,返回
childVal
的数组
所以,一旦 parent
和 child
都定义了相同的钩子函数,会把 2 个钩子函数合并成一个数组
mergeOptions 函数
// src\core\util\options.ts
/**
* Merge two option objects into a new one.
* Core utility used in both instantiation and inheritance.
*/
export function mergeOptions(
parent: Record<string, any>,
child: Record<string, any>,
vm?: Component | null
): ComponentOptions {
if (__DEV__) {
checkComponents(child)
}
if (isFunction(child)) {
// @ts-expect-error
child = child.options
}
normalizeProps(child, vm)
normalizeInject(child, vm)
normalizeDirectives(child)
// Apply extends and mixins on the child options,
// but only if it is a raw options object that isn't
// the result of another mergeOptions call.
// Only merged options has the _base property.
if (!child._base) {
if (child.extends) {
parent = mergeOptions(parent, child.extends, vm)
}
if (child.mixins) {
for (let i = 0, l = child.mixins.length; i < l; i++) {
parent = mergeOptions(parent, child.mixins[i], vm)
}
}
}
const options: ComponentOptions = {} as any
let key
for (key in parent) {
mergeField(key)
}
for (key in child) {
if (!hasOwn(parent, key)) {
mergeField(key)
}
}
function mergeField(key: any) {
const strat = strats[key] || defaultStrat
options[key] = strat(parent[key], child[key], vm, key)
}
return options
}
LIFECYCLE_HOOKS.forEach(hook => {
strats[hook] = mergeLifecycleHook
})
/**
* Hooks and props are merged as arrays.
*/
export function mergeLifecycleHook(
parentVal: Array<Function> | null,
childVal: Function | Array<Function> | null
): Array<Function> | null {
const res = childVal
? parentVal
? parentVal.concat(childVal)
: isArray(childVal)
? childVal
: [childVal]
: parentVal
return res ? dedupeHooks(res) : res
}
// src\shared\constants.ts
export const LIFECYCLE_HOOKS = [
'beforeCreate',
'created',
'beforeMount',
'mounted',
'beforeUpdate',
'updated',
'beforeDestroy',
'destroyed',
'activated',
'deactivated',
'errorCaptured',
'serverPrefetch',
'renderTracked',
'renderTriggered'
] as const
组件内部调用合并配置
组件的构造函数是通过 Vue.extend(extendOptions)
继承 Vue 的。其中,extendOptions
为定义的组件对象,会和 Vue.options
合并到 Sub.options
中。
Vue.extend
export function initExtend(Vue: GlobalAPI) {
/**
* Each instance constructor, including Vue, has a unique
* cid. This enables us to create wrapped "child
* constructors" for prototypal inheritance and cache them.
*/
Vue.cid = 0
let cid = 1
/**
* Class inheritance
*/
Vue.extend = function (extendOptions: any): typeof Component {
extendOptions = extendOptions || {}
const Super = this
const SuperId = Super.cid
const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
if (cachedCtors[SuperId]) {
return cachedCtors[SuperId]
}
const name =
getComponentName(extendOptions) || getComponentName(Super.options)
if (__DEV__ && name) {
validateComponentName(name)
}
const Sub = function VueComponent(this: any, options: any) {
this._init(options)
} as unknown as typeof Component
Sub.prototype = Object.create(Super.prototype)
Sub.prototype.constructor = Sub
Sub.cid = cid++
Sub.options = mergeOptions(Super.options, extendOptions)
Sub['super'] = Super
// For props and computed properties, we define the proxy getters on
// the Vue instances at extension time, on the extended prototype. This
// avoids Object.defineProperty calls for each instance created.
if (Sub.options.props) {
initProps(Sub)
}
if (Sub.options.computed) {
initComputed(Sub)
}
// allow further extension/mixin/plugin usage
Sub.extend = Super.extend
Sub.mixin = Super.mixin
Sub.use = Super.use
// create asset registers, so extended classes
// can have their private assets too.
ASSET_TYPES.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
Sub.sealedOptions = extend({}, Sub.options)
// cache constructor
cachedCtors[SuperId] = Sub
return Sub
}
}
子组件通过 createComponentInstanceForVnode
函数进行组件实例化,并执行实例的 this._init
方法,判断 options._isComponent
为 true
会执行 initInternalComponent(vm, options)
进行 options
合并。其中:
- 执行
const opts = vm.$options = Object.create(vm.constructor.options)
vm.constructor
是子组件的构造函数Sub
,相当于vm.$options = Object.create(Sub.options)
- 然后,把实例化子组件传入的子组件父 VNode 实例
parentVnode
、 子组件的父 Vue 实例parent
保存到vm.$options
中。 - 同时,还保留了
parentVnode
配置中的propsData
等其他属性
initInternalComponent
// src\core\instance\init.ts
export function initInternalComponent(
vm: Component,
options: InternalComponentOptions
) {
const opts = (vm.$options = Object.create((vm.constructor as any).options))
// doing this because it's faster than dynamic enumeration.
const parentVnode = options._parentVnode
opts.parent = options.parent
opts._parentVnode = parentVnode
const vnodeComponentOptions = parentVnode.componentOptions!
opts.propsData = vnodeComponentOptions.propsData
opts._parentListeners = vnodeComponentOptions.listeners
opts._renderChildren = vnodeComponentOptions.children
opts._componentTag = vnodeComponentOptions.tag
if (options.render) {
opts.render = options.render
opts.staticRenderFns = options.staticRenderFns
}
}
生命周期
Vue 实例在被创建之前会经过一系列的初始化过程。例如,需要设置数据监听、编译模板、挂载实例到 DOM 、在数据变化时更新 DOM 等。在执行的过程中,会运行生命周期钩子函数,供用户在一些特定的场景中执行相关代码。
执行生命周期函数都会调用 callHook
函数,在 lifecycle
模块中定义。
在 callHook
函数中,会根据传入的字符串 hook
,获取 vm.$options[hook]
对应的回调函数数组,然后遍历执行,执行的时候会将 vm
作为函数执行的上下文。
lifecycle 模块 callHook 函数
// src\core\instance\lifecycle.ts
export function callHook(
vm: Component,
hook: string,
args?: any[],
setContext = true
) {
// #7573 disable dep collection when invoking lifecycle hooks
pushTarget()
const prev = currentInstance
setContext && setCurrentInstance(vm)
const handlers = vm.$options[hook]
const info = `${hook} hook`
if (handlers) {
for (let i = 0, j = handlers.length; i < j; i++) {
invokeWithErrorHandling(handlers[i], vm, args || null, vm, info)
}
}
if (vm._hasHookEvent) {
vm.$emit('hook:' + hook)
}
setContext && setCurrentInstance(prev)
popTarget()
}
beforeCreate & created
beforeCreate
和 created
函数在进行 Vue 实例化的过程中,在调用 this._init
方法(即:Vue.prototype._init
方法)进行初始化的时候执行的。
beforeCreate
和 created
生命周期钩子函数分别在 initState
前后执行。initState
函数会初始化 props
、 data
、methods
、watch
、 computed
等属性。beforeCreate
的生命钩子函数中,不能获取到 props
、 data
中定义的值,也不能调用 methods
中定义的函数。
beforeCreate
和 created
在执行的时候,并没有渲染 DOM ,也不能访问 DOM 。
一般来说,如果组件在加载的时候,需要与后端进行交互,将交互逻辑放在这两个钩子函数之后即可。
Vue.prototype._init(options) : 初始化 Vue
// src\core\instance\init.ts
export function initMixin(Vue: typeof Component) {
Vue.prototype._init = function (options?: Record<string, any>) {
const vm: Component = this
// a uid
vm._uid = uid++
let startTag, endTag
/* istanbul ignore if */
if (__DEV__ && config.performance && mark) {
startTag = `vue-perf-start:${vm._uid}`
endTag = `vue-perf-end:${vm._uid}`
mark(startTag)
}
// a flag to mark this as a Vue instance without having to do instanceof
// check
vm._isVue = true
// avoid instances from being observed
vm.__v_skip = true
// effect scope
vm._scope = new EffectScope(true /* detached */)
vm._scope._vm = true
// merge options
if (options && options._isComponent) {
// optimize internal component instantiation
// since dynamic options merging is pretty slow, and none of the
// internal component options needs special treatment.
initInternalComponent(vm, options as any)
} else {
// 合并配置
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor as any),
options || {},
vm
)
}
/* istanbul ignore else */
if (__DEV__) {
initProxy(vm)
} else {
vm._renderProxy = vm
}
// expose real self
vm._self = vm
initLifecycle(vm) // 初始化生命周期
initEvents(vm) // 初始化事件中心
initRender(vm) // 初始化渲染
callHook(vm, 'beforeCreate', undefined, false /* setContext */)
initInjections(vm) // 初始化 inject - resolve injections before data/props
initState(vm) // 初始化 props 、 methods 、 data 、 computed 、 watch
initProvide(vm) // 初始化 provide - resolve provide after data/props
callHook(vm, 'created')
/* istanbul ignore if */
if (__DEV__ && config.performance && mark) {
vm._name = formatComponentName(vm, false)
mark(endTag)
measure(`vue ${vm._name} init`, startTag, endTag)
}
// 如果有 el 属性, 则调用 vm.$mount 方法挂载 vm
// 目的是将模板渲染程最终的 DOM
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
}
beforeMount & mounted
beforeMount
函数发生在 Vue 实例挂载 DOM 之前,在执行公共 $mount
方法中,最终调用 mountComponent
函数中执行。
在执行 vm._render()
函数渲染 VNode 之前,执行了 beforeMount
钩子函数,在执行完 vm._update()
把 VNode patch
到真实 DOM 后,执行 mounted
钩子函数。
在执行 mounted
钩子函数的时候,会进行判断。如果 vm.$vnode == null
,则表示不是组件的初始化过程,是通过外部 new Vue
的初始化过程。
对于组件 VNode patch
到 DOM 的过程中,会执行 invokeInsertHook
函数,把 insertedVnodeQueue
里保存的钩子函数依次执行一遍。
invokeInsertHook
函数会执行 insert
钩子函数,每个子组件都是在这个钩子函数中执行 mounted
钩子函数。insertedVnodeQueue
的添加顺序是先子后父,对于同步渲染的子组件而言,mounted
钩子函数的执行顺序也是先子后父。
【$mount 挂载】过程:调用的 mountComponent 方法
// src\core\instance\lifecycle.ts
export function mountComponent(
vm: Component,
el: Element | null | undefined,
hydrating?: boolean
): Component {
vm.$el = el
if (!vm.$options.render) {
// @ts-expect-error invalid type
vm.$options.render = createEmptyVNode
if (__DEV__) {
/* istanbul ignore if */
if (
(vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
vm.$options.el ||
el
) {
warn(
'You are using the runtime-only build of Vue where the template ' +
'compiler is not available. Either pre-compile the templates into ' +
'render functions, or use the compiler-included build.',
vm
)
} else {
warn(
'Failed to mount component: template or render function not defined.',
vm
)
}
}
}
callHook(vm, 'beforeMount')
let updateComponent
/* istanbul ignore if */
if (__DEV__ && config.performance && mark) {
updateComponent = () => {
const name = vm._name
const id = vm._uid
const startTag = `vue-perf-start:${id}`
const endTag = `vue-perf-end:${id}`
mark(startTag)
const vnode = vm._render()
mark(endTag)
measure(`vue ${name} render`, startTag, endTag)
mark(startTag)
vm._update(vnode, hydrating)
mark(endTag)
measure(`vue ${name} patch`, startTag, endTag)
}
} else {
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
}
const watcherOptions: WatcherOptions = {
before() {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}
if (__DEV__) {
watcherOptions.onTrack = e => callHook(vm, 'renderTracked', [e])
watcherOptions.onTrigger = e => callHook(vm, 'renderTriggered', [e])
}
// we set this to vm._watcher inside the watcher's constructor
// since the watcher's initial patch may call $forceUpdate (e.g. inside child
// component's mounted hook), which relies on vm._watcher being already defined
new Watcher(
vm,
updateComponent,
noop,
watcherOptions,
true /* isRenderWatcher */
)
hydrating = false
// flush buffer for flush: "pre" watchers queued in setup()
const preWatchers = vm._preWatchers
if (preWatchers) {
for (let i = 0; i < preWatchers.length; i++) {
preWatchers[i].run()
}
}
// manually mounted instance, call mounted on self
// mounted is called for render-created child components in its inserted hook
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}
【patch】过程:调用 invokeInsertHook 函数
function invokeInsertHook(vnode, queue, initial) {
// delay insert hooks for component root nodes, invoke them after the
// element is really inserted
if (isTrue(initial) && isDef(vnode.parent)) {
vnode.parent.data.pendingInsert = queue
} else {
for (let i = 0; i < queue.length; ++i) {
queue[i].data.hook.insert(queue[i])
}
}
}
beforeUpdate & updated
beforeUpdate
和 updated
的钩子函数执行时机都应该是在数据更新的时候。
beforeUpdate
的执行时机是在调用 mountComponent
函数中,渲染 Watcher
的 before
函数中调用。在组件 mounted
之后,才会调用这个钩子函数。
update
的执行时机是在 flushSchedulerQueue
函数调用的时候。其中,updatedQueue
是更新了的 watcher
数组,在调用 callUpdatedHooks
函数中,对 updatedQueue
中的数组进行遍历,只有满足当前 watcher
为 vm._watcher
以及组件已完成 mounted
,才会调用 updated
钩子函数。
- 在组件
mount
的过程中,会在mountComponent
函数执行时,会实例化一个渲染的Watcher
去监听vm
上的数据变化重新渲染。 - 在实例化
Watcher
的过程中,在它的构造函数里会判断isRenderWatcher
,把当前watcher
的实例赋值给vm._watcher
,同时,把当前wathcer
实例 push 到vm._watchers
中。 vm._watcher
是专门用来监听vm
上数据变化然后重新渲染的,所以它是一个渲染相关的watcher
,因此在callUpdatedHooks
函数中,只有vm._watcher
的回调执行完毕后,才会执行updated
钩子函数。
flushSchedulerQueue 函数
// src\core\observer\scheduler.ts
/**
* Flush both queues and run the watchers.
*/
function flushSchedulerQueue() {
currentFlushTimestamp = getNow()
flushing = true
let watcher, id
// Sort queue before flush.
// This ensures that:
// 1. Components are updated from parent to child. (because parent is always
// created before the child)
// 2. A component's user watchers are run before its render watcher (because
// user watchers are created before the render watcher)
// 3. If a component is destroyed during a parent component's watcher run,
// its watchers can be skipped.
queue.sort(sortCompareFn)
// do not cache length because more watchers might be pushed
// as we run existing watchers
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
if (watcher.before) {
watcher.before()
}
id = watcher.id
has[id] = null
watcher.run()
// in dev build, check and stop circular updates.
if (__DEV__ && has[id] != null) {
circular[id] = (circular[id] || 0) + 1
if (circular[id] > MAX_UPDATE_COUNT) {
warn(
'You may have an infinite update loop ' +
(watcher.user
? `in watcher with expression "${watcher.expression}"`
: `in a component render function.`),
watcher.vm
)
break
}
}
}
// keep copies of post queues before resetting state
const activatedQueue = activatedChildren.slice()
const updatedQueue = queue.slice()
resetSchedulerState()
// call component updated and activated hooks
callActivatedHooks(activatedQueue)
callUpdatedHooks(updatedQueue)
// devtool hook
/* istanbul ignore if */
if (devtools && config.devtools) {
devtools.emit('flush')
}
}
function callUpdatedHooks(queue: Watcher[]) {
let i = queue.length
while (i--) {
const watcher = queue[i]
const vm = watcher.vm
if (vm && vm._watcher === watcher && vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'updated')
}
}
}
beforeDestroy & destroyed
beforeDestroy
和 destroyed
钩子函数的执行时机在组件销毁的阶段,最终会调用 $destroy
方法。
beforeDestroy
钩子函数的执行时机是在 $destroy
函数执行最开始的地方,接着执行了一系列的销毁动作:从 parent
的 $children
中删掉自身、删除 watcher
、删除当前渲染的 VNode 执行销毁钩子函数等,执行完毕后再调用 destroy 钩子函函数
$destroy
的执行过程中,它又会执行 vm.__patch__(vm._vnode, null)
触发它子组件的销毁钩子函数,一层层的递归调用,所以 destroy
钩子函数执行顺序是先子后父,和 mounted
过程一样。
Vue.prototype.$destroy
Vue.prototype.$destroy = function () {
const vm: Component = this
if (vm._isBeingDestroyed) {
return
}
callHook(vm, 'beforeDestroy')
vm._isBeingDestroyed = true
// remove self from parent
const parent = vm.$parent
if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
remove(parent.$children, vm)
}
// teardown scope. this includes both the render watcher and other
// watchers created
vm._scope.stop()
// remove reference from data ob
// frozen object may not have observer.
if (vm._data.__ob__) {
vm._data.__ob__.vmCount--
}
// call the last hook...
vm._isDestroyed = true
// invoke destroy hooks on current rendered tree
vm.__patch__(vm._vnode, null)
// fire destroyed hook
callHook(vm, 'destroyed')
// turn off all instance listeners.
vm.$off()
// remove __vue__ reference
if (vm.$el) {
vm.$el.__vue__ = null
}
// release circular reference (#6759)
if (vm.$vnode) {
vm.$vnode.parent = null
}
}
组件注册
全局注册
注册全局属性,可通过 Vue.component(tagName, options)
进行注册。
Vue.component
是在初始化 Vue 过程中,调用 initGlobalAPI(Vue)
函数,执行 initAssetRegisters
的时候定义。在 initAssetRegisters
函数中:
- 如果
type
是component
且definition
是一个对象的话,通过this.opitons._base.extend
,相当于Vue.extend
把这个对象转换成一个继承于 Vue 的构造函数。 - 最后通过
this.options[type + 's'][id] = definition
把它挂载到Vue.options.components
上。
初始化 Vue 过程中,调用的 initGlobalAPI 函数
// src\core\global-api\index.ts
export function initGlobalAPI(Vue: GlobalAPI) {
// config
const configDef: Record<string, any> = {}
configDef.get = () => config
if (__DEV__) {
configDef.set = () => {
warn(
'Do not replace the Vue.config object, set individual fields instead.'
)
}
}
Object.defineProperty(Vue, 'config', configDef)
// exposed util methods.
// NOTE: these are not considered part of the public API - avoid relying on
// them unless you are aware of the risk.
Vue.util = {
warn,
extend,
mergeOptions,
defineReactive
}
Vue.set = set
Vue.delete = del
Vue.nextTick = nextTick
// 2.6 explicit observable API
Vue.observable = <T>(obj: T): T => {
observe(obj)
return obj
}
Vue.options = Object.create(null)
ASSET_TYPES.forEach(type => {
Vue.options[type + 's'] = Object.create(null)
})
// 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
extend(Vue.options.components, builtInComponents)
initUse(Vue)
initMixin(Vue)
initExtend(Vue)
initAssetRegisters(Vue)
}
在调用 initGlobalAPI 函数中,执行的 initAssetRegisters 函数
// src\shared\constants.ts
export const ASSET_TYPES = ['component', 'directive', 'filter'] as const
// src\core\global-api\assets.ts
export function initAssetRegisters(Vue: GlobalAPI) {
/**
* Create asset registration methods.
*/
ASSET_TYPES.forEach(type => {
// @ts-expect-error function is not exact same 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 (__DEV__ && type === 'component') {
validateComponentName(id)
}
if (type === 'component' && isPlainObject(definition)) {
// @ts-expect-error
definition.name = definition.name || id
definition = this.options._base.extend(definition)
}
if (type === 'directive' && isFunction(definition)) {
definition = { bind: definition, update: definition }
}
this.options[type + 's'][id] = definition
return definition
}
}
})
}
每个组件的创建都是通过 Vue.extend
基础而来的。在 Vue.extend
方法中,会执行 Sub.options = mergeOptions(Super.options, extendOptions)
,把 Vue.options
合并到 Super.options
(即:组件的 options
)。然后,在组件的实例化阶段,执行 merge options
逻辑,把 Sub.options.components
合并到 vm.$options.components
上。
在创建 vnode
过程中,会通过 createElement
方法执行 _createElement
方法。在 _createElement
方法中,会执行判断逻辑 isDef(Ctor = resolveAsset(context.$options, 'components', tag))
。
在 resolveAsset
方法中 :
先通过
const assets = options[type]
获取assets
再获取
assets[id]
,获取规则如下:- 直接使用
id
,获取assets[id]
- 如果不存在,则将
id
变成驼峰的形式,获取assets[id]
- 如果仍然不存在,则在驼峰的基础上,将首字母再变成大写的形式,虎丘
assets[id]
- 如果仍然不存在,则报错
该获取规则,标识在使用
Vue.component(id, definition)
全局注册组件时,id
可以是连字符、驼峰或首字母大写的形式- 直接使用
通过调用 resolveAsset(context.$options, 'components', tag)
(即获取 vm.$options.components[tag]
)获取组件的构造函数,并作为 createComponent
的钩子的参数。
【创建 vnode】过程:通过 createElement 方法执行的 _createElement 方法
export function _createElement(
context: Component,
tag?: string | Component | Function | Object,
data?: VNodeData,
children?: any,
normalizationType?: number
): VNode | Array<VNode> {
if (isDef(data) && isDef((data as any).__ob__)) {
__DEV__ &&
warn(
`Avoid using observed data object as vnode data: ${JSON.stringify(
data
)}\n` + 'Always create fresh vnode data objects in each render!',
context
)
return createEmptyVNode()
}
// object syntax in v-bind
if (isDef(data) && isDef(data.is)) {
tag = data.is
}
if (!tag) {
// in case of component :is set to falsy value
return createEmptyVNode()
}
// warn against non-primitive key
if (__DEV__ && isDef(data) && isDef(data.key) && !isPrimitive(data.key)) {
warn(
'Avoid using non-primitive value as key, ' +
'use string/number value instead.',
context
)
}
// support single function children as default scoped slot
if (isArray(children) && isFunction(children[0])) {
data = data || {}
data.scopedSlots = { default: children[0] }
children.length = 0
}
if (normalizationType === ALWAYS_NORMALIZE) {
children = normalizeChildren(children)
} else if (normalizationType === SIMPLE_NORMALIZE) {
children = simpleNormalizeChildren(children)
}
let vnode, ns
if (typeof tag === 'string') {
let Ctor
ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
if (config.isReservedTag(tag)) {
// platform built-in elements
if (
__DEV__ &&
isDef(data) &&
isDef(data.nativeOn) &&
data.tag !== 'component'
) {
warn(
`The .native modifier for v-on is only valid on components but it was used on <${tag}>.`,
context
)
}
vnode = new VNode(
config.parsePlatformTagName(tag),
data,
children,
undefined,
undefined,
context
)
} else if (
(!data || !data.pre) &&
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 as any, data, context, children)
}
if (isArray(vnode)) {
return vnode
} else if (isDef(vnode)) {
if (isDef(ns)) applyNS(vnode, ns)
if (isDef(data)) registerDeepBindings(data)
return vnode
} else {
return createEmptyVNode()
}
}
【创建 vnode】过程:在 _createElement 方法中,调用的 resolveAsset 方法
// src\core\util\options.ts
/**
* Resolve an asset.
* This function is used because child instances need access
* to assets defined in its ancestor chain.
*/
export function resolveAsset(
options: Record<string, any>,
type: string,
id: string,
warnMissing?: boolean
): any {
/* istanbul ignore if */
if (typeof id !== 'string') {
return
}
const assets = options[type]
// check local registration variations first
if (hasOwn(assets, id)) return assets[id]
const camelizedId = camelize(id)
if (hasOwn(assets, camelizedId)) return assets[camelizedId]
const PascalCaseId = capitalize(camelizedId)
if (hasOwn(assets, PascalCaseId)) return assets[PascalCaseId]
// fallback to prototype chain
const res = assets[id] || assets[camelizedId] || assets[PascalCaseId]
if (__DEV__ && warnMissing && !res) {
warn('Failed to resolve ' + type.slice(0, -1) + ': ' + id)
}
return res
}
局部注册
每个组件的创建都是通过 Vue.extend
基础而来的。在 Vue.extend
方法中,会执行 Sub.options = mergeOptions(Super.options, extendOptions)
,把 Vue.options
合并到 Super.options
(即:组件的 options
)。然后,在组件的实例化阶段,执行 merge options
逻辑,把 Sub.options.components
合并到 vm.$options.components
上。这样可以在执行 resolveAsset
函数的时候,获取到这个组件的构造函数,并作为 createComponent
的参数。
局部注册和全局注册不同的,全局注册是扩展到 Vue.options
下,所以在所有组件创建的过程中,都会从全局的 Vue.options.components
扩展到当前组件的 vm.$options.components
下,这就是全局注册的组件能被任意使用的原因。
异步组件
在大型应用中,为了减少首屏代码体积,会把一些非首屏的组件设计成异步组件,按需加载。主要分为以下三种异步组件。
// 普通函数异步组件
Vue.component('async-example', function (resolve, reject) {
// 这个特殊的 require 语法告诉 webpack,自动将编译后的代码分割成不同的块
// 这些块将通过 Ajax 请求自动下载。
require(['./my-async-component'], resolve)
})
// Promise 异步组件
Vue.component(
'async-webpack-example',
// 该 `import` 函数返回一个 `Promise` 对象。
() => import('./my-async-component')
)
// 高级异步组件
const AsyncComp = () => ({
// 需要加载的组件。应当是一个 Promise
component: import('./MyComp.vue'),
// 加载中应当渲染的组件
loading: LoadingComp,
// 出错时渲染的组件
error: ErrorComp,
// 渲染加载中组件前的等待时间。默认:200ms。
delay: 200,
// 最长等待时间。超出此时间则渲染错误组件。默认:Infinity
timeout: 3000
})
Vue.component('async-example', AsyncComp)
在 render
过程中,通过 createComponent
创建组件 VNode 。
在异步组件场景下,createComponent(Ctor, data, context, children, tag)
中,Ctor
传入的是一个函数,并不会执行 Vue.extend
逻辑,因此它的 cid
是 undefined
,进入了异步组件创建的处理逻辑,执行 Ctor = resolveAsyncComponent(asyncFactory, baseCtor)
。
【render】过程:通过 createComponent 创建 VNode
export function createComponent(
Ctor: typeof Component | Function | ComponentOptions | void,
data: VNodeData | undefined,
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 as typeof Component)
}
// if at this stage it's not a constructor or an async component factory,
// reject.
if (typeof Ctor !== 'function') {
if (__DEV__) {
warn(`Invalid Component definition: ${String(Ctor)}`, context)
}
return
}
// async component
let asyncFactory
// @ts-expect-error
if (isUndef(Ctor.cid)) {
asyncFactory = Ctor
Ctor = resolveAsyncComponent(asyncFactory, baseCtor)
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 as typeof Component)
// transform component v-model data into props & events
if (isDef(data.model)) {
// @ts-expect-error
transformModel(Ctor.options, data)
}
// extract props
// @ts-expect-error
const propsData = extractPropsFromVNodeData(data, Ctor, tag)
// functional component
// @ts-expect-error
if (isTrue(Ctor.options.functional)) {
return createFunctionalComponent(
Ctor as typeof Component,
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
// @ts-expect-error
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
// @ts-expect-error
const name = getComponentName(Ctor.options) || tag
const vnode = new VNode(
// @ts-expect-error
`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
data,
undefined,
undefined,
undefined,
context,
// @ts-expect-error
{ Ctor, propsData, listeners, tag, children },
asyncFactory
)
return vnode
}
创建异步组件: resolveAsyncComponent 函数
export function resolveAsyncComponent(
factory: { (...args: any[]): any; [keye: string]: any },
baseCtor: typeof Component
): typeof Component | void {
if (isTrue(factory.error) && isDef(factory.errorComp)) {
return factory.errorComp
}
if (isDef(factory.resolved)) {
return factory.resolved
}
const owner = currentRenderingInstance
if (owner && isDef(factory.owners) && factory.owners.indexOf(owner) === -1) {
// already pending
factory.owners.push(owner)
}
if (isTrue(factory.loading) && isDef(factory.loadingComp)) {
return factory.loadingComp
}
if (owner && !isDef(factory.owners)) {
const owners = (factory.owners = [owner])
let sync = true
let timerLoading: number | null = null
let timerTimeout: number | null = null
owner.$on('hook:destroyed', () => remove(owners, owner))
const forceRender = (renderCompleted: boolean) => {
for (let i = 0, l = owners.length; i < l; i++) {
owners[i].$forceUpdate()
}
if (renderCompleted) {
owners.length = 0
if (timerLoading !== null) {
clearTimeout(timerLoading)
timerLoading = null
}
if (timerTimeout !== null) {
clearTimeout(timerTimeout)
timerTimeout = null
}
}
}
const resolve = once((res: Object | Component) => {
// cache resolved
factory.resolved = ensureCtor(res, baseCtor)
// invoke callbacks only if this is not a synchronous resolve
// (async resolves are shimmed as synchronous during SSR)
if (!sync) {
forceRender(true)
} else {
owners.length = 0
}
})
const reject = once(reason => {
__DEV__ &&
warn(
`Failed to resolve async component: ${String(factory)}` +
(reason ? `\nReason: ${reason}` : '')
)
if (isDef(factory.errorComp)) {
factory.error = true
forceRender(true)
}
})
const res = factory(resolve, reject)
if (isObject(res)) {
if (isPromise(res)) {
// () => Promise
if (isUndef(factory.resolved)) {
res.then(resolve, reject)
}
} else if (isPromise(res.component)) {
res.component.then(resolve, reject)
if (isDef(res.error)) {
factory.errorComp = ensureCtor(res.error, baseCtor)
}
if (isDef(res.loading)) {
factory.loadingComp = ensureCtor(res.loading, baseCtor)
if (res.delay === 0) {
factory.loading = true
} else {
// @ts-expect-error NodeJS timeout type
timerLoading = setTimeout(() => {
timerLoading = null
if (isUndef(factory.resolved) && isUndef(factory.error)) {
factory.loading = true
forceRender(false)
}
}, res.delay || 200)
}
}
if (isDef(res.timeout)) {
// @ts-expect-error NodeJS timeout type
timerTimeout = setTimeout(() => {
timerTimeout = null
if (isUndef(factory.resolved)) {
reject(__DEV__ ? `timeout (${res.timeout}ms)` : null)
}
}, res.timeout)
}
}
}
sync = false
// return in case resolved synchronously
return factory.loading ? factory.loadingComp : factory.resolved
}
}
普通函数异步组件
// 普通函数异步组件
Vue.component('async-example', function (resolve, reject) {
// 这个特殊的 require 语法告诉 webpack,自动将编译后的代码分割成不同的块
// 这些块将通过 Ajax 请求自动下载。
require(['./my-async-component'], resolve)
})
针对普通函数的情况,在 resolveAsyncComponent
创建异步组件函数中:
对当前渲染的实例(
currentRenderingInstance
,从render
模块中获取)进行判断,是考虑到多个地方同时初始化一个异步组件,实际应该只加载一次。进入实际加载逻辑,定义了
forceRender
、resolve
和reject
函数,其中resolve
和reject
函数使用once
函数进行了包装,确保只执行一次。forceRender
函数const forceRender = (renderCompleted: boolean) => { for (let i = 0, l = owners.length; i < l; i++) { owners[i].$forceUpdate() } if (renderCompleted) { owners.length = 0 if (timerLoading !== null) { clearTimeout(timerLoading) timerLoading = null } if (timerTimeout !== null) { clearTimeout(timerTimeout) timerTimeout = null } } }
resolve
函数const resolve = once((res: Object | Component) => { // cache resolved factory.resolved = ensureCtor(res, baseCtor) // invoke callbacks only if this is not a synchronous resolve // (async resolves are shimmed as synchronous during SSR) if (!sync) { forceRender(true) } else { owners.length = 0 } })
reject
函数const reject = once(reason => { __DEV__ && warn( `Failed to resolve async component: ${String(factory)}` + (reason ? `\nReason: ${reason}` : '') ) if (isDef(factory.errorComp)) { factory.error = true forceRender(true) } })
once
函数接收一个函数参数,并返回一个新的函数。利用闭包和标志位保证通过once
包装的函数只执行一次。/** * Ensure a function is called only once. */ export function once<T extends (...args: any[]) => any>(fn: T): T { let called = false return function () { if (!called) { called = true fn.apply(this, arguments as any) } } as any }
通过执行
const res = factory(resolve, reject)
逻辑,执行定义普通函数异步组件的工厂函数,同时将resolve
和reject
函数作为参数传入。异步组件的工厂函数通常会发送请求加载异步组件的 JavaScript 文件,获取到定义的对象res
后,执行resolve(res)
逻辑。执行定义的
resolve(res)
函数,其中:执行
factory.resolved = ensureCtor(res, baseCtor)
逻辑。ensureCtor
函数保证能找到异步组件 JavaScript 定义的组件对象,并且如果是一个普通对象,则调用Vue.extend
将其转换成一个组件的构造函数。function ensureCtor(comp: any, base) { if (comp.__esModule || (hasSymbol && comp[Symbol.toStringTag] === 'Module')) { comp = comp.default } return isObject(comp) ? base.extend(comp) : comp }
对
sync
标识进行判断,在该场景下sync
为false
,则会执行forceRender
函数
执行
forceRender
函数,遍历factory.contexts
获取每一个调用异步组件的实例vm
,执行vm.$forceUpdate()
方法,其定义在lifecycle
模块中。在
$forceUpdate()
方法中,调用渲染watcher
的update
方法,让渲染watcher
对应的回调函数执行,触发组件的重新渲染。因为 Vue 通常是数据驱动视图重新渲染,在整个异步组件加载过程中是没有数据变化的,所以通过执行$forceUpdate()
强制组件重新渲染一次。// src\core\instance\lifecycle.ts Vue.prototype.$forceUpdate = function () { const vm: Component = this if (vm._watcher) { vm._watcher.update() } }
Promise 异步组件
// Promise 异步组件
Vue.component(
'async-webpack-example',
// 该 `import` 函数返回一个 `Promise` 对象。
() => import('./my-async-component')
)
webpack 2+ 支持了异步加载的语法糖:() => import('./my-async-component')
当在 resolveAsyncComponent
创建异步组件函数中,执行 const res = factory(resolve, reject)
,得到的返回值就是 import('./my-async-component')
的返回值,是一个 Promise
对象。进入后续判断逻辑,满足 isPromise(res)
,执行如下逻辑:
if (isUndef(factory.resolved)) {
res.then(resolve, reject)
}
- 组件异步加载成功,则执行在
resolveAsyncComponent
函数中定义的resolve(res)
函数 - 组件异步加载失败,则执行在
resolveAsyncComponent
函数中定义的reject(res)
函数
高级异步组件
高级异步组件支持设置 loading 组件与 error 组件,用于处理组件异步加载过程中与加载失败的场景。
// 高级异步组件
const AsyncComp = () => ({
// 需要加载的组件。应当是一个 Promise
component: import('./MyComp.vue'),
// 加载中应当渲染的组件
loading: LoadingComp,
// 出错时渲染的组件
error: ErrorComp,
// 渲染加载中组件前的等待时间。默认:200ms。
delay: 200,
// 最长等待时间。超出此时间则渲染错误组件。默认:Infinity
timeout: 3000
})
Vue.component('async-example', AsyncComp)
当在 resolveAsyncComponent
创建异步组件函数中,执行 const res = factory(resolve, reject)
,得到的返回值为定义的组件对象。进入后续判断逻辑,满足 isPromise(res.component)
,执行 res.component.then(resolve, reject)
。
- 组件异步加载成功,则执行在
resolveAsyncComponent
函数中定义的resolve(res)
函数 - 组件异步加载失败,则执行在
resolveAsyncComponent
函数中定义的reject(res)
函数
因为异步组件加载是一个异步过程,同时,又会同步执行在 resolveAsyncComponent
中的如下逻辑:
- 判断
res.error
是否定义了error
组件。如果有的话,则赋值给factory.errorComp
。 - 判断
res.loading
是否定义了loading
组件,如果有的话,则赋值给factory.loadingComp
。 - 如果设置了
res.delay
且为0
,则设置factory.loading = true
,否则,延时delay
的时间执行 - 最后,判断
res.timeout
,如果配置了该项,则在res.timout
时间后,如果组件没有成功加载,执行reject
在 resolveAsyncComponent
函数的最后,设置 sync = false
,同时执行 factory.loading ? factory.loadingComp : factory.resolved
。
- 如果
delay
配置为0
,则这次直接渲染loading
组件 - 否则,则延时
delay
执行forceRender
,会再一次执行到resolveAsyncComponent
此时,会存在几种情况,按照逻辑的执行顺序,对不同情况进行判断:
异步组件加载失败,会执行
reject
函数。const reject = once(reason => { __DEV__ && warn( `Failed to resolve async component: ${String(factory)}` + (reason ? `\nReason: ${reason}` : '') ) if (isDef(factory.errorComp)) { factory.error = true forceRender(true) } })
此时,会把
factory.error
设置为true
,同时执行forceRender()
再次执行到resolveAsyncComponent
,会在该方法中执行到以下逻辑,返回factory.errorComp
,直接渲染error
组件if (isTrue(factory.error) && isDef(factory.errorComp)) { return factory.errorComp }
异步组件加载成功,会执行
resolve
函数const resolve = once((res: Object | Component) => { // cache resolved factory.resolved = ensureCtor(res, baseCtor) // invoke callbacks only if this is not a synchronous resolve // (async resolves are shimmed as synchronous during SSR) if (!sync) { forceRender(true) } else { owners.length = 0 } })
此时,会把加载结果缓存到
factory.resolved
中,这个时候因为sync
已经为false
,则执行forceRender()
再次执行到resolveAsyncComponen
,会在该方法中执行到以下逻辑,返回factory.resolved
,直接渲染成功加载的组件if (isDef(factory.resolved)) { return factory.resolved }
异步组件加载中,未成功返回,会执行到以下逻辑,返回
factory.loadingComp
,渲染loading
组件if (isTrue(factory.loading) && isDef(factory.loadingComp)) { return factory.loadingComp }
异步组件加载超时,会进入
reject
逻辑,之后逻辑和加载失败一样,渲染error
组件。
异步组件 patch
在 createComponent
创建组件 VNode 方法中,通过 resolveAsyncComponen
函数完成异步组件创建后,会对返回值进行逻辑处理。
【render】过程:通过的 createComponent 方法创建 VNode
export function createComponent(
Ctor: typeof Component | Function | ComponentOptions | void,
data: VNodeData | undefined,
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 as typeof Component)
}
// if at this stage it's not a constructor or an async component factory,
// reject.
if (typeof Ctor !== 'function') {
if (__DEV__) {
warn(`Invalid Component definition: ${String(Ctor)}`, context)
}
return
}
// async component
let asyncFactory
// @ts-expect-error
if (isUndef(Ctor.cid)) {
asyncFactory = Ctor
Ctor = resolveAsyncComponent(asyncFactory, baseCtor)
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 as typeof Component)
// transform component v-model data into props & events
if (isDef(data.model)) {
// @ts-expect-error
transformModel(Ctor.options, data)
}
// extract props
// @ts-expect-error
const propsData = extractPropsFromVNodeData(data, Ctor, tag)
// functional component
// @ts-expect-error
if (isTrue(Ctor.options.functional)) {
return createFunctionalComponent(
Ctor as typeof Component,
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
// @ts-expect-error
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
// @ts-expect-error
const name = getComponentName(Ctor.options) || tag
const vnode = new VNode(
// @ts-expect-error
`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
data,
undefined,
undefined,
undefined,
context,
// @ts-expect-error
{ Ctor, propsData, listeners, tag, children },
asyncFactory
)
return vnode
}
如果是第一次执行 resolveAsyncComponent
,除非使用高级异步组件 0 delay
去创建了一个 loading
组件,否则返回是 undefiend
,接着通过 createAsyncPlaceholder
创建一个注释节点作为占位符,同时把 asyncFactory
和 asyncMeta
赋值给当前 vnode
。
createAsyncPlaceholder 方法
export function createAsyncPlaceholder(
factory: Function,
data: VNodeData | undefined,
context: Component,
children: Array<VNode> | undefined,
tag?: string
): VNode {
const node = createEmptyVNode()
node.asyncFactory = factory
node.asyncMeta = { data, context, children, tag }
return node
}
当执行 forceRender
的时候,会触发组件的重新渲染,会再一次执行 resolveAsyncComponent
。此时,会根据不同的情况,可能返回 loading
、error
或成功加载的异步组件,返回值不为 undefined
,因此会走正常的组件 render
、patch
过程,与组件第一次渲染流程不一样,这个时候是存在新旧 vnode
的,会进行组件更新的 patch
过程。