Vue 2.x 源码阅读记录(二):组件化

我心飞翔 分类:javascript

写在前面

文章为阅读笔记向,需 clone 下来 Vue 源码并加以调试服用~~

一个最基本的组件例子,下面围绕这个例子,对源码进行分析:

// 创建一个名为 App 的组件
const App = Vue.component('App', {
  name: 'App',
  template: `<h1>My App</h1>`,
  props: {},
  data() {
    return {};
  },
  methods: {},
});

new Vue({
  el: '#app',
  render: (h) => h(App)
});
 

createComponent

在渲染的部分,我们分析了 _createElement 方法的具体实现,其中有一段代码是分析了tag,如果传入的是 html 标签则生成标签的 vnode 对象,否则调用 createComponent方法创建一个组件的 vnode:

  // 开始创建 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
        )
      }
      // tag 如果是一个内置节点(div, span 等),直接创建一个 vNode 实例
      vnode = new VNode(
        config.parsePlatformTagName(tag), data, children,
        undefined, undefined, context
      )
    } else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
      // component
      // tag 如果是一个已注册的组件名,则创建一个组件类型的 vnode
      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
      vnode = new VNode(
        tag, data, children,
        undefined, undefined, context
      )
    }
  } else {
    // direct component options / constructor
    // tag 直接传递了一个组件,创建组件类型的 vnode
    vnode = createComponent(tag, data, context, children)
  }
 

createComponent定义在src\core\vdom\create-component.js,它在我们例子中主要做的三件事是:构造子类构造函数、安装组件函数钩子、实例化 vNode。先大致看看它的流程:

// vnode = createComponent(tag, data, context, children)
// 创建组件 vNode
export function createComponent (
Ctor: Class<Component> | Function | Object | void,
data: ?VNodeData,
context: Component,
children: ?Array<VNode>,
tag?: string
): VNode | Array<VNode> | void {
// debugger
if (isUndef(Ctor)) {
return
}
/* 
context = 当前 Vue 实例
Vue.option 在 src\core\global-api\index.js 定义并将 Vue 的构造函数赋值给 _base:Vue.options._base = Vue
在初始化 _init 时有一个合并 option 的操作
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
baseCtor 就是当前上下文的构造函数
*/
const baseCtor = context.$options._base
// plain options object: turn it into a constructor
if (isObject(Ctor)) {
// 如果 Ctor 是一个普通对象,则调用 Vue.extend 返回的
// 在例子中,使用的是 Vue.component() 创建组件,返回的就是 extend 返回的一个组件构造函数
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
// 向组件上挂载 init、prepatch、insert、destroy 钩子函数,后面介绍
installComponentHooks(data)
// return a placeholder vnode
const name = Ctor.options.name || tag
// 创建组件的 vnode
// 组件名由 Vue 根据组件的 cid 和 name/tag 定义
// 将 props、事件、tag、children 等属性存到 VNode 的 context 对象下
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)
}
// 最后返回组件的 vnode
return vnode
}

构造子类构造函数

  const baseCtor = context.$options._base
// plain options object: turn it into a constructor
if (isObject(Ctor)) {
Ctor = baseCtor.extend(Ctor)
}

在我们 import Vue from 'vue' 的时候,会执行一个 initGlobalAPI方法,它定义在src\core\global-api\index.js,主要做的是将一些静态的属性和方法挂载到 Vue 构造函数上:

export function initGlobalAPI (Vue: GlobalAPI) {
// config
const configDef = {}
configDef.get = () => config
if (process.env.NODE_ENV !== 'production') {
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)
}

在这里面,看到了 _base的定义,它指向的是 Vue 的构造函数:

Vue.options._base = Vue

而在createComponent中,如果传入的ctor是一个对象,则调用baseCtorextend方法,这个方法返回一个继承了 Vue 构造函数的子类,通常来说,就是一个组件的构造函数。它在上面的initExtend方法中被定义:

