Vue源码解析之 组件化

363 阅读4分钟

本文为原创文章,未获授权禁止转载,侵权必究!

本篇是 Vue 源码解析系列第 4 篇,关注专栏

前言

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

数据驱动 章节我们讲到 createElement 时,最终会调用 _createElement 方法,该方法定义在 src/core/vdom/create-element.js,其中一段逻辑对 tag 判断,如果 tag 为普通的 html 标签,即 tagdiv (案例一),则会实例化一个普通的 vnode 节点,否则通过 createComponent 方法创建一个组件 vnode (案例二)

// 案例一
<div id="app"> {{ message }}</div>

const app = new Vue({
    el: '#app',
    data() {
      return {
        message: 'Hello Vue'
      }
    }
})

// 案例二
<div id="app"></div>

const childComp = {
    name: 'ChildComp',
    template: '<div>{{ msg }}</div>',
    data() {
        return {
            msg: 'Hello Vue'
        }
    }
}

const app = new Vue({
    el: '#app',
    // 这里的 h 是 createElement 方法
    render: h => h(childComp)
})
// tag 为 "div"
if (typeof tag === 'string') {
  let Ctor
  ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
  if (config.isReservedTag(tag)) {
    // platform built-in elements
    vnode = new VNode(
      config.parsePlatformTagName(tag), data, children,
      undefined, undefined, context
    )
  } else if (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 {
  // tag 为 组件对象
  // direct component options / constructor
  vnode = createComponent(tag, data, context, children)
}

组件创建

上文分析到组件的创建实际是调用 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)
  }

  // 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, context)
    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 方法主要有三个核心步骤:

  • 构造子类构造函数
const baseCtor = context.$options._base

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

这里的 baseCtor 实际就是 Vue, context.$options._base 定义在 Vue 初始化阶段。在 src/core/global-api/index.js 文件里 initGlobalAPI 函数中有这么一段逻辑:

Vue.options._base = Vue

这里定义的是 Vue.options,但 createComponent 取的是 context.$options。实际上在 Vue 初始化时,即 _init 方法,它被定义在 src/core/instance/init.js,存在如下逻辑:

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

mergeOptions 方法主要把 Vue 构造函数的 options 和用户传入的 options 合并到 vm.$options 上,这样我们就能通过 vm.$options._base 拿到 Vue 这个构造函数了。

之后我们在看下这段逻辑 baseCtor.extend(Ctor),实际是执行 Vue.extend 方法,该方法定义在 src/core/global-api/extend.js

Vue.extend = function (extendOptions: Object): Function {
  extendOptions = extendOptions || {}
  const Super = this // vm
  const SuperId = Super.cid // vm.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)
  }
  
  // 实例化 Sub
  const Sub = function VueComponent (options) {
    // 执行 Vue 实例的初始化逻辑
    this._init(options)
  }
  Sub.prototype = Object.create(Super.prototype)
  Sub.prototype.constructor = Sub
  Sub.cid = cid++
  // options 扩展
  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.
  // 初始化 props
  if (Sub.options.props) {
    initProps(Sub)
  }
  // 初始化 computed
  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 的时候对同一个子组件重复构造。(可查看相应的注释)

  • 安装组件钩子函数
// install component management hooks onto the placeholder node
installComponentHooks(data)

installComponentHooks 方法定义在 src/core/vdom/create-component.js

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)) {
      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 的过程中执行相关的钩子函数,这里使用了合并策略,在合并过程中,如果某个时机的钩子已经存在 data.hook 中,那么通过执行 mergeHook 函数做合并,即在最终执行的时候,依次执行这两个钩子函数即可。

  • 实例化 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 没有 children

组件更新

组件 VNode 创建完毕,之后会执行 vm._update 方法,该方法实际是执行 vm.__patch__VNode 转换成真正 DOM 节点。patch 的过程会调用 createEle 方法创建元素节点,它定义在 src/core/vdom/patch.js 中。patch 的流程:createComponent --> 子组件初始化 --> 子组件 render --> 子组件 patch。另外需要注意的是,嵌套组件的插入顺序为 先子后父

function createElm (
  vnode,
  insertedVnodeQueue,
  parentElm,
  refElm,
  nested,
  ownerArray,
  index
) {
  // ...
  if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
    return
  }
  // ...
}

参考

Vue.js 技术揭秘

Vue 源码解析系列

  1. Vue源码解析之 源码调试
  2. Vue源码解析之 编译
  3. Vue源码解析之 数据驱动
  4. Vue源码解析之 组件化
  5. Vue源码解析之 合并配置
  6. Vue源码解析之 生命周期
  7. Vue源码解析之 响应式对象
  8. Vue源码解析之 依赖收集
  9. Vue源码解析之 派发更新
  10. Vue源码解析之 nextTick