Skip to content
On this page

组件化

本章节主要分析 Vue 组件的创建过程

在创建 VNode 过程中,调用到 vdomcreate-element.js

调用栈:$mount -> mountComponent -> Watcher -> get -> updateComponent(vm._update) -> render -> createElement

js
// vdom/create-element.js
function _createElement(context, tag, data, children, normalizationType) {
  // 以 render(h) { return h(APP)} 为例子
  // 传入的tag是APP对象
  vnode = createComponent(tag, data, context, children) // 根据构造的信息创建组件的 vnode
  
  // 如果是通过 components 注册使用的组件
  if ((!data || !data.pre) && isDef(Ctor = resolveAssest(context.$options, 'components', tag))){
    vnode = createComponent(Ctor, data, context, children, tag)
  }
  ...
  // 这里的context 都是指当前的 vm 实例
  return vnode // 最后是返回生成的 组件vnode
}

分析createComponent函数

js
// vdom/create-components.js
function createComponnet(Ctor, data, context, children, tag) {
  ...
  const baseCtor = context.$options._base 
  // 这里 baseCtor = Vue, 之前通过初始化过程中混入 (global-api.js)
  // Vue.options._base = Vue
  // 之后再通过 mergeOptions 合并到实例
  if (isObject(Ctor)) {
    Ctor = baseCtor.extend(Ctor) // 这里就是 Vue.extend(Ctor),返回一个用该配置生成的构造器
  }
  ...
  installComponentHooks(data) // data是创建时提供,就是render函数写法中的data对象
  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
  )
  // 这一行是创建 vnode 过程,其中的倒数第二个参数为 componentOptions 选项
  ...
  return vnode // 返回生成的组件 vnode
}

组件构造器生成函数(extend)

上述生成 组件vnode 的过程中,会调用 Vue.extend ,生成组件对应的构造器函数

js
// global-api/extend.js
function initExtend (Vue) {
  Vue.cid = 0
  let cid = 0
  
  Vue.extend = function(extendOptions) {
    extendOptions = extendOptions || {}
    // 注意这里是 Vue 的静态方法,this = Vue
    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;
    // 子类构造器,开发过程中的单文件组件就是通过这个构造器生成
    const Sub = function VueComponent(options) {
      // 实际上执行Sub.prototype._init => S
      this._init(options)
    }
    Sub.prototype = Object.create(Super.prototype)
    Sub.prototype.constructor = Sub
    // 以上构成 Sub 继承 Super 的原型链
    Sub.cid = cid++
    // 合并传入的选项和 Super 的选项
    Sub.options = mergeOptions(Super.options, extendOptions)
    Sub['super'] = Super
    // 将Super 的 静态方法赋值 给Sub 使用,之后也可以将 Sub 作为新的 Super 进行 Sub.extend() 等等
    if (Sub.options.props) {
      initProps(Sub)
    }
    if (Sub.options.computed) {
      initComputed(Sub)
    }
    Sub.extend = Super.extend
    Sub.mixin = Super.mixin
    Sub.use = Super.use
    // ['component', 'directive', 'filter'] Super的选项混入
    ASSET_TYPES.forEach(function (type) {
      Sub[type] = Super[type]
    })
    if (name) {
      Sub.options.components[name] = Sub
    }
    Sub.superOptions = Super.options
    Sub.extendOptions = extendOptions
    Sub.sealedOptions = extend({}, Sub.options)
    cachedCtors[SuperId] = Sub // 缓存已生成的构造器,当传入相同对象的时候就能获取,避免重复生成
    return Sub
  }
}

组件的钩子函数安装

在上述生成 组件vnode 的过程中,调用到 installComponentHooks 函数来安装 组件的钩子

js
// vdom/create-component.js
function installComponentHooks (data) {
  const hooks = data.hook || (data.hook = {})
  for (let i = 0; i < hooksToMerge.length; i++) {
    const key = hooksToMerge[i]
    const existing = hooks[i]
    const toMerge = componentVNodeHooks[key]
    if (existing !== toMerge && !(existing && existing._merged)) {
      hooks[key] = existing ? mergeHook(toMerge, existing) : toMerge
    }
  }
}
// 简单来说就是将传入的data中的 hook 对象与组件默认的hooks合并
const componentVNodeHooks = {
  init() {},
  prepatch() {},
  insert() {},
  destroy() {},
}

到这里为止,已经生成 组件vnode(也可以叫占位vnode), 接下来是将 vnode 返回给上一个调用栈,一般来说是 vdom/create-element.js 中的 _createElement 方法

在 _createElement 方法中再将返回的 vnode 返回到上一级调用栈,就是 vm 的 _update 方法

在 _update 方法中,通过 patch 方法,最终会将 vnode 传递到 vdom/patch.js 的 patch 方法中

再通过其中的 createComponent 方法生成对应的 patch

