Vue 源码深入解析之 组件化、createComponent、patch 和 合并配置

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

  2. 我们在用 Vue.js 开发实际项目的时候,就是像搭积木一样,编写一堆组件拼装生成页面。在 Vue.js的官网中,也是花了大篇幅来介绍什么是组件,如何编写组件以及组件拥有的属性和特性。我们将从源码的角度来分析 Vue 的组件内部是如何工作的,只有了解了内部的工作原理,才能让我们使用它的时候更加得心应手。

  3. 接下来我们会用 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 的理解
  1. 我们之前在分析 createElement 的实现的时候,它最终会调用 _createElement 方法,其中有一段逻辑是对参数 tag 的判断,如果是一个普通的 html 标签,是一个普通的 div,则会实例化一个普通 VNode 节点,否则通过 createComponent 方法创建一个组件 VNode,如下所示:
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 {
   
  // direct component options / constructor
  vnode = createComponent(tag, data, context, children)
}
  1. 在这里传入的是一个 App 对象,它本质上是一个 Component 类型,那么它会走到上述代码的 else 逻辑,直接通过 createComponent 方法来创建 vnode。所以接下来我们来看一下 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
}
  1. 从上面可以看到,createComponent 的逻辑也会有一些复杂,但是分析源码比较推荐的是只分析核心流程,分支流程可以之后针对性的看,所以这里针对组件渲染这个 case 主要就三个关键步骤:
    构造子类构造函数,安装组件钩子函数和实例化 vnode

  2. 构造子类构造函数,如下所示:

const baseCtor = context.$options._base

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

我们在编写一个组件的时候,通常都是创建一个普通对象,还是以我们的 App.vue 为例,代码如下:

import HelloWorld from './components/HelloWorld'

export default {
   
  name: 'app',
  components: {
   
    HelloWorld
  }
}
  1. 这里 export 的是一个对象,所以 createComponent 里的代码逻辑会执行到 baseCtor.extend(Ctor),在这里 baseCtor 实际上就是 Vue,这个的定义是在最开始初始化 Vue 的阶段,在 src/core/global-api/index.js 中的 initGlobalAPI 函数有这么一段逻辑:
// 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
  1. 这里定义的是 Vue.options,而我们的 createComponent 取的是 context.$options,实际上在 src/core/instance/init.jsVue 原型上的 _init 函数中有这么一段逻辑:
vm.$options = mergeOptions(
  resolveConstructorOptions(vm.constructor),
  options || {
   },
  vm
)
  1. 这样就把 Vue 上的一些 option 扩展到了 vm.$options 上,所以我们也就能通过 vm.$options._base 拿到 Vue 这个构造函数了。mergeOptions 的实现我们会在后面具体分析,现在只需要理解它的功能是把 Vue 构造函数的 options 和用户传入的 options 做一层合并,到 vm.$options 上。

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

/**
 * Class inheritance
 */
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)
  }

  const Sub = function VueComponent (options) {
   
    this._init(options
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值