vue2源码解析(二) - 组件化原理

Vue2的组件化流程源码解析

  • 前言
  • 一、源码位置
  • 二、源码解析
    • 1. 前置处理 - 组件API的注册
      • 1.1 初始化Vue的components选项
      • 1.2 实现component API
    • 2. 组件实例的创建和挂载
      • 2.1 组件初始化方法的定义
        • 2.1.1 获取组件构造方法
        • 2.1.2 处理组件钩子函数和生成vnode
      • 2.2 组件创建和挂载的时机
        • 2.2.1 父子组件生命周期的关系
        • 2.2.2 子组件创建和挂载的时机
  • 三、总结
    • 1. component API的注册时机
    • 2. 自定义组件创建和挂载的时机
    • 3. 父子组件的生命周期关系

前言

我们通常在使用Vue开发时,难免会使用到组件component,那么在Vue中对于实现组件化的原理是怎么样的呢?为了让我们更好的使用Vue,了解其中的实现原理也是有必要的。
我们的主要目标是:组件是如何创建和挂载的?它和父组件的创建和挂载是个怎样的过程?组件的生命周期是怎样的?

我们先看一个简单的例子:

<body>
    <div id='demo'>
        <h1>vue组件化机制h1>
    div>

    <script>
        Vue.component('comp', {
            template: '
I am component
'
}) const app = new Vue({ el: '#demo' }) console.log(this.$options.reader)
script> body>

输出:

"with(this){return _c('div',{attrs:{"id":"demo"}},[
_c('h1',[_v("虚拟DOM")]),_v(" "),
_c('comp') // 对于组件的处理并⽆特殊之处
],1)}"

发现,与普通的reader()没什么不同,对于vnode的结构与普通的节点元素基本是一致的。那么,Vue内部是如何对我们定义的组件进行转化和处理的呢?

一、源码位置

根据我们在上一篇文章vue2源码解析(一) - new Vue()的初始化过程2.3.1小节中提到的初始化全局API方法,找到initGlobalAPI(Vue),进入该方法。所以,我们查看的起始文件为src/core/global-api/index.js

二、源码解析

1. 前置处理 - 组件API的注册

1.1 初始化Vue的components选项

我们new Vue()时,会进行一系列的初始化操作,在初始化全局API的initGlobalAPI(Vue)方法中,为Vue的options初始化了components、filters和directives三个属性。

 Vue.options = Object.create(null)
  ASSET_TYPES.forEach(type => {
    Vue.options[type + 's'] = Object.create(null)
  })

components、filters和directives三个选项,就是我们在new Vue()时,设置的那三个选项。

1.2 实现component API

实现component()方法是在文件最下方的initAssetRegisters(Vue)中去做的,进入该方法对应的文件src/core/global-api/assets.js

在initAssetRegisters()中,definition为我们Vue.component()的第二个参数。它的任务:
1)为component定义了name;
2)并根据definition继承Vue的构造函数,将该构造函数赋值给definition;Vue.extend({})为获取组件构造函数;
3)在Vue的options注册了components属性,这也是为什么我们能直接使用component的原因。但是,这里还并没有挂载
4)最后返回definition。

if (type === 'component' && isPlainObject(definition)) {
    definition.name = definition.name || id
	definition = this.options._base.extend(definition)
}

this.options[type + 's'][id] = definition
return definition

也就是说,注册了一个Vue的全局API component,也为Vue的options设置了components属性,用户在使用components[id]就会对应设置上组件的构造函数,为后面创建组件对象做准备。

注意:组件的对象是VueComponent,是Vue的子类型,所以上面需要继承Vue的构造函数。

到这里,组件的API注册已经完成了。

2. 组件实例的创建和挂载

由上一篇vue2源码解析(一) - new Vue()的初始化过程我们知道,Vue的初始化过程是:
new Vue() => _init() => $mount() => mountComponent() =>
new Watcher() => updateComponent() => render() => _update()

