vue源码解读四:组件化之创建组件vnode

489 阅读3分钟

本文主要内容摘抄自黄轶老师的慕课网课程Vue.js 源码全方位深入解析 全面深入理解Vue实现原理,主要用于个人学习和复习,不用作其他用途。

组件化

Vue.js 另一个核心思想是组件化。所谓组件化,就是把页面拆分成多个组件 (component),每个组件依赖的 CSS、JavaScript、模板、图片等资源放在一起开发和维护。组件是资源独立的,组件在系统内部可复用,组件和组件之间可以嵌套。

我们在用 Vue.js 开发实际项目的时候,就是像搭积木一样,编写一堆组件拼装生成页面。那么在后面的几篇文章,将从源码的角度来分析 Vue 的组件内部是如何工作的,只有了解了内部的工作原理,才能让我们使用它的时候更加得心应手。

接下来我们会用 Vue-cli 初始化的代码为例,来分析一下 Vue 组件初始化的一个过程。

import Vue from 'vue'
import App from './App.vue'

var app = new Vue({
  el: '#app',
  // 这里的 h 是 createElement 方法
  render: h => h(App)
})

这段代码我们都很熟悉,它和我们上一章相同的点也是通过 render 函数去渲染的,不同的这次通过 createElement 传的参数是一个组件而不是一个原生的标签,那么接下来我们就开始分析这一过程。

createComponent

我们在分析 createElement 的实现的时候,它最终会调用 _createElement 方法,其中有一段逻辑是对参数 tag 的判断,如果是一个普通的 html 标签,比如是一个普通的 div,则会实例化一个普通 VNode 节点,否则通过 createComponent 方法创建一个组件 VNode。

if (typeof tag === 'string') {
  ...
  vnode = new VNode(tag, data, children, undefined, undefined, context)
} else {
  vnode = createComponent(tag, data, context, children)
}

如果是一个组件,那么tag是一个对象,这个对象就是我们在写组件的时候export default导出来的对象,可以看到template模板已经被转化为了render函数。

image.png

所以接下来我们来看一下 createComponent 方法的实现,它定义在 src/core/vdom/create-component.js 文件中:

export function createComponent (Ctor,data,context,children, tag) {
  const baseCtor = context.$options._base

  if (isObject(Ctor)) {
    Ctor = baseCtor.extend(Ctor)
  }
  ...
  data = data || {}
  ...

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

  const vnode = new VNode(
    `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
    data, undefined, undefined, undefined, context,
    { Ctor, propsData, listeners, tag, children },
    asyncFactory
  )

  return vnode
}

构造子类构造函数

const baseCtor = context.$options._base

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 函数有这么一段逻辑:

Vue.options._base = Vue

我们会发现,这里定义的是 Vue.options,而我们的 createComponent 取的是 context.$options,实际上在 src/core/instance/init.js 里 Vue 原型上的 _init 函数中有这么一段逻辑:

vm.$options = mergeOptions(
  resolveConstructorOptions(vm.constructor),
  options || {},
  vm
)

vm 是 Vue构造函数的实例,那么vm.constructor就等于Vue构造函数。这样就把 Vue 上的一些 option 扩展到了 vm.options上,所以我们也就能通过 vm.options 上,所以我们也就能通过 `vm.options._base` 拿到 Vue 这个构造函数了。

在了解了 baseCtor 指向了 Vue 之后,我们来看一下 Vue.extend 函数的定义,在 src/core/global-api/extend.js 中。

Vue.extend = function (extendOptions: Object): Function {
  extendOptions = extendOptions || {}
  // this表示Vue构造函数
  const Super = this
  const SuperId = Super.cid
  const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
  // 如果某个组件被多次引用,那么就对构造函数做一个缓存,以免每次都要创建构造函数
  // 当组件第一次被使用的时候,就在extendOptions增加一个属性,当下次再被引用的时候把extendOptions传进来,发现已经被创建过了,那么就直接返回
  if (cachedCtors[SuperId]) {
    return cachedCtors[SuperId]
  }
  // 组件构造函数
  const Sub = function VueComponent (options) {
    this._init(options)
  }
  // 组件构造函数的原型指向Vue的原型,实现继承
  Sub.prototype = Object.create(Super.prototype)
  // 把组件构造函数的constructor重新指向组件构造函数
  Sub.prototype.constructor = Sub
  Sub.cid = cid++
  // 把Vue构造函数的options和组件构造函数的option做一个合并
  Sub.options = mergeOptions(
    Super.options,
    extendOptions
  )
  Sub['super'] = Super
  ...
  Sub.extend = Super.extend
  Sub.mixin = Super.mixin
  Sub.use = Super.use
  ASSET_TYPES.forEach(function (type) {
    Sub[type] = Super[type]
  })
  ...
  cachedCtors[SuperId] = Sub
  return Sub
}

ue.extend 的作用就是构造一个 Vue 的子类,它使用一种非常经典的原型继承的方式把一个纯对象转换一个继承于 Vue 的构造器 Sub 并返回,然后对 Sub 这个对象本身扩展了一些属性,如扩展 options、添加全局 API 等;并且对配置中的 props 和 computed 做了初始化工作;最后对于这个 Sub 构造函数做了缓存,避免多次执行 Vue.extend 的时候对同一个子组件重复构造。

这样当我们去实例化 Sub 的时候,就会执行 this._init 逻辑再次走到了 Vue 实例的初始化逻辑。

安装组件钩子函数

installComponentHooks(data)

Vue.js 使用的 Virtual DOM 参考的是开源库 snabbdom,它的一个特点是在 VNode 的 patch 流程中对外暴露了各种时机的钩子函数,方便我们做一些额外的事情,Vue.js 也是充分利用这一点,在初始化一个 Component 类型的 VNode 的过程中实现了几个钩子函数:

const componentVNodeHooks = {
  // 初始化钩子
  init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
    ...
    const child = vnode.componentInstance = createComponentInstanceForVnode(
      vnode, activeInstance)
    child.$mount(hydrating ? vnode.elm : undefined, hydrating)
  },

  prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
    ...
  },

  insert (vnode: MountedComponentVNode) {
    ...
  },

  destroy (vnode: MountedComponentVNode) {
    ...
  }
}

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)) {
      hooks[key] = existing ? mergeHook(toMerge, existing) : toMerge
    }
  }
}

整个 installComponentHooks 的过程就是把 componentVNodeHooks 的钩子函数合并到 data.hook 中,在 VNode 执行 patch 的过程中执行相关的钩子函数。

实例化 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

到此就生成了一个组件的vnode,它仅仅是一个占位符vnode,并不是渲染vnode,如下代码就是一个组件的占位符vnode:

{
    tag"vue-component-1-App",
    data: {
        on: undefined,
        hook: {
            init(){},
            prepatch(){},
            insert(){},
            destory(){}
        }
    },
    children: undefined,
    text: undefined,
    componentInstance: undefined,
    componentOptions: {
        Ctor: function VueComponent(){}, // 组件的构造器
        childrenundefined,
        listenersundefined,
        propsDataundefined,
        tagundefined
    }
    ...
}