js
// 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 */) 
      // 调用hooks中的init钩子
      // 在 vdom/create-component.js 中生成占位 vnode 添加的 componentVNodeHooks
    }
    if (isDef(vnode.componentInstance)) {
      initComponent(vnode, insertedVnodeQueue)
      insert(parentElm, vnode.elm, refElm)
      if (isTrue(isReactivated)) {
        reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
      }
      return true
    }
  }
}

组件的 init 钩子函数

js
// vdom/create-components.js
const componentVNodeHooks = {
  init(vnode, hydrating) {
    if (vnode.componentInstance && !vnode.componentInstance._isDestroyed && vnode.data.keepAlive) {
      // kept-alive components
      const mountedNode = vnode
      componentVNodeHooks.prepatch(mountedNode, mountedNode) // keep-alive 重复渲染会当成prepatch
    } else {
      // 传入对应的 占位vnode
      // createComponentInstanceForVnode 会返回一个 vm 实例!!!
      const child = vnode.componentInstance = createComponentInstanceForVnode(
        vnode,
        activeInstance // 这里的 activeInstance 就是这个 组件vnode 的 父vm 实例
      )
      // 这里$mount 传入的是 undefined
      child.$mount(hydrating ? vnode.elm : undefined, hydrating)
    }
  }
  ...
}

createComponentInstanceForVnode 函数

js
function createComponentInstanceForVnode (vnode, parent) {
  const options = {
    _isComponent: true, // 组件标识
    _parentVnode: vnode, // 占位 vnode
    parent
  }
  ...
  return new vnode.componentOptions.Ctor(options)
  // 这里的这个 componentOptions 也就是本篇一开始创建这个组件占位vnode时,出入的那个对象参数 
  // { Ctor, propsData, listeners, tag, children }
  // 其中Ctor 是通过 extend 得到的 Sub(或者叫 VueComponent) 构造器函数
  // 也就是会重新利用这个options得到一个新的组件 vm 实例返回
}

组件的初始化 _init

js
// instance/init.js
Vue.prototype._init = function (options) {
  ...
  if (options && options._isComponent) {
    initInternalComponent(vm, options)
  } else {
    /** 这里是new Vue的合并options **/
  }
  ...
}
js
function initInternalComponent(vm, options)  {
  const opts = vm.$options = Object.create(vm.constructor.options) // 这里就是 Sub 的 options
  const parentVnode = options._parentVnode // 占位vnode
  opts.parent = options.parent // 父 vm 实例
  options._parentVnode = parentVnode
  ...
  // 这里就将 Sub 的options 加到了组件实例的 $options上
}
js
// 在创建完组件 vm 实例之后
// 进行实例挂载
// child.$mount(undefined)
// platform/web/runtime/index.js
Vue.prototype.$mount = function(el, hydrating /* false */) {
  ...
  return mountComponent(this, el, hydrating) // el = undefined
}
js
// instance/lifecycle.js
function mountComponent(vm, el, hydrating) {
  // el = undefined
	//  hydrating = false
  // 接下来就是通过之前的流程,创建渲染 Watcher -> updateComponent -> vm._render -> vm._update
  // _render 中调用的 $options.render 是在 vue-loader 解析单文件时候得到的,也可以手写render函数
}

组件的 _update 过程

还是调用Vue原型上的_update函数

js
const prevEl = vm.$el
const prevVnode = vm._vnode // 实例vm 刚创建出来,没有这个_vnode属性
const restoreActiveInstance = setActiveInstance(vm) // 这里就是存储当前 vm 实例的过程
vm._vnode = vnode // 实例 vm 的_vnode 属性是当前的 组件实际vnode而非占位vnode
if (!prevVnode) {
  vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false) // 通过patch 之后赋值 $el
}
...
// 到这里为止其实还没有进行挂载到真实的dom上,因为在child.$mount()传进来的是空

回到createComponent方法中

js
// 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 */) 
      // 调用hooks中的init钩子
      // 在 vdom/create-component.js中生成占位vnode添加的componentVNodeHooks
    }
    // 初始化完之后,对组件进行初始化和插入到DOM
    if (isDef(vnode.componentInstance)) {
      initComponent(vnode, insertedVnodeQueue) // vnode.elm = vnode.componentInstance.$el
      insert(parentElm, vnode.elm, refElm) // 插入到DOM
      ...
      return true
    }
  }
}

从根节点开始 -> 通过render生成得到 占位 vnode(<App/>) -> 通过 占位vnode 的 Ctor 生成其对应的vm 实例(vm.componentInstance) -> 通过实例下的 render 方法,生成其 渲染vnode(也就是文中的实际 vnode) -> 最后通过initComponent 、insert 等方法插入 组件 到 DOM 节点中

到这里,就已经将创建出来的 VueComponent 插入到对应的 DOM节点中,并且已经关联好对应的 vm 实例

createComponent 返回 true 之后,调用栈回到 patch 函数内,返回 vnode.elm,最后回到 vm._update 内部