所以,接下来在Vue内部的处理流程是:在reader(h)的h中将传入的组件配置转化成vnode。reader(h)函数中h的定义是在src/core/instance/render.js

在前言部分的例子的输出内容中,我们发现:组件component的vnode结构与普通的节点元素基本是一致的。也就是说,都是调用_c()获取vnode。

而_c()其实和reader(h)中的h是同一个方法,也就是createElement()方法。_c()是给编译器使用的,h是给用户手写reader()时使用的

进入createElement()方法对应的文件src/core/vdom/create-element.js

2.1 组件初始化方法的定义

2.1.1 获取组件构造方法

观察源码发现createElement()调用_createElement(),在_createElement()中通过相关判断最后调用createComponent()去创建组件:

if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
	// component
	vnode = createComponent(Ctor, data, context, children, tag)
}

其中Ctor = resolveAsset(context.$options, ‘components’, tag))就是出从在Vue初始化全局API-组件注册过程中配置的选项中获取对应的组件构造函数。

2.1.2 处理组件钩子函数和生成vnode

进入createComponent()方法,对应的文件src/core/vdom/create-component.js

  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
  )

这里主要是处理组件的钩子函数:
1)进入installComponentHooks(data)方法

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
    }
  }
}

合并用户编写的钩子函数和系统默认的钩子函数。

2)查看hooksToMerge

const hooksToMerge = Object.keys(componentVNodeHooks)

作用:遍历系统默认钩子。

3)查看componentVNodeHooks
这里就不贴代码了,主要是系统默认钩子函数的定义,有4个钩子:
init():初始化、实例化和挂载组件;
prepatch():组件更新之前调用;
insert():组件被插入到父组件时调用;
destroy():组件销毁时调用。
我们这里主要是看init()方法做了什么,何时被调用。

4)init()的定义
在componentVNodeHooks中定义的init()方法会调用createComponentInstanceForVnode()方法根据生成的VNode创建组件实例,并调用$mount()挂载。

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)
    }
  },

2.2 组件创建和挂载的时机

由上面的分析,我们知道了组件初始化方法定义的过程,那么组件的创建和挂载是何时进行的呢?子组件的生命周期和父组件的生命周期是什么关系呢?

我们再回顾一下new Vue()的初始化过程,new Vue() => _init() => $mount() => mountComponent() =>
new Watcher() => updateComponent() => render() => _update() => patch() => createElm()

2.2.1 父子组件生命周期的关系

我们知道,patch()方法就是diff算法,作用是:初始化或遍历更新节点。所以,组件创建的init()方法应该是在patch()中调用

因为通过上面的分析我们知道,组件component的vnode结构与普通的节点元素基本是一致的。也就是说,子组件是父组件的一部分,和普通节点一样,都是父组件在创建时通过patch()函数遍历生成的真实子节点。所以,肯定是“先父后子”的创建过程

我们通过源码去验证一下我们的推断:

2.2.2 子组件创建和挂载的时机

由于子组件在父组件中与普通元素节点无异,所以,也是通过createElm()递归遍历创建出真实dom的。进入createElm()方法,对应文件src/core/vdom/patch.js。找到了:

if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
      return
    }

进入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
      }
    }
  }

这里会判断组件实例是否存活,不存活则调用init()方法创建并挂载组件实例

if (isDef(i = i.hook) && isDef(i = i.init)) {
	i(vnode, false /* hydrating */)
}

所以,我们的推断是准确的!“先父后子”的创建过程,“由内而外”的挂载过程

三、总结

1. component API的注册时机

new Vue()的初始化过程中的initGlobalAPI()

2. 自定义组件创建和挂载的时机

在父组件创建后,未挂载前。在patch()中创建和挂载。

3. 父子组件的生命周期关系

创建过程:先父后子。挂载过程:先子后父。
parent.created => parent.beforeMounted => child.created => child.beforeMount => child.mounted… => parent.mounted…

你可能感兴趣的:(#,Vue2源码解析,vue,js)