【七】Vue2 源码 — 组件详解
Vue.js 版本为 v2.6.14
Vue.js 另一个核心思想是组件化。所谓组件化,就是把页面拆分成多个组件(component),每个组件依赖的 CSS、JavaScript、模板、图片等资源放在一起维护。组件是资源独立的,组件在系统内部可复用,组件和组件之间可以嵌套。
这一章,将从源码的角度分析 Vue 组件内部是如何工作的,接下来我们分析一下 Vue 组件初始化的过程。
import Vue from 'vue'
import App from './App.vue'
var app = new Vue({
el: '#app',
// 这里的 h 是 createElement 方法
render: (h) => h(App),
})
这段代码跟之前我们写过的一个例子比较相似,也是通过 render
函数去渲染的,不同的是,这次 createElement
传的参数是一个组件而不是一个原生的标签,接下来我们就分析这一过程。
组件创建
createElement
之前我们分析 createElement
实现的时候知道,它最终会调用 _createElement
方法,其中有一段逻辑是对参数 tag
的判断,如果是一个普通的 html 标签,例如一个普通的 div,则会实例化一个普通 VNode 节点,否则通过 createComponent
方法创建一个组件 VNode。
/** src/core/vdom/cteate-element.js */
export function _createElement(
context: Component,
tag: any,
data: any,
children: any,
normalizationType: any,
alwaysNormalize: boolean
) {
//……
if (typeof tag === 'string') {
let Ctor
ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
if (config.isReservedTag(tag)) {
// platform built-in elements
if (
process.env.NODE_ENV !== 'production' &&
isDef(data) &&
isDef(data.nativeOn)
) {
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, data, context, children)
}
//……
}
在本章最开始的例子中,我们给 createElement
传入的是一个 APP 对象,它本质上是一个 Component
类型,那么会走到上述代码的 else
逻辑,直接通过 createComponent
方法来创建 vnode
。
createElement
方法的实现定义在 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)
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 主要就 3 个关键步骤:
构造子类构造函数
安装组件钩子函数
实例化
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 这个构造函数了。
在了解了 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)
}
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
}
}
Vue.extend
的作用就是构造一个 Vue
的子类,它使用一种非常经典的原型继承的方式把一个纯对象转换成一个继承 Vue
构造函数的 Sub
并返回,然后对 Sub
这个对象本身扩展了一些属性,如:扩展 options
、添加全局 API 等;并且对配置中的 props
和 computed
做了初始化工作;最后对这个 Sub
构造函数做了缓存,避免多次执行 Vue.extend
的时候对同一个子组件重复构造。
这样当我们去实例化 Sub
的时候,执行 this._init
逻辑就会再次走到 Vue
实例的初始化逻辑。
const Sub = function VueComponent(options) {
this._init(options)
}
安装组件钩子函数
// install component management hooks onto the placeholder node
installComponentHooks(data)
我们之前提到过,Vue.js 使用的 Virtual DOM 参考的是开源库 snabbdom,它的一个特点是在 VNode 的 patch 流程中对外暴露了各种时机的钩子函数,方便我们做一些额外的事情, Vue.js 也是充分利用这一点,在初始化一个 Component 类型的 VNode 的过程中实现了几个钩子函数:
/** src/core/vdom/create-component.js */
// inline hooks to be invoked on component VNodes during patch
const componentVNodeHooks = {
init(vnode: VNodeWithData, hydrating: boolean): ?boolean {
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)
//…… 省略了中间的 createComponent
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]
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
}
整个 installComponentHooks
的过程就是把 componentVNodeHooks
的钩子函数合并到 data.hook
中,在 VNode 执行 patch
的过程中执行相关的钩子函数,稍后在介绍 patch
过程时会详细介绍具体的执行。这里要注意的是合并策略,在合并过程中,如果某个时机的钩子已经存在 data.hook
中,那么通过执行 mergeHook
函数做合并,这个逻辑很简单,就是在最终执行的时候,依次执行这两个钩子函数即可。
实例化 VNode
// 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
)
return vnode
最后一步非常简单,通过 new VNode
实例化一个 vnode
并返回。需要注意的是,这里和普通元素节点的 vnode
不同,组件的 vnode
是没有 children
的,这点很关键,在之后的 patch
过程中会再提及。
以上是 createComponent
的实现分析,在渲染一个组件的时候有 3 个关键逻辑:构造子类构造函数、安装组件钩子函数、实例化 vnode
。
patch
createComponent
创建了组件 VNode,接下来会走到 vm._update
方法,进而执行 vm.__patch__
函数去把 VNode 转换成真正的 DOM 节点。
patch 过程会调用 createElm
创建元素节点,createElm
定义在 src/core/vdom/patch.js
文件中:
// 删掉多余的代码,只保留关键的逻辑。
function createElm(
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
) {
// ……
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
// ……
}
这里会判断 createComponent(vnode, insertedVnodeQueue, parentElm, refElm)
的返回值,如果为 true
则直接结束。createComponent
方法也是定义在 src/core/vdom/patch.js
文件中:
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
}
}
}
createComponent
函数中,首先对 vnode.data
做了一些判断:
let i = vnode.data
if (isDef(i)) {
// ……
if (isDef((i = i.hook)) && isDef((i = i.init))) {
i(vnode, false /* hydrating */)
}
// ……
}
如果 vnode
是一个组件 VNode,那么条件会满足,并且得到 i
就是 init
钩子函数,回顾上面我们在创建组件 VNode 的时候合并钩子函数中就包含 init
钩子函数,定义在 src/core/vdom/create-component.js
中:
init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
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)
}
}
init
钩子函数执行也很简单(我们先不考虑 keepAlive
的情况),它是通过 createComponentInstanceForVnode
创建一个 Vue 的实例,然后调用 $mount
方法挂载子组件。createComponentInstanceForVnode
的实现也在 src/core/vdom/create-component.js
文件中:
export function createComponentInstanceForVnode(
vnode: any, // we know it's MountedComponentVNode but flow doesn't
parent: any // activeInstance in lifecycle state
): 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)
}
createComponentInstanceForVnode
函数构造了一个内部组件的参数,然后执行 new vnode.componentOptions.Ctor(options)
。这里的 vnode.componentOptions.Ctor
对应的就是子组件的构造函数,上面我们分析了它实际上是继承于 Vue 的一个构造函数 Sub
,所以这里相当于是 new Sub(options)
。这里有几个关键参数要注意: _isComponent
为 true
表示它是一个组件,parent
表示当前激活的组件实例。
所以子组件的实例化实际上就是在这个时机执行的,并且它会执行实例的 _init
方法,这个过程有一些和之前不同的地方需要挑出来说, _init
代码在 src/core/instance/init.js
文件中:
Vue.prototype._init = function(options?: Object) {
const vm: Component = this
//……
// 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)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
//……
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
这里首先是合并 options
的过程有变化, _isComponent
为 true
,所以走到了 initInternalComponent
过程。initInternalComponent
代码也在 src/core/instance/init.js
文件中:
export function initInternalComponent(
vm: Component,
options: InternalComponentOptions
) {
const opts = (vm.$options = Object.create(vm.constructor.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
}
}
这个过程重点记住以下几个点即可:opts.parent = options.parent
、opts._parentVnode = parentVnode
,它们是把我们通过 createComponentInstanceForVnode
函数传入的几个参数合并到内部的选项 $options
里了。
再来看一下 _init
函数最后执行的代码:
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
由于组件初始化的时候是不传 el
的,因此组件是自己接管了 $mount
的过程,回到组件 init
的过程, componentVNodeHooks
的 init
钩子函数,在完成实例化的 _init
后,接着会执行 child.$mount(hydrating ? vnode.elm : undefined, hydrating)
。这里 hydrating
为 true
一般是服务端渲染的情况,我们现在只考虑客户端的渲染,所以这里 $mount
相当于执行 child.$mount(undefined, false)
,它最终会调用 mountComponent
方法,进而执行 vm._render
方法,vm._render
定义在 src/core/instance/render.js
文件中:
Vue.prototype._render = function(): VNode {
const vm: Component = this
const { render, _parentVnode } = vm.$options
//……
// 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 {
//……
vnode = render.call(vm._renderProxy, vm.$createElement)
} catch (e) {
//……
} finally {
//……
}
//……
// set parent
vnode.parent = _parentVnode
return vnode
}
上面的代码只保留了关键部分,这里的 _parentVnode
就是当前组件的父 VNode,而 render
函数生成的 vnode
就是当前组件的渲染 vnode
,vnode
的 parent
指向了 _parentVnode
,也就是 vm.$vnode
它们是一种父子关系。
我们知道在执行完 vm._render
,生成 VNode 后,接下来就要执行 vm._update
去渲染 VNode 了。组件渲染的过程也有一些需要注意的, vm._update
定义在 src/core/instance/lifecycle.js
文件中:
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.
}
_update
过程中有几个关键的代码:首先 vm._vnode = vnode
的逻辑,这个 vnode
是通过 vm._render()
返回的组件渲染 VNode,vm._vnode
和 vm.$vnode
的关系就是一种父子关系,用代码表达就是 vm._vnode.parent === vm.$vnode
。还有一段比较有意思的代码:
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 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()
//……
}
这里的 activeInstance
作用就是保持当前上下文的 Vue 实例,它是在 src/core/instance/lifecycle.js
文件中定义的全局变量 export let activeInstance: any = null
,并且在之前我们调用 createComponentInstanceForVnode
方法的时候,就是从 src/core/instance/lifecycle.js
中引入的 activeInstance
并作为参数传入到 createComponentInstanceForVnode
中的。因为实际上 JavaScript 引擎是单线程的,Vue 整个初始化是一个深度遍历的过程,在实例化子组件的过程中,它需要知道当前上下文的 Vue 实例是什么,并把它作为子组件的父 Vue 实例。之前我们提到过对子组件的实例化过程会先调用 initInternalComponent(vm, options)
合并 options
,把 parent
存储在 vm.$options
中,在 $mount
之前会调用 initLifecycle(vm)
方法:
/** src/core/instance/lifecycle.js */
export function initLifecycle(vm: Component) {
const options = vm.$options
// locate first non-abstract parent
let parent = options.parent
if (parent && !options.abstract) {
while (parent.$options.abstract && parent.$parent) {
parent = parent.$parent
}
parent.$children.push(vm)
}
vm.$parent = parent
//……
}
可以看到 vm.$parent
就是用来保存当前 vm
的父实例,并且通过 parent.$children.push(vm)
,把当前的 vm
存储到父实例的 $children
中。
在 vm._update
的过程中,把当前的 vm
赋值给 activeInstance
,同时通过 const prevActiveInstance = activeInstance
用 prevActiveInstance
保留上一次的 activeInstance
。实际上, prevActiveInstance
和当前的 vm
是一个父子关系,当一个 vm
实例完成它的所有子树的 patch 或者 update 过程后, activeInstance
会回到它的父实例,这样就完美地保证了 createComponentInstanceForVnode
整个深度遍历过程中,在实例化子组件的时候能传入当前子组件的父 Vue 实例,并在 _init
的过程中,通过 vm.$parent
把这个父子关系保留。
那么回到 _update
,最后就是调用 __patch__
渲染 VNode 了。
/** src/core/instance/lifecycle.js */
// initial render
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
/** src/core/vdom/patch.js */
function patch(oldVnode, vnode, hydrating, removeOnly) {
//……
let isInitialPatch = false
const insertedVnodeQueue = []
if (isUndef(oldVnode)) {
// empty mount (likely as component), create new root element
isInitialPatch = true
createElm(vnode, insertedVnodeQueue)
} else {
//……
}
//……
}
这里又回到了之前开始的过程,之前分析过负责渲染成 DOM 的函数是 createElm
,createElm
也是定义在 src/core/vdom/patch.js
文件中,不过要注意这里我们只传了 2 个参数,所以对应的 parentElm
是 undefined
。
function createElm(
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
) {
//……
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
const data = vnode.data
const children = vnode.children
const tag = vnode.tag
if (isDef(tag)) {
//……
vnode.elm = vnode.ns
? nodeOps.createElementNS(vnode.ns, tag)
: nodeOps.createElement(tag, vnode)
setScope(vnode)
/* istanbul ignore if */
if (__WEEX__) {
//……
} else {
createChildren(vnode, children, insertedVnodeQueue)
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue)
}
insert(parentElm, vnode.elm, refElm)
}
//……
} 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)
}
}
注意,这里我们传入的 vnode
是组件渲染的 vnode
,也就是我们之前说的 vm._vnode
,如果组件的根元素是个普通元素,那么 vm._vnode
也是普通的 vnode
,这里 createComponent(vnode, insertedVnodeQueue, parentElm, refElm)
的返回值是 false
。那么接下来的过程就跟之前说过的一样,先创建一个父节点占位符,然后再遍历所有子 VNode 递归调用 createElm
,在遍历的过程中,如果遇到子 VNode 是一个组件的 VNode,则重复本节开始的过程,这样通过一个递归的方式就可以完整地构建了整个组件树。
由于我们这个时候传入的 parentElm
是空,所以对组件的插入,在 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
}
}
}
在完成组件的整个 patch
过程后,最后执行 insert(parentElm, vnode.elm, refElm)
完成组件的 DOM 插入,如果组件 patch
过程中又创建了子组件,那么 DOM 的插入顺序是先子后父。
合并配置
通过之前的源码分析我们知道, new Vue
的过程通常由 2 种场景,一种是外部用户主动调用 new Vue(options)
的方式实例化一个 Vue 对象;另一种是我们上面分析的组件创建过程中内部通过 new Vue(options)
实例化子组件。
无论哪种场景,都会执行实例的 _init(options)
方法,它首先会先执行一个 merge options
的逻辑,相关代码在 src/core/instance/init.js
中:
Vue.prototype._init = function(options?: Object) {
const vm: Component = this
//……
// 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)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
//……
}
可以看到不同场景对于 options
的合并逻辑是不一样的,并且传入的 options
值也有非常大的不同,接下来分别介绍 2 中场景的 options 合并过程。
为了更直观,我们可以举个简单的示例:
import Vue from 'vue'
let childComp = {
template: '<div>{{msg}}</div>',
created() {
console.log('child created')
},
mounted() {
console.log('child mounted')
},
data() {
return {
msg: 'Hello Vue',
}
},
}
Vue.mixin({
created() {
console.log('parent created')
},
})
let app = new Vue({
el: '#app',
render: (h) => h(childComp),
})
外部调用场景
当执行 new Vue
的时候,执行 this._init(options)
就会执行如下逻辑去合并 options
:
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
这里通过调用 mergeOptions
方法来合并,它实际上就是把 resolveConstructorOptions(vm.constructor)
的返回值和 options
做合并,resolveConstructorOptions
我们之前分析过,在我们这个场景下,它还是简单返回 vm.constructor.options
,而 vm.constructor
就是 Vue,所以相当于 Vue.options
,Vue.options
其实是在 initGlobalAPI(Vue)
的时候定义的,代码在 src/core/global-api/index.js
中:
export function initGlobalAPI(Vue: GlobalAPI) {
//……
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)
//……
}
这里之前分析过,整理了 Vue.options
应该是如下结构:
Vue.options = {
components: {
KeepAlive,
Transition,
TransitionGroup
},
directives: {
model,
show
},
filters: {},
_base: Vue
}
所以刚开始的示例最后 vm.$options
的值差不多是如下这样:
vm.$options = {
components: {
KeepAlive,
Transition,
TransitionGroup,
},
created: [
function created() {
console.log('parent created')
},
],
directives: {
model,
show,
},
filters: {},
_base: function Vue(options) {
// ...
},
el: '#app',
render: function(h) {
//...
},
}
组件场景
由于组件的构造函数是通过 Vue.extend
继承自 Vue
的,先回顾一下这个过程,代码定义在 src/core/global-api/extend.js
中:
/**
* Class inheritance
*/
Vue.extend = function (extendOptions: Object): Function {
//……
Sub.options = mergeOptions(
Super.options,
extendOptions
)
//……
// 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)
//……
return Sub
}
}
上面的代码只保留了关键逻辑,这里的 extendOptions
对应的就是前面定义的组件对象,它会和 Vue.options
合并到 Sub.options
中。
接下来,我们再回忆一下子组件的初始化过程,代码定义在 src/core/vdom/create-component.js
中:
export function createComponentInstanceForVnode(
vnode: any, // we know it's MountedComponentVNode but flow doesn't
parent: any // activeInstance in lifecycle state
): Component {
const options: InternalComponentOptions = {
_isComponent: true,
_parentVnode: vnode,
parent,
}
//……
return new vnode.componentOptions.Ctor(options)
}
这里的 vnode.componentOptions.Ctor
就是指向 Vue.extend
的返回值 Sub
,所以执行 new vnode.componentOptions.Ctor(options)
接着执行 this._init(options)
因为 options._isComponent
为 true,那么合并 options
的过程就走到了 initInternalComponent(vm, options)
逻辑。initInternalComponent
定义在 src/core/instance/init.js
中:
export function initInternalComponent(
vm: Component,
options: InternalComponentOptions
) {
const opts = (vm.$options = Object.create(vm.constructor.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
}
}
initInternalComponent
方法首先执行 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
只是做了简单一层对象赋值,并不涉及递归,合并策略等复杂逻辑。
因此,在我们当前这个 case 下,执行完如下合并后:
initInternalComponent(vm, options)
vm.$options
的值差不多是如下这样:
vm.$options = {
parent: Vue /*父Vue实例*/,
propsData: undefined,
_componentTag: undefined,
_parentVnode: VNode /*父VNode实例*/,
_renderChildren: undefined,
__proto__: {
components: {
KeepAlive,
Transition,
TransitionGroup,
},
directives: {
model,
show,
},
filters: {},
_base: function Vue(options) {
//...
},
_Ctor: {},
created: [
function created() {
console.log('parent created')
},
function created() {
console.log('child created')
},
],
mounted: [
function mounted() {
console.log('child mounted')
},
],
data() {
return {
msg: 'Hello Vue',
}
},
template: '<div>{{msg}}</div>',
},
}
声明周期
每个 Vue 实例在被创建之前都要经过一系列的初始化过程。例如需要设置数据监听、编译模板、挂载实例到 DOM、在数据变化时更新 DOM 等。同时在这个过程中也会运行一些叫做生命周期钩子的函数,给用户在一些特定的场景下添加他们自己的代码的机会。
在我们实际项目开发过程中,会非常频繁地和 Vue 组件的生命周期打交道,接下来我们就从源码的角度来看一下这些生命周期的钩子函数是如何被执行的。
源码中最终执行生命周期的函数都是调用 callHook
方法,它定义在 src/core/instance/lifecycle.js
中:
export function callHook(vm: Component, hook: string) {
// #7573 disable dep collection when invoking lifecycle hooks
pushTarget()
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, null, vm, info)
}
}
if (vm._hasHookEvent) {
vm.$emit('hook:' + hook)
}
popTarget()
}
callHook
函数的逻辑很简单,根据传入的字符串 hook
,去拿到 vm.$options[hook]
对应的回调函数数组,然后遍历执行,执行的时候把 vm
作为函数执行的上下文。
上一小节,我们详细地介绍了 Vue.js 合并 options
的过程,各个阶段的生命周期的函数也被合并到 vm.$options
里,并且是一个数组。因此 callHook
函数的功能就是调用某个生命周期钩子注册的所有回调函数。
了解了生命周期的执行方式后,接下来我们会具体介绍每一个生命周期钩子函数的调用时机。
beforeCreate & created
beforeCreate
和 created
函数都是在实例化 Vue
的阶段,在 _init
方法中执行的,它定义在 src/core/instance/init.js
中:
Vue.prototype._init = function(options?: Object) {
const vm: Component = this
//……
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')
//……
}
可以看到 beforeCreate
和 created
的钩子调用是在 initState
的前后,initState
的作用是初始化 props
、data
、methods
、watch
、computed
等属性,那么显然 beforeCreate
的钩子函数中就不能获取到 props
、data
中定义的值,也不能调用 methods
中定义的方法。
在这 2 个钩子函数执行的时候,并没有渲染 DOM ,所以我们也不能够访问 DOM,一般来说,如果组件在加载的时候需要和后端有交互,放在这 2 个钩子函数执行都可以,如果是需要访问 props
、data
等数据的话,就需要使用 created
钩子函数。
我们如果去看 vue-router 和 vuex 的时候就会发现它们都混合了 beforeCreate
钩子函数。
beforeMount & mounted
顾名思义,beforeMount
钩子函数发生在 mount
之前,也就是 DOM 挂载之前,它的调用时机是在 mountComponent
函数中,定义在 src/core/instance/lifecycle.js
中:
export function mountComponent(
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
//……
callHook(vm, 'beforeMount')
let updateComponent
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && 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)
}
}
// 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,
{
before() {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
},
},
true /* isRenderWatcher */
)
hydrating = false
// 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
}
在执行 vm._render()
函数渲染 VNode 之前,执行了 beforeMount
钩子函数,在执行完 vm._update()
把 VNode patch 到真实 DOM 后,执行 mounted
钩子。注意,这里对 mounted
钩子函数执行有一个判断逻辑,vm.$vnode
如果为 null
,则表明这不是一次组件的初始化过程,而是我们通过外部 new Vue
初始化过程。
那么对于组件,它的 mounted
时机在哪儿呢?之前我们提到过,组件的 VNode patch 到 DOM 后,会执行 invokeInsertHook
函数,把 insertedVnodeQueue
里保存的钩子函数一次执行一遍,它定义在 src/core/vdom/patch.js
中:
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])
}
}
}
该函数会执行 insert
这个钩子函数,对于组件而言, insert
钩子函数的定义在 src/core/vdom/create-component.js
中的 componentVNodeHooks
中:
// inline hooks to be invoked on component VNodes during patch
const componentVNodeHooks = {
//……
insert(vnode: MountedComponentVNode) {
const { context, componentInstance } = vnode
if (!componentInstance._isMounted) {
componentInstance._isMounted = true
callHook(componentInstance, 'mounted')
}
//……
},
//……
}
我们可以看到,每个子组件都是在这个钩子函数中执行 mounted
钩子函数,并且我们之前分析过, insertedVnodeQueue
的添加顺序是先子后父,所以对于同步渲染的子组件而言,mounted
钩子函数的执行顺序也是先子后父。
beforeUpdate & updated
顾名思义, beforeUpdate
和 updated
的钩子函数执行时机都应该是在数据更新的时候。
beforeUpdate
的执行时机是在渲染 Watcher 的 before
函数中,我们刚才提到了:
/** src/core/instance/lifecycle.js */
export function mountComponent(
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
//……
// 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,
{
before() {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
},
},
true /* isRenderWatcher */
)
//……
}
注意这里有个判断,也就是在组件已经 mounted
之后并且没有 destroyed
,才会去调用这个钩子函数。
update
的执行时机是在 flushSchedulerQueue
函数调用的时候,它定义在 src/core/observer/scheduler.js
中:
/**
* Flush both queues and run the watchers.
*/
function flushSchedulerQueue() {
// ……
callUpdatedHooks(updatedQueue)
// ……
}
function callUpdatedHooks(queue) {
let i = queue.length
while (i--) {
const watcher = queue[i]
const vm = watcher.vm
if (vm._watcher === watcher && vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'updated')
}
}
}
updateQueue
是更新了的 watcher
数组,那么在 callUpdatedHooks
函数中,它对这些数组做遍历,只有满足当前 watcher
为 vm._watcher
, 并且组件已经 mounted
,以及组件没有 destroyed
这三个条件,才会执行 updated
钩子函数。
我们之前提到过,在组件 mount 的过程中,会实例化一个渲染的 Watcher
去监听 vm
上的数据变化重新渲染,这段逻辑发生在 mountComponent
函数执行的时候:
/** src/core/instance/lifecycle.js */
export function mountComponent(
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
let updateComponent
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
//……
} else {
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
}
new Watcher(
vm,
updateComponent,
noop,
{
before() {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
},
},
true /* isRenderWatcher */
)
}
那么在实例化 Watcher
的过程中,在它的构造函数里会判断 isRenderWatcher
,接着把当前 watcher
的实例赋值给 vm._watcher
, 定义在 src/core/observer/watcher.js
中:
export default class Watcher {
constructor(
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
this.vm = vm
if (isRenderWatcher) {
vm._watcher = this
}
vm._watchers.push(this)
//……
}
}
同时,还把当前 watcher
实例 push 到 vm._watchers
中, vm._watcher
是专门用来监听 vm
上数据变化然后重新渲染的 watcher,因此在 callUpdatedHooks
函数中,只有 vm._watcher
的回调执行完毕后,才会执行 updated
钩子函数。
beforeDestroy & destroyed
顾名思义,beforeDestroy
和 destroyed
钩子函数的执行时机在组件销毁的阶段,组件销毁过程最终会调用 $destroy
方法,它定义在 src/core/instance/lifecycle.js
中:
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 watchers
if (vm._watcher) {
vm._watcher.teardown()
}
let i = vm._watchers.length
while (i--) {
vm._watchers[i].teardown()
}
// 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
}
}
}
beforeDestroy
钩子函数的执行时机在 $destroy
函数执行最开始的地方,接着执行了一系列的销毁动作,包括从 parent
的 $children
中删掉自身,删除 watcher
,当前渲染的 VNode 执行销毁钩子函数等,执行完毕后再调用 destroyed
钩子函数。
在 $destroy
的执行过程中,它又会执行 vm.__patch__(vm._vnode, null)
触发它的子组件的销毁钩子函数,这样一层层的递归调用,所以 destroyed
钩子函数执行顺序是先子后父,和 mounted
过程一样。
activated & deactivated
activated
和 deactivated
钩子函数是专门为 keep-alive
组件定制的钩子函数。
组件注册
在 Vue.js 中,除了它内置的组件,如 keep-alive
、transition
、 transition-group
等,其他用户自定义组件在使用前必须注册。在开发过程中可能会遇到如下报错信息:
'Unknown custom element: <xxx> - did you register the component correctly?
For recursive components, make sure to provide the "name" option.'
一般报这个错的原因都是使用了未注册的组件。Vue.js 提供了 2 种组件的注册方式,全局注册和局部注册。接下来从源码的角度来分析这两种注册方式:
全局注册
要注册一个全局组件,可以使用 Vue.component(tagName, options)
。例如:
Vue.component('my-component', {
// 选项
})
那么, Vue.component
函数是在什么时候定义的呢,它的定义过程发生在最开始初始化 Vue 的全局函数的时候,代码在 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)
}
if (type === 'component' && isPlainObject(definition)) {
definition.name = definition.name || id
definition = this.options._base.extend(definition)
}
if (type === 'directive' && typeof definition === 'function') {
definition = { bind: definition, update: definition }
}
this.options[type + 's'][id] = definition
return definition
}
}
})
}
函数首先遍历 ASSET_TYPES
,得到 type
后挂载到 Vue 上。 ASSET_TYPES
定义在 src/shared/constants.js
中:
export const ASSET_TYPES = ['component', 'directive', 'filter']
所以在 initAssetRegisters
方法中,实际上 Vue 是初始化了 3 个全局函数,并且如果 type
是 component
且 definition
是一个对象的话,通过 this.options._base.extend
,相当于 Vue.extend
把这个对象转换成一个继承于 Vue 的构造函数,最后通过 this.options[type + 's'][id] = definition
把它挂载到 Vue.options.components
上。
由于我们每个组件的创建都是通过 Vue.extend
继承而来,我们之前分析过在继承的过程中有那么一段逻辑:
Sub.options = mergeOptions(Super.options, extendOptions)
也就是说它会把 Vue.options
合并到 Sub.options
,也就是组件的 options
上,然后在组件的实例化阶段,会执行 merge options
逻辑,把 Sub.options.components
合并到 vm.$options.components
上。
然后在创建 vnode
的过程中,会执行 _createElement
方法,我们再来回顾一下这部分的逻辑,它定义在 src/core/vdom/create-element.js
中:
export function _createElement(
context: Component,
tag?: string | Class<Component> | Function | Object,
data?: VNodeData,
children?: any,
normalizationType?: number
): VNode | Array<VNode> {
// ……
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 (
process.env.NODE_ENV !== 'production' &&
isDef(data) &&
isDef(data.nativeOn)
) {
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, data, context, children)
}
// ……
}
这里有一个判断逻辑 isDef((Ctor = resolveAsset(context.$options, 'components', tag)))
先来看一下 resolveAsset
的定义,在 src/core/utils/options.js
中:
/**
* Resolve an asset.
* This function is used because child instances need access
* to assets defined in its ancestor chain.
*/
export function resolveAsset(
options: Object,
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 (process.env.NODE_ENV !== 'production' && warnMissing && !res) {
warn('Failed to resolve ' + type.slice(0, -1) + ': ' + id, options)
}
return res
}
这段逻辑很简单,先通过 const assets = options[type]
拿到 assets
,然后再尝试拿 assets[id]
,这里有个顺序,先直接使用 id
拿,如果不存在,则把 id
变成驼峰的形式再拿,如果仍然不存在,则在驼峰的基础上把首字母再变成大写的形式(大驼峰)再拿,如果仍然拿不到则报错。这样说明了我们在使用 Vue.component(id, definition)
全局注册组件的时候,id 可以是连字符、小驼峰或大驼峰的形式。
那么回到我们的调用 resolveAsset(context.$options, 'components', tag)
,即拿 vm.$options.components[tag]
,这样我们就可以在 resolveAsset
的时候拿到这个组件的构造函数,并作为 createComponent
函数的参数。
局部注册
Vue.js 也同样支持局部注册组件,我们可以在一个组件内部使用 components
选项做组件的局部注册,例如:
import HelloWorld from './components/HelloWorld'
export default {
components: {
HelloWorld,
},
}
其实理解了全局注册的过程,局部注册是非常简单的,在组件的 Vue 的实例化阶段有一个合并 options
的逻辑,之前我们也分析过,其中就有把 components
合并到 vm.$options.components
上,这样就可以在 resolveAsset
的时候拿到这个组件的构造函数,并作为 createComponent
函数的参数。
注意:局部注册和全局注册不同的是,只有在组件引用并注册了,才能访问局部注册的子组件,而全局注册是扩展到 Vue.options
下,所以在所有组件的创建过程中,都会从全局的 Vue.options.components
扩展到当前组件的 vm.$options.components
下,这就是全局注册的组件能被任意使用的原因。
异步组件
在我们平时的开发工作中,为了减少首屏代码体积,往往会把一些非首屏的组件设计成异步组件,按需加载。Vue 也原生支持了异步组件的能力,如下:
Vue.component('async-example', function(resolve, reject) {
// 这个特殊的 require 语法告诉 webpack
// 自动将编译后的代码分割成不同的块,
// 这些块将通过 Ajax 请求自动下载。
require(['./my-async-component'], resolve)
})
示例中可以看到,Vue 注册的组件不再是一个对象,而是一个工厂函数,函数有两个参数 resolve
和 reject
,函数内部用 setTimeout
模拟了异步,实际使用可能是通过动态请求异步组件的 JS 地址,最终通过执行 resolve
方法,它的参数就是我们的异步组件对象。
在了解了异步组件如何注册后,我们从源码的角度来分析一下它的实现。
上一节,我们分析了组件的注册逻辑,由于异步组件的定义并不是一个普通对象,所以不会执行 Vue.extend
的逻辑把它变成一个组件的构造函数,但是它仍然可以执行到 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)
}
// ……
// async component
let asyncFactory
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)
}
}
//……
}
我们省略了不必要的逻辑,只保留关键逻辑,由于我们这个时候传入的 Ctor
是一个函数,那么它也并不会执行 Vue.extend
逻辑,因此它的 cid
是 undefined
,进入了异步组件创建的逻辑。这里首先执行了 Ctor = resolveAsyncComponent(asyncFactory, baseCtor)
方法,它定义在 src/core/vdom/helpers/resolve-async-component.js
中:
export function resolveAsyncComponent(
factory: Function,
baseCtor: Class<Component>
): Class<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 = null
let timerTimeout = null
;(owner: any).$on('hook:destroyed', () => remove(owners, owner))
const forceRender = (renderCompleted: boolean) => {
for (let i = 0, l = owners.length; i < l; i++) {
;(owners[i]: any).$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 | Class<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) => {
process.env.NODE_ENV !== 'production' &&
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 {
timerLoading = setTimeout(() => {
timerLoading = null
if (isUndef(factory.resolved) && isUndef(factory.error)) {
factory.loading = true
forceRender(false)
}
}, res.delay || 200)
}
}
if (isDef(res.timeout)) {
timerTimeout = setTimeout(() => {
timerTimeout = null
if (isUndef(factory.resolved)) {
reject(
process.env.NODE_ENV !== 'production'
? `timeout (${res.timeout}ms)`
: null
)
}
}, res.timeout)
}
}
}
sync = false
// return in case resolved synchronously
return factory.loading ? factory.loadingComp : factory.resolved
}
}
resolveAsyncComponent
函数的逻辑略复杂,因为它实际上处理了 3 种异步组件的创建方式,除了刚才示例的组件注册方式,还支持 2 种,一种是支持 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)
那么接下来,就根据这 3 种异步组件的情况,来分别去分析 resolveAsyncComponent
的逻辑。
1、普通函数异步组件
针对普通函数的情况,前面几个 if 判断可以忽略,它们是为高级组件所用,对于 factory.owners
的判断,是考虑到多个地方同时初始化一个异步组件,那么它的实际加载应该只有一次。接着进入实际加载逻辑,定义了 forceRender
、resolve
和 reject
函数,注意 resolve
和 reject
函数用 once
函数做了一层包装,once
函数定义在 src/shared/util.js
中:
/**
* Ensure a function is called only once.
*/
export function once(fn: Function): Function {
let called = false
return function() {
if (!called) {
called = true
fn.apply(this, arguments)
}
}
}
once
逻辑非常简单,传入一个函数,并返回一个新函数,它非常巧妙地利用闭包和一个标志位保证了它包装的函数只会执行一次,在本小节中,就是确保了 resolve
和 reject
函数只执行一次。
接下来执行 const res = factory(resolve, reject)
逻辑,这块儿就是执行我们异步组件的工厂函数,同时把 resolve
和 reject
函数作为参数传入,组件的工厂函数通常会先发送请求去加载异步组件的 JS 文件,拿到组件定义的对象 res
后,执行 resolve(res)
逻辑,它会先执行 factory.resolved = ensureCtor(res, baseCtor)
:
/** src/core/vdom/helpers/resolve-async-component.js */
function ensureCtor(comp: any, base) {
if (comp.__esModule || (hasSymbol && comp[Symbol.toStringTag] === 'Module')) {
comp = comp.default
}
return isObject(comp) ? base.extend(comp) : comp
}
这个函数目的是为了保证能找到异步组件 JS 定义的组件对象,并且如果它是一个普通对象,则调用 Vue.extend
把它转换成一个组件的构造函数。
resolve
逻辑最后判断了 sync
,显然我们这个场景下 sync
为 false,那么就会执行 forceRender
函数,它会遍历 factory.owners
,拿到每一个调用异步组件的实例 vm
,执行 vm.$forceUpdate()
方法,它定义在 src/core/instance/lifecycle.js
中:
Vue.prototype.$forceUpdate = function() {
const vm: Component = this
if (vm._watcher) {
vm._watcher.update()
}
}
$forceUpdate
的逻辑非常简单,就是调用渲染 watcher
的 update
方法,让渲染 watcher
对应的回调函数执行,也就是触发了组件的重新渲染。之所以这么做是因为 Vue 通常是数据驱动视图重新渲染,但是在整个异步组件加载过程中是没有数据发生变化的,所以通过执行 $forceUpdate
可以强制组件重新渲染一次。
2、 Promise 异步组件
Vue.component(
'async-webpack-example',
// 该 `import` 函数返回一个 `Promise` 对象。
() => import('./my-async-component')
)
webpack 2+ 支持了异步加载的语法糖: () => import('./my-async-component')
,当执行完 res = factory(resolve, reject)
,返回的值是 import('./my-async-component')
的返回值,它是一个 Promise
对象,接着进入 if 条件,判断了 isPromise(res)
,条件满足,执行:
if (isUndef(factory.resolved)) {
res.then(resolve, reject)
}
当组件异步加载成功后,执行 resolve
, 加载失败则执行 reject
,这样就非常巧妙地实现了配合 webpack 2+ 的异步加载组件的方式(Promise)加载异步组件。
3、高级异步组件
由于异步加载组件需要动态加载 JS,有一定网络延时,而且有加载失败的情况,所以通常我们在开发异步组件相关逻辑的时候需要设计 loading 组件和 error 组件,并在适当的实际渲染它们。 Vue.js 2.3+支持了一种高级异步组件的方式,他通过一个简单的对象配置,帮你搞定 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
,当执行完 res = factory(resolve, reject)
,返回值就是定义的组件对象,显然满足 else if (isPromise(res.component))
的逻辑,接着执行 res.component.then(resolve, reject)
,当异步组件加载成功后,执行 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 {
timerLoading = setTimeout(() => {
timerLoading = null
if (isUndef(factory.resolved) && isUndef(factory.error)) {
factory.loading = true
forceRender(false)
}
}, res.delay || 200)
}
}
if (isDef(res.timeout)) {
timerTimeout = setTimeout(() => {
timerTimeout = null
if (isUndef(factory.resolved)) {
reject(
process.env.NODE_ENV !== 'production'
? `timeout (${res.timeout}ms)`
: null
)
}
}, res.timeout)
}
先判断 res.error
是否定义了 error 组件,如果有的话则赋值给 factory.errorComp
。接着判断 res.loading
是否定义了 loading 组件,如果有的话则赋值给 factory.loadingComp
,如果设置了 res.delay
且为 0,就立即执行 facotry.loading = true
,否则延时 delay
的时间执行。
if (isUndef(factory.resolved) && isUndef(factory.error)) {
factory.loading = true
forceRender(false)
}
最后判断 res.timeout
如果配置了该项,则超过 res.timeout
时间,如果组件没有成功加载,执行 reject
。
在 resolveAsyncComponent
的最后有一段逻辑:
sync = false
// return in case resolved synchronously
return factory.loading ? factory.loadingComp : factory.resolved
如果 delay
的配置为 0,则这次直接渲染 loading 组件,否则延时 delay
执行 forceRender
,那么又会再一次执行到 resolveAsyncComponent
。
那么这时候,我们有几种情况,按逻辑的执行顺序,对不同的情况做判断。
异步组件加载失败
当异步组件加载失败,会执行 reject
函数:
const reject = once((reason) => {
process.env.NODE_ENV !== 'production' &&
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
:
if (isTrue(factory.error) && isDef(factory.errorComp)) {
return factory.errorComp
}
那么这个时候就返回 factory.errorComp
,直接渲染 error 组件。
异步组件加载成功
当异步组件加载成功,会执行 resolve
函数:
const resolve = once((res: Object | Class<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
再次执行到 resolveAsyncComponent
:
if (isDef(factory.resolved)) {
return factory.resolved
}
那么这个时候直接返回 factory.resolved
,渲染成功加载的组件。
异步组件加载中
如果异步组件加载中并未返回,这时候会走到这个逻辑:
if (isTrue(factory.loading) && isDef(factory.loadingComp)) {
return factory.loadingComp
}
那么则会返回 factory.loadingComp
,渲染 loading 组件。
异步组件加载超时
如果超时,则走到了 reject
逻辑,之后逻辑和加载失败一样,渲染 error 组件。
异步组件 patch
回到 createComponent
的逻辑:
// async component
let asyncFactory
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)
}
}
如果是第一次执行 resolveAsyncComponent
,除非使用高级异步组件 0 delay
创建了一个 loading 组件,否则返回是 undefined
,接着通过 createAsyncPlaceholder
创建一个注释节点作为占位符。它定义在 src/core/vdom/helpers/resolve-async-component.js
中:
export function createAsyncPlaceholder(
factory: Function,
data: ?VNodeData,
context: Component,
children: ?Array<VNode>,
tag: ?string
): VNode {
const node = createEmptyVNode()
node.asyncFactory = factory
node.asyncMeta = { data, context, children, tag }
return node
}
实际上就是创建一个占位的注释 VNode,同时把 asyncFactory
和 asyncMeta
赋值给当前 vnode
。
当运行 forceRender
的时候,会触发组件的重新渲染,那么会再一次执行 resolveAsyncComponent
,这个时候就会根据不同的情况,可能返回 loading、error 或成功加载的异步组件,返回值不为 undefined
,因此就走正常的组件 render
、patch
过程,与组件第一次渲染流程不一样,这个时候是存在新旧 vnode
的,会执行组件更新的 patch
过程。
组件更新
/** src/core/instance/lifecycle.js 中的 mountComponent 函数 */
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
new Watcher(
vm,
updateComponent,
noop,
{
before() {
if (vm._isMounted) {
callHook(vm, 'beforeUpdate')
}
},
},
true /* isRenderWatcher */
)
组件的更新还是调用了 vm._update
方法,我们再回顾一下这个方法,它定义在 src/core/instance/lifecycle.js
中:
Vue.prototype._update = function(vnode: VNode, hydrating?: boolean) {
const vm: Component = this
//……
const prevVnode = vm._vnode
//……
if (!prevVnode) {
// initial render
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
} else {
// updates
vm.$el = vm.__patch__(prevVnode, vnode)
}
//……
}
组件更新的过程,会执行 vm.$el = vm.__patch__(prevVnode, vnode)
,它仍然会调用 patch
函数,在 src/core/vdom/patch.js
中定义:
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 = []
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) {
//……
}
// 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
的逻辑和首次渲染是不一样的,因为 oldVnode
不为空,并且它和 vnode
都是 VNode
类型,接下来会通过 sameVnode(oldVnode, vnode)
判断它们是否是相同的 VNode 来决定走不同的更新逻辑:
function sameVnode (a, b) {
return (
a.key === b.key && (
(
a.tag === b.tag &&
a.isComment === b.isComment &&
isDef(a.data) === isDef(b.data) &&
sameInputType(a, b)
) || (
isTrue(a.isAsyncPlaceholder) &&
a.asyncFactory === b.asyncFactory &&
isUndef(b.asyncFactory.error)
)
)
)
}
sameVnode
的逻辑非常简单,如果两个 vnode
的 key
不相等,则是不同的;否则继续判断,对于同步组件,则判断 isComment
、data
、input
类型等是否相同,对于异步组件,则判断 asyncFactory
是否相同。
所以根据新旧 vnode
是否为 sameVnode
会走到不同的更新逻辑,我们先来说一下不同的情况。
新旧节点不同
如果新旧 vnode
不同,那么更新的逻辑非常简单,它本质上是要替换已存在的节点,大致分为 3 步:
创建新节点
// 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)
)
以当前旧节点为参考节点,创建新的节点,并插入到 DOM 中。
更新父的占位符节点
// 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
}
}
我们只关注主要逻辑即可,找到当前 vnode
的父的占位符节点,先执行各个 module
的 destroy
的钩子函数,如果当前占位符是一个可挂载的节点,则执行 module
的 create
钩子函数。
删除旧节点
// destroy old node
if (isDef(parentElm)) {
removeVnodes([oldVnode], 0, 0)
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode)
}
Last updated
Was this helpful?