/**
* Class inheritance
* Vue.extend 静态方法,返回一个继承了 Vue 实例的组件构造函数
* 在 initGlobalAPI 的运行中被 initExtend 定义
*/
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)
}
// 定义一个子类继承 Vue 构造函数
const Sub = function VueComponent (options) {
this._init(options)
}
// 原型继承
Sub.prototype = Object.create(Super.prototype)
Sub.prototype.constructor = Sub
Sub.cid = cid++
Sub.options = mergeOptions(
Super.options,
extendOptions
)
Sub['super'] = Super
// 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
}
}

安装组件钩子函数

// install component management hooks onto the placeholder node
installComponentHooks(data)

它的过程就是把 componentVNodeHooks 的钩子函数合并到 data.hook 中 ,这些 hook 在后面 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)
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)) {
// 在合并过程中,如果某个时机的钩子已经存在 data.hook 中,那么通过执行 mergeHook 函数做合并
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

  // return a placeholder vnode
const name = Ctor.options.name || tag
// 创建组件的 vnode
// 组件名由 Vue 根据组件的 cid 和 name/tag 定义
// 将 props、事件、tag、children 等属性存到 VNode 的 context 对象下
const vnode = new VNode(
`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
data, undefined, undefined, undefined, context,
{ Ctor, propsData, listeners, tag, children },
asyncFactory
)

createComponent返回组件的 vnode 后,一样会走到_update方法,执行patch函数。


patch

通过前面我们知道,_updatecreateElm方法用于 patch vnode,将 vnode 生成真实 DOM 并插入。这里大致记录一下createElm流程:

vnode为普通元素:创建一个对应的tag的元素,调用createChildren循环调用createElm完成 DOM 的渲染:

vnode.elm = vnode.ns
? nodeOps.createElementNS(vnode.ns, tag)
: nodeOps.createElement(tag, vnode)
// ...
createChildren(vnode, children, insertedVnodeQueue)

vnode为组件时,调用组件的init钩子函数进行组件的创建与插入 DOM 过程,并终止:

// createElm
// 如果 createComponent 返回 true,则说明已经完成组件 DOM 的 patch
// 随后中止 createElm 后面的 DOM 逻辑
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}

这里分步骤来看createComponent内部执行的流程:

​ 1、调用init钩子函数,相当于组件流程的入口:

function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
let i = vnode.data
if (isDef(i)) {
// 是一个组件 vnode
const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
if (isDef(i = i.hook) && isDef(i = i.init)) {
// 在 src\core\vdom\create-component.js 中的 createComponent 挂载了组件钩子函数
// 满足条件则 i 就变成了 init 钩子函数
i(vnode, false /* hydrating */)
}
}
// ...
}

2、init函数内部调用了createComponentInstanceForVnode,拿到组件实例对象并执行组件的挂载方法:

// 组件实例
const child = vnode.componentInstance = createComponentInstanceForVnode(
vnode,
// activeInstance 在 _update 的调用中被赋值,是当前实例的父级构造函数
activeInstance
)
// 调用组件的 $mount 方法
// 由于组件是不用传入 el 的,所以是 $mount(undefined, false)
child.$mount(hydrating ? vnode.elm : undefined, hydrating)

3、createComponentInstanceForVnode执行组件的_init_init 中执行initLifecycle将当前组件实例插入到父实例的$children中。接着执行了initInternalComponent,合并了一些组件的 options。最后返回组件的实例:

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
}
// 调用 Vue 的子类构造函数,返回组件实例
// 也就是组件构造函数调用了 this._init(options)
// 组件也会走一遍 new Vue(options) 的初始化过程
return new vnode.componentOptions.Ctor(options)
}

4、init函数调用了组件实例的$mount(vnode.elm),由于组件的 vnode 只是一个占位符,所以elmundefined。执行了$mount中拿到用户传入的template生成render方法,调用mountComponent。调用 mountComponent,就相当于进入了普通元素的_update(_render())流程。由于elmundefined,所以在这个 patch 过程中不会被插入到 DOM 中,但是生成的 DOM 元素会被赋值给占位 vnode 的componentInstance 属性下的$el属性。:

// 开始调用 $mount
child.$mount(hydrating ? vnode.elm : undefined, hydrating)

5、createComponent中的i执行完成之后,执行initComponent方法,将vnode.componentInstance.$el赋值给vnode.elm

// createComponent
if (isDef(vnode.componentInstance)) {
initComponent(vnode, insertedVnodeQueue)
insert(parentElm, vnode.elm, refElm)
// 在完成组件的整个 patch 过程后,最后执行 insert(parentElm, vnode.elm, refElm) 完成组件的 DOM 插入
// init 钩子里会不断地去递归子组件,所以 DOM 会按先子后父的顺序插入
// 如果组件 patch 过程中又创建了子组件,那么DOM 的插入顺序是先子后父。
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
}
return true
}
// initComponent
vnode.elm = vnode.componentInstance.$el

6、上面执行insert了方法,将vnode.elm插入 DOM 树,删除旧 vnode,返回组件vnode.elm赋值给vm.$el。至此,createComponent完成了对组件的 patch,然后将主要流程还给createElm,此时createComponent返回true,则当前createElm流程结束。这是 Vue 一种深度遍历的手段:

if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
// 如果 createComponent 始终返回 true,表示是当前还是一个组件,继续进入组件流程
return
}

7、最后,再由patch函数删除 DOM 中的旧节点,完成整个 patch 过程:

if (isDef(parentElm)) {
removeVnodes([oldVnode], 0, 0)
}

PS:记录一些在整个 patch 过程中的重要遍历以及定位:

// 在 initLifecycle 中定义,initLifecycle 把当前实例 vm 插入到其父实例中 parent(如果父实例存在)
$parent, $root, $children 
// 在 mountComponent,第一次 patch 时为 #app 根元素
vm.$el = el
// createComponent 中将挂载组件钩子函数,定义组件占位符 vnode(tag = vue-component-cid..)
// _render() 返回的 vnode 对象
vm._vnode
// 在 createComponentInstanceForVnode 中定义,表示父 vnode 和当前组件 vnode
options.parent, _parentVnode
/* 
小结:在 patch 的 createElm 中的 createComponent 中只要传入的 vnode 属于组件 vnode,就会返回 true,从而结束 createElm(patch) 的执行,createElm 的 vnode 一旦为组件,则组件从头开始执行 _init(),当组件 patch(insert) 完之后,才会继续执行父级的 patch ,从而形成先子后父的插入顺序。
*/

合并配置

普通实例合并

一个基本的实例配置例子,合并之后会将所有配置存储到实例的$options属性上:

let app = new Vue({
// mixins: [mixin],
el: '#app',
data: {
msg: 'Hello'
},
created() {}
})
/*
app.$options: {
components: {}
created: [ƒ]
data: ƒ mergedInstanceDataFn()
directives: {}
el: "#app"
filters: {}
render: ƒ anonymous( )
staticRenderFns: []
_base: ƒ Vue(options)
}
*/

initGlobalAPI的调用中,首先定义了一些默认的配置,包括componentsdirectivesfilters,随后将 Vue 的一些内置组件keep-alivetransitiontransitionGroup合并到components配置中,这就是为什么 Vue 可以不用注册就使用这些组件:

{/* 默认的 options 定义 */}
Vue.options = Object.create(null)
{/* 添加 components, directives, filters 属性 */}
ASSET_TYPES.forEach(type => {
Vue.options[type + 's'] = Object.create(null)
})
{/* 向 components 里赋值 builtInComponents 中的属性 */}
{/* 这里实际上就是将 Vue 的内置组件率先合并到默认的 components 中 */}
extend(Vue.options.components, builtInComponents)

在创建实例时执行_init方法,对这个例子,它调用mergeOptions合并默认和传入的配置:

// 合并 new Vue 的配置参数
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),  // 默认 options
options || {},  // 用户传入的 options
vm
)

在例子中,普通实例的resolveConstructorOptions直接返回默认的options

export function resolveConstructorOptions (Ctor: Class<Component>) {
let options = Ctor.options
if (Ctor.super) {
//...
}
// 当前是 Vue 根构造函数时直接返回默认的 options
return options
}

mergeOptions将默认生成的配置与传入的配置合并,然后返回赋值给$options属性。它遍历默认和传入的配置,在内部为每个配置的项都设置了独立的合并策略函数,调用它们进行合并,使用options存储并返回:

const options = {}
let key
for (key in parent) {
// console.log('parentKey :>> ', key);
mergeField(key)
}
for (key in child) {
if (!hasOwn(parent, key)) {
// child 不存在 parent 中
mergeField(key)
}
}
// console.log('strats :>> ', strats);
function mergeField (key) {
const strat = strats[key] || defaultStrat
// strats 将所有选项都定义一个自己的合并策略函数
// 合并策略函数将 parent 和 child 合并后的属性赋值给 options[key]
options[key] = strat(parent[key], child[key], vm, key)
}
return options

这里看一下生命周期函数的合并策略,生命周期函数最终会被合并为一个数组,便于 mixin 等属性的生命周期函数顺序执行:

/**
* Hooks and props are merged as arrays.
*/
function mergeHook (
parentVal: ?Array<Function>,
childVal: ?Function | ?Array<Function>
): ?Array<Function> {
const res = childVal
? parentVal
? parentVal.concat(childVal)
: Array.isArray(childVal)
? childVal
: [childVal]
: parentVal
return res
? dedupeHooks(res)
: res
}
function dedupeHooks (hooks) {
const res = []
for (let i = 0; i < hooks.length; i++) {
if (res.indexOf(hooks[i]) === -1) {
res.push(hooks[i])
}
}
return res
}
LIFECYCLE_HOOKS.forEach(hook => {
strats[hook] = mergeHook
})

mixins的场景

接下来向配置中添加一个mixin,看看是如何合并的:

const mixin = Vue.mixin({
created() {
console.log('mixin created')
}
})
let app = new Vue({
mixins: [mixin],
el: '#app',
data: {
msg: 'Hello'
},
created() {}
})
/*
app.$options: {
components: {}
created: (2) [ƒ, ƒ]
data: ƒ mergedInstanceDataFn()
directives: {}
el: "#app"
filters: {}
mixins: [ƒ]
render: ƒ anonymous( )
staticRenderFns: []
_base: ƒ Vue(options)
}
*/

mergeOptions中,如果传入的配置中带有mixins则将其遍历,递归调用mergeOptions将各个mixin的配置合并到parent默认配置中,最后再将传入的其它配置合并:

if (child.mixins) {
for (let i = 0, l = child.mixins.length; i < l; i++) {
parent = mergeOptions(parent, child.mixins[i], vm)
}
}

需要注意的是,在initGlobalAPI调用的initMixin中,定义了Vue.mixin静态方法。在调用它时会将当前mixinVue的默认options合并,再到后面实例化时将这个合并后的mixin与实例的options合并:

export function initMixin (Vue: GlobalAPI) {
Vue.mixin = function (mixin: Object) {
this.options = mergeOptions(this.options, mixin)
return this
}
}

组件合并配置

再将例子改造成组件的使用场景:

const childComponent = {
template: '<div>{{msg}}</div>',
data() {
return {
msg: 'Hello Vue'
};
},
created() {
console.log('component created');
},
mounted() {
console.log('component mounted');
},
};
const mixin = Vue.mixin({
created() {
console.log('mixin created');
}
})
let app = new Vue({
mixins: [mixin],
el: '#app',
data: {
msg: 'Hello'
},
created() {},
render: (h) => h(childComponent)
})

在渲染组件环节,会调用createComponentInstanceForVnode来生成组件特有的options并调用组件构造函数。在initGlobalAPI生成子组件构造函数的initExtend方法中,将父构造函数的 options 与当前组件传入的 options 合并:

// 将父构造函数的 options 与当前组件传入的 options 合并
Sub.options = mergeOptions(
Super.options,
extendOptions
)

createComponentInstanceForVnode实例化组件构造函数,执行_init方法,此时合并配置调用的是initInternalComponent

if (options && options._isComponent) {
// 组件构造函数的 _init() 走这里
initInternalComponent(vm, options)
}
export function initInternalComponent (vm: Component, options: InternalComponentOptions) {
// vm.constructor.options 拿到之前父构造函数与当前组件传入的配置合并后的配置
// 相当于 vm.$options = Object.create(Sub.options)
// Object.create 创建一个指定原型的对象,也就是将 vm.constructor.options 放到了 opts.__proto__ 属性中
const opts = vm.$options = Object.create(vm.constructor.options)
// doing this because it's faster than dynamic enumeration.
// _parentVnode 在 createComponentInstanceForVnode 被赋值
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
}
}

最终,组件合并后将结果保存在$options, 子组件初始化过程通过 initInternalComponent 方式要比外部初始化 Vue 通过 mergeOptions 的过程要快 ,因为没有递归与合并策略等,最终组件的$options是这样的:

/*
vm.$options: {
parent: Vue {_uid: 0, _isVue: true, $options: {…}, _renderProxy: Proxy, _self: Vue, …}
propsData: undefined
render: ƒ anonymous( )
staticRenderFns: []
_componentTag: undefined
_parentListeners: undefined
_parentVnode: VNode {tag: "vue-component-1", data: {…}, children: undefined, text: undefined, elm: div, …}
_renderChildren: undefined
__proto__:
components: {}
created: (2) [ƒ, ƒ]
data: ƒ data()
directives: {}
filters: {}
mounted: [ƒ]
template: "<div>{{msg}}</div>"
_Ctor: {0: ƒ}
_base: ƒ Vue(options)
__proto__: Object
}
*/

生命周期

在组件渲染的各个阶段会调用不同的生命周期,它使用一个总线callHook函数来调用生命周期钩子。它在内部调用了传入的hook,并带有错误处理:

export function callHook (vm: Component, hook: string) {
// #7573 disable dep collection when invoking lifecycle hooks
pushTarget()
// hook 函数
const handlers = vm.$options[hook]
const info = `${hook} hook`
if (handlers) {
for (let i = 0, j = handlers.length; i < j; i++) {
// 执行 hook 函数,内部进行了错误处理
invokeWithErrorHandling(handlers[i], vm, null, vm, info)
}
}
if (vm._hasHookEvent) {
vm.$emit('hook:' + hook)
}
popTarget()
}

beforeCreate & created

在初始化调用_init方法时,会调用这两个钩子:

// 初始化生命周期,渲染等
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
// 初始化 props, data, methods, watch, computed 等
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')

由于beforeCreate调用在initState前面,所以beforeCreate时无法访问实例内的datamethods等属性。created在调用实例初始化完成调用$mount时还没有调用$mount,所以created无法访问refs、DOM 等元素属性。

beforeMount & mounted

在开始调用mountComponent方法时,beforeMount 会调用,此时可以访问datamethods等属性。在vm._update(vm._render(), hydrating)将 DOM 挂载完成之后,mounted会被调用,此时可以访问 DOM:

xport function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
// ...
// 执行 beforeMount 生命周期
callHook(vm, 'beforeMount')
let updateComponent
// ...
updateComponent = () => {
vm._update(vm._render(), hydrating /* false */)
}
// ...
new Watcher(vm, updateComponent, noop /* 空函数 */, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
// 组件已挂载并未销毁,执行 beforeUpdate 生命周期
callHook(vm, 'beforeUpdate')
}
}
}, true) 
// ...
if (vm.$vnode == null) {
// 渲染完成
vm._isMounted = true
// 执行 mounted 生命周期
callHook(vm, 'mounted')
} 
// ...
}

vm.$vnode = _parentVnode_render方法中定义,如果vm.$vnodenull,表示这不是一次组件的 mount,而是new Vue()的 mount。那么再看看组件的 mount。首先了解一下,在patch方法中,定义了一个insertedVnodeQueue = []队列,当vnode是组件时,在组件插入 DOM 之前的initComponent方法中,会将组件vnodepush 到队列中:

// patch 
const insertedVnodeQueue = []
// initComponent
function initComponent (vnode, insertedVnodeQueue) {
if (isDef(vnode.data.pendingInsert)) {
insertedVnodeQueue.push.apply(insertedVnodeQueue, vnode.data.pendingInsert)
vnode.data.pendingInsert = null
}
vnode.elm = vnode.componentInstance.$el
if (isPatchable(vnode)) {
invokeCreateHooks(vnode, insertedVnodeQueue)
setScope(vnode)
} else {
// empty component root.
// skip all element-related modules except for ref (#3455)
registerRef(vnode)
// make sure to invoke the insert hook
// 组件 vnode push 到队列中
insertedVnodeQueue.push(vnode)
}
}

由于在组件渲染过程中执行_render的时候,$vnode已经赋值了_parentVnode。所以组件的init钩子最后调用child.$mount不会执行mounted钩子。在patch方法最后,组件渲染完成之后,会调用 invokeInsertHook方法,它调用了队列中所有组件的insert钩子函数:

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) {
// 执行组件的 insert 钩子
queue[i].data.hook.insert(queue[i])
}
}
}

insert钩子内调用了组件的mounted生命周期,insertedVnodeQueue的添加顺序是先子后父,所以对于同步渲染的子组件而言,mounted钩子函数的执行顺序也是先子后父 :

insert (vnode: MountedComponentVNode) {
const { context, componentInstance } = vnode
if (!componentInstance._isMounted) {
componentInstance._isMounted = true
// 组件的 mounted 调用
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 */)
}
}
},

beforeUpdate & updated

beforeUpdate的执行时机是在mountComponent渲染 Watcher 的before函数中,它在组件mounted之后才会执行:

// 在挂载时添加一个`渲染 watcher`
// 用于元素的第一次渲染和后续的更新
new Watcher(vm, updateComponent, noop /* 空函数 */, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
// 组件已挂载并未销毁,执行 beforeUpdate 生命周期
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)

updated后续更新。。。

beforeDestroy & destroyed

组件销毁的流程暂时还没有了解,但最后都会调用$destroy全局方法来销毁组件。beforeDestroy$destroy开始调用时执行。经过一系列销毁操作之后执行destroyed,包括从父实例的$children删除自身、卸载watcher、移除 DOM 等:

  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) {
// 从父的 $children 树中删除自身
remove(parent.$children, vm)
}
// teardown watchers
if (vm._watcher) {
// 卸载 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
}
}
}

在销毁过程中会递归销毁该组件的子组件,所以destroyed也是先子后父顺序执行。


组件注册

全局注册

先看一个全局注册组件的例子:

const MyComp = Vue.component('MyComp', {
template: `
<div class="my-comp">
<span>{{msg}}</span>
</div>
`,
data() {
return {
msg: 'Hello MyComp'
};
},
});
new Vue({
el: '#app',
render: (h) => h(MyComp)
});

在执行initGlobalAPI时,最后执行了initAssetRegisters方法,这个方法为Vue构造函数添加了componentdirectivefilter三个全局方法,其中component就是注册一个全局组件:

  // 定义了 Vue.component, Vue.directive, Vue.filter 三个静态方法
// 这三个常量值也成为组件内部的选项
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
// 调用实例上的 exntend 方法创建组件
// 相当于 Vue.extend(definition /* options */)
definition = this.options._base.extend(definition)
}
if (type === 'directive' && typeof definition === 'function') {
definition = { bind: definition, update: definition }
}
// 向 options 的 components 选项中存入该组件
this.options[type + 's'][id] = definition
return definition
}
}
})

当调用Vue.component时,实际上是调用了this.options._base.extend,相当于Vue.extend,返回一个组件的构造函数:

definition = this.options._base.extend(definition)

此时MyComp是一个组件的构造函数。后来执行到这个组件的initInternalComponent时,会将这个组件的options合并到vm.$options的原型上:

var opts = vm.$options = Object.create(vm.constructor.options);

要注意的是,组件在初始化构造函数的时候合并了根Vue构造函数的options

Sub.options = mergeOptions(
Super.options,
extendOptions
)

合并完之后,该组件自身components中也会有该组件自身的构造函数供自身调用,并且按照组件的合并策略,会将Vue构造函数的components作为一个原型对象合并到组件的components.__proto__下:

function mergeAssets (
parentVal: ?Object,
childVal: ?Object,
vm?: Component,
key: string
): Object {
const res = Object.create(parentVal || null)
if (childVal) {
process.env.NODE_ENV !== 'production' && assertObjectType(key, childVal, vm)
return extend(res, childVal)
} else {
return res
}
}

此时组件的components.__proto__选项下包含Vue.components下的所有组件,在该组件下就可以使用transition以及其它全局的组件。

改造一下例子:

const MyComp = Vue.component('MyComp', {
template: `
<div class="my-comp">MyComp</div>
`,
data() {
return {
msg: 'Hello MyComp'
};
},
});
const App = Vue.component('App', {
template: `
<div class="my-comp">
<MyComp />
</div>
`,
data() {
return {
msg: 'Hello App'
};
},
});
new Vue({
el: '#app',
render: (h) => h(App)
});

App组件在执行的_createElement时,它的tagMyComp,因此会下面这个逻辑。调用resolveAsset返回组件构造函数给Ctor在进行组件 vnode 的创建:

if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
// component
// tag 如果是一个已注册的组件名,则创建一个组件类型的 vnode
vnode = createComponent(Ctor, data, context, children, tag)
}

resolveAsset通过一系列条件与原型链查找(由于前面提到过,组件构造函数在初始化时会合并父实例的options,并且在initInternalComponent中组件构造函数的options定义在__proto__上),将需要的组件构造函数找出:

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
// 从 components 里取出组件构造函数返回
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 localComp = Vue.extend({
template: '<h1>Local component</h1>'
});
new Vue({
el: '#app',
components: {
localComp
},
});

组件注册由于直接在选项中编写,会直接进入mergeOptions的组件合并策略环节。将传入的components直接合并到vm.$options.components, 这样就可以在resolveAsset的时候拿到这个组件的构造函数,并作为createComponent 的钩子的参数 :

function mergeAssets (
parentVal: ?Object,
childVal: ?Object,
vm?: Component,
key: string
): Object {
const res = Object.create(parentVal || null)
if (childVal) {
process.env.NODE_ENV !== 'production' && assertObjectType(key, childVal, vm)
return extend(res, childVal)
} else {
return res
}
}

二者区别:

Vue.component全局创建的组件直接向Vue构造函数的options.components选项中添加,子组件创建时会合并Vueoptions并将使用原型链串联起来,所以在子组件内都可以使用全局注册的组件。

局部创建的组件只会保存在当前$options.component中,其它组件不会合并当前的$options,所以只能在当前组件内访问注册的组件。

理解组件注册最重要的地方就是组件 options 的合并、组件合并策略、initInternalComponent三个地方。


异步组件

异步组件在需要使用(render)到的时候才会去创建一个组件,是一种优化手段。看一个基本例子,AsyncComp只有在render时才会去创建组件实例:

const AsyncComp = Vue.component('AsyncComp', (resolve, reject) => {
// require(MyComp, resolve);
setTimeout(() => {
resolve({
template: '<h1>My component</h1>'
});
}, 1000);
});
new Vue({
render: (h) => h(AsyncComp)
}).$mount('#app');

异步组件第二个参数接收一个工厂函数,在完成异步操作后将组件 options 传给resolve函数。在Vue.component方法的处理中,不会将工厂函数变成组件的构造函数,仅仅是保存到Vue.options中:

// 调用 Vue.component
if (type === 'component' && isPlainObject(definition)) {
definition.name = definition.name || id
// 调用实例上的 exntend 方法创建组件
// 相当于 Vue.extend(definition /* options */)
definition = this.options._base.extend(definition)
}
// 向 options 的 components 选项中存入工厂函数
this.options[type + 's'][id] = definition
return definition

在不对组件进行 render 的情况下,组件上没有optionscid以及其它方法,只是一个函数体。因为在上述代码中并没有执行到extend方法。当组件被渲染的时候,还是会走到createElement >createComponent逻辑,对应例子,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用来解析异步组件,并处理了工厂函数、promise、高级异步组件三种异步组件写法。

工厂函数

写法就是上面的例子。在工厂函数流程中定义了resolvereject两个方法,它们使用once方法做了一层包装,来确保对同一个组件只执行一次:

/**
* 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)
}
}
}
const resolve = once((res: Object | Class<Component>) => {
// cache resolved
// ensureCtor 实际是一个闭包,它返回组件构造函数
// 这里相当于调用了组件的构造函数,也就是 vm._init
// _init 方法没有返回值,所以 factory.resolved 是一个 undefined,表示组件构造函数已被调用
factory.resolved = ensureCtor(res, baseCtor)
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)
}
})

resolve方法使用ensureCtor方法将传入的组件转为构造函数,在ensureCtor中实际将组件options调用Vue.extend来生成组件构造函数执行extend逻辑,这也就是按需引入组件的本质:

function ensureCtor (comp: any, base) {
if (
comp.__esModule ||
(hasSymbol && comp[Symbol.toStringTag] === 'Module')
) {
comp = comp.default
}
return isObject(comp)
? base.extend(comp)
: comp
}

在异步组件流程中,resolve接下来执行forceRender方法,将当前渲染的每个实例都执行一次$forceUpdate方法,$forceUpdate调用了实例上的渲染 watcher 的update方法触发组件重新渲染。 之所以这么做是因为 Vue 通常是数据驱动视图重新渲染,但是在整个异步组件加载过程中是没有数据发生变化的,所以通过执行 $forceUpdate 可以强制组件重新渲染一次:

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
}
}
}
Vue.prototype.$forceUpdate = function () {
const vm: Component = this
if (vm._watcher) {
vm._watcher.update()
}
}

最后调用工厂函数,返回到createComponent中返回一个异步组件占位符 vnode:

const res = factory(resolve, reject)
// createElement
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
)
}

promise 异步组件

写法:

Vue.component(
'AsyncComp', 
() => import('SomeComp')
);

解析 promise 异步组件执行() => import()时,res返回一个 Promise 对象。最后将resolvereject作为res.then方法参数调用这两个方法,就完成了 promise 异步组件写法:

const res = factory(resolve, reject)  // Promise
if (isObject(res)) {
if (isPromise(res)) {
// () => Promise
if (isUndef(factory.resolved)) {
res.then(resolve, reject)
}
}
// 。。。
}

高级异步组件(较少使用,详情看源码)

异步组件和普通组件区别

普通组件在定义时以及执行了组件的构造函数,完成了组件的合并配置等一些初始化的工作;异步组件在没有进入 render 流程时则没有进行组件初始化,待到 render 的时候才会执行extend生成组件的构造函数及其初始化。代码逻辑层面,就是何时执行extend的区别

回复

我来回复
  • 暂无回复内容