Vue.js 框架源码与进阶 - Vue.js源码剖析 - 组件化

7.1 组件化

组件化回顾

一个 Vue 组件就是一个拥有预定义选项的一个 Vue 实例

  • 一个组件可以组成页面上一个功能完备的区域,组件可以包含脚本、样式、模板
    在这里插入图片描述
    像上面这个图,我们可以把一个页面抽象成若干个组件,把这些组件组合成一个页面。组件就像 ‘积木块’,当我们有了组件之后搭建页面就好像在拼积木。这个图中把一个页面划分成了三部分:页头、主内容区域、侧边栏,每一个组件内部又可能嵌套了粒度更小的组件:比如主内容区域对应的组件,它里面又嵌套了列表组件,它们最终组合成了一个页面。

我们可以总结一下:组件化可以让我们方便地把页面拆分成多个可重用的组件,使用组件可以让我们重用页面中的某一个区域。另外,组件之间是可以嵌套的,有了组件之后我们开发页面就像搭积木一样。

组件化机制

  • 组件化可以让我们方便的把页面拆分成多个可重用的组件
  • 组件是独立的,系统内可重用,组件之间可以嵌套
  • 有了组件可以像搭积木一样开发网页
  • 下面我们将从源码的角度来分析 Vue 组件内部如何工作

组件注册

  • Vue中注册组件的两种方式
    • 全局注册:在任何位置都可以使用
    • 局部注册:只能在当前注册的范围中使用

全局组件的注册方式

<div id="app">
</div>
<script>
  const Comp = Vue.component('comp', {
    template: '<div>Hello Component</div>'
  })
  const vm = new Vue({
    el: '#app',
    render (h) {
	  return h(Comp)
    }
  })
</script>

以上创建了一个全局组件Comp,在任何位置都可以直接使用,在Vue的选项render函数中通过h函数创建组件对应的vnode,下面我们来看一下注册全局组件的Vue.component这个函数的内部实现:

import { initAssetRegisters } from './assets'
...
// 注册 Vue.directive()、Vue.component()、Vue.filter()
initAssetRegisters(Vue)
import { ASSET_TYPES } from 'shared/constants'
...
// 接收Vue构造函数作为参数
export function initAssetRegisters (Vue: GlobalAPI) {
  // 遍历 ASSET_TYPES 数组,为 Vue 定义相应方法
  // ASSET_TYPES 包括了directive、 component、filter
  ASSET_TYPES.forEach(type => {
    ...
  })
}
// 对应 Vue.component、Vue.directive、Vue.filter
export const ASSET_TYPES = [
  'component',
  'directive',
  'filter'
]
ASSET_TYPES.forEach(type => {
  Vue[type] = function (
    id: string,
    definition: Function | Object
  ): Function | Object | void {
    if (!definition) {
      // 没有传定义 直接取出定义好的 directives、components、filters 并返回
      return this.options[type + 's'][id]
    } else {
      /* istanbul ignore if */
      // 环境判断
      if (process.env.NODE_ENV !== 'production' && type === 'component') {
        validateComponentName(id)
      }
      // Vue.component('comp', { template: '' })
      // 类型(type)是否是组件 如果是 判断传递的对象(definition)是否是原始对象([object Object])
      if (type === 'component' && isPlainObject(definition)) {
        definition.name = definition.name || id
        // 把组件配置转换为组件的构造函数
        // _base == Vue
        definition = this.options._base.extend(definition)
      }
      // 类型(type)是否是指令 如果是 判断传递的函数(definition) 将function设置给bind update
      if (type === 'directive' && typeof definition === 'function') {
        definition = { bind: definition, update: definition }
      }
      // 指令 - 传对象 或 组件 - 直接传构造函数:则直接存储
      // 全局注册,存储资源并赋值
      // this.options['components']['comp'] = definition
      this.options[type + 's'][id] = definition
      return definition
    }
  }
})

重点来看 Vue.component 全局组件的注册过程,其中如果第二个参数传递是对象,调用Vue.extend,把组件的选项对象转换成组件的构造函数,最终记录到options.compents中;如果第二个参数是函数,会直接把这个函数记录到options.compents中。

// Vue.component('comp', { template: '' })
// 类型(type)是否是组件 如果是 判断传递的对象(definition)是否是原始对象([object Object])
if (type === 'component' && isPlainObject(definition)) {
  definition.name = definition.name || id
  // 把组件配置转换为组件的构造函数
  // _base == Vue
  definition = this.options._base.extend(definition)
}

Vue.extend

export function initExtend (Vue: GlobalAPI) {
  /**
   * Each instance constructor, including Vue, has a unique
   * cid. This enables us to create wrapped "child
   * constructors" for prototypal inheriance and cache them.
   */
  // 包括Vue在内的每个实例构造函数都有一个唯一的cid
  // 这使我们能够为原型继承创建封装的 "子构造函数",并将它们缓存起来。
  Vue.cid = 0
  let cid = 1

  /**
   * Class inheritance
   */
  Vue.extend = function (extendOptions: Object): Function {
    extendOptions = extendOptions || {}
    // Vue 构造函数
    const Super = this
    const SuperId = Super.cid
    // 从缓存中加载组件的构造函数
    const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
    // 通过cid获取缓存的组件的构造函数,有的话直接返回
    if (cachedCtors[SuperId]) {
      return cachedCtors[SuperId]
    }

    const name = extendOptions.name || Super.options.name
    if (process.env.NODE_ENV !== 'production' && name) {
      // 如果是开发环境验证组件的名称
      validateComponentName(name)
    }

    // 给Sub初始化VueComponent构造函数(组件构建的构造函数)
    const Sub = function VueComponent (options) {
      // 调用 _init() 初始化
      this._init(options)
    }
    // 原型继承自 Vue
    // Vue的原型上之前注入了_init()
    // 所以Sub的实例也可以访问到_init()
    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和计算属性,我们在扩展时在Vue实例上,在扩展原型上定义代理获取器
    // 这样就避免了对每个创建的实例进行Object.defineProperty调用
    if (Sub.options.props) {
      initProps(Sub)
    }
    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
    // 把组件构造构造函数保存到 Ctor.options.components.comp = Ctor
    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
    // 把组件的构造函数缓存到 options._Ctor
    cachedCtors[SuperId] = Sub
    return Sub
  }
}

通过源码我们可以看到,它内部就是基于传入的选项对象创建了组件的构造函数,组件的构造函数继承自Vue构造函数,所以组件对象拥有和Vue实例一样的成员

调试组件注册过程

  • 接下来通过调试查看全局组件的注册过程
  • 调试代码
<div id='app'>
</div>
<script src="../../dist/vue.js"></script>
<script>
  const Comp = Vue.component('comp', {
    template: '<div>Hello Component</div>'
  })
  const vm = new Vue({
    el: '#app',
    render (h) {
    return h(Comp)
    }
  })
</script>
  • 增加断点并F5进入
    在这里插入图片描述
  • 此时的type就是component,我们调用的是Vue.component这个方法,id是组件的名称comp、definition是调用时传入的第二个参数:组件的选项对象,里边只有template选项。往下执行,如果第二个参数为空,会获取这个组件,此时第二个参数有值
    在这里插入图片描述
  • 开发环境中,会验证组件的名称,如果组件的名称不合法会警告
    在这里插入图片描述
  • 接下来获取组件的名称,调用vue.extend,进入extend,在这个方法上面已经初始化了cid,用来唯一标识当前组件的构造函数并且作为缓存的键。接下来获取options选项中的_Ctor(缓存的构造函数)
    在这里插入图片描述
  • 当前的_Ctor是undefined,它初始化为一个空对象,接下来从缓存中获取构造函数(取不到,因为此时缓存中还没有)
    在这里插入图片描述
  • 接下来验证组件名称,Vue.component中已经验证过组件名称,但是如果直接调用Vue.extend也要去验证(且刚刚是在开发环境执行的,生产环境下会过滤掉,不用担心性能问题)
    在这里插入图片描述
  • 接着,创建了组件的构造函数VueComponent
    在这里插入图片描述
  • 组件的构造函数继承自Vue构造函数
    在这里插入图片描述
    在这里插入图片描述
  • cid++ 记录到sub中,然后合并Super.options和当前调用Vue.extend传入的选项
    在这里插入图片描述
    在这里插入图片描述
  • 执行完毕后Sub的选项,可以看到它把刚刚两个选项合并起来了,在组件的构造函数的选项中也可以访问到_base,从这里还可以看出Vue的选项都被合并到组件的选项中,所以Vue选项中的成员可以在任何组件中去使用
    在这里插入图片描述
  • 再去初始化组件中的props、computed
    在这里插入图片描述
  • 继承Vue中的一些静态成员,记录不同的options
    在这里插入图片描述
  • 此处的extendOptions里面有一个_Ctor空对象
    在这里插入图片描述
  • 缓存当前组件的构造函数,当这个构造函数换成完毕之后,再来看extendOptions中的_Ctor,里面记录了刚刚缓存好的构造函数
    在这里插入图片描述
  • 返回到Vue.component方法,把创建好的构造函数最终记录到Vue构造函数的options选项的components对应的组件名称中,当前是comp。因为记录到了Vue的构造函数中,所以所有位置都可以直接使用全局组件
    在这里插入图片描述
  • 全局组件注册完毕

组件创建过程

  • 回顾首次渲染过程
    • Vue 构造函数
    • this._init()
    • this.$mount()
    • mountComponent()
    • new Watcher() 渲染 Watcher
    • updateComponent()
    • vm._render() -> createElement()
    • vm._updata()

观察如下代码:

const Comp = Vue.component('comp', {
  template: '<div>Hello Component</div>'
})
const vm = new Vue({
  el: '#app',
  render (h) {
  return h(Comp)
  }
})

首先通过Vue.component返回了一个组件的构造函数,在render中的h函数就是createElement函数,调用createElement的时候传入了组件的构造函数。

我们要看的就是在createElement中是如何去处理组件的

// 判断 tag 是字符串还是组件
if (typeof tag === 'string') { ...
} else {
  // direct component options / constructor
  vnode = createComponent(tag, data, context, children)
}
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
  }

  // context.$options._base 就是 Vue构造函数
  // 在_init()会把Vue构造函数中的选项合并到Vue实例的选项中
  // 所以此处可以通过context(实例的选项)获取_base
  const baseCtor = context.$options._base

  // plain options object: turn it into a constructor
  // 如果 Ctor 不是一个构造函数,是一个对象
  // 使用 Vue.extend() 创造一个子组件的构造函数
  // render: h => h(App)  这种情况会进入
  if (isObject(Ctor)) {
    Ctor = baseCtor.extend(Ctor)
  }

  ...

  data = data || {}

  // resolve constructor options in case global mixins are applied after
  // component constructor creation
  // 解决在创建组件构造函数后合并当前组件选项和通过Vue.mixins混入的情况下的构造函数选项
  resolveConstructorOptions(Ctor)

  // transform component v-model data into props & events
  // 处理组件上的 v-model
  if (isDef(data.model)) {
    transformModel(Ctor.options, data)
  }

  ...

  // install component management hooks onto the placeholder node
  // 安装组件的钩子函数 init/prepatch/insert/destroy
  // 准备好了 data.hook 中的钩子函数
  installComponentHooks(data)

  // return a placeholder vnode
  const name = Ctor.options.name || tag
  // 创建自定义组件的 VNode,设置自定义组件的名字
  // 记录this.componentOptions = componentOptions
  const vnode = new VNode(
    `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
    data, undefined, undefined, undefined, context,
    { Ctor, propsData, listeners, tag, children },
    asyncFactory
  )
  
  ...

  return vnode
}
  • installComponentHooks() 初始化组件的 data.hook
function installComponentHooks (data: VNodeData) {
  // 获取用户传入的组件的钩子函数
  const hooks = data.hook || (data.hook = {})
  // 用户可以传递自定义钩子函数
  // 把用户传入的自定义钩子函数和 componentVNodeHooks 中预定义的钩子函数合并
  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
    }
  }
}
const hooksToMerge = Object.keys(componentVNodeHooks)
  • 钩子函数定义的位置(init()钩子中创建组件的实例)
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) {
    ...
  },

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

  destroy (vnode: MountedComponentVNode) {
    ...
  }
}
  • 创建组件实例的位置,由自定义组件的 init() 钩子方法调用
export function createComponentInstanceForVnode (
  vnode: any, // we know it's MountedComponentVNode but flow doesn't
  parent: any, // activeInstance in lifecycle state
): Component {
  const options: InternalComponentOptions = {
    _isComponent: true,
    _parentVnode: vnode,
    parent
  }
  // check inline-template render functions
  // 获取 inline-template
  // <comp inline-template> xxxx </comp>
  const inlineTemplate = vnode.data.inlineTemplate
  if (isDef(inlineTemplate)) {
    options.render = inlineTemplate.render
    options.staticRenderFns = inlineTemplate.staticRenderFns
  }
  // 创建组件实例
  return new vnode.componentOptions.Ctor(options)
}
  • init()钩子函数又是在什么地方调用的呢? 它是在patch的过程中调用的

组件实例的创建和挂载过程

我们看过了组件被转换为VNode的过程,接下来再来看init钩子函数调用的位置,因为在init钩子函数在中最终创建了组件对象,init钩子函数是在patch中调用的

  • src/core/vdom/patch.js
  • Vue._update() --> patch() --> createElm() --> createComponent()
  • 创建组件实例,挂载到真实 DOM
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)) {
      // 调用 init() 方法,创建和挂载组件实例
      // init() 的过程中创建好了组件的真实 DOM,挂载到了 vnode.elm 上
      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)) {
      // 调用钩子函数(VNode的钩子函数初始化属性/事件/样式等,组件的钩子函数)
      initComponent(vnode, insertedVnodeQueue)
      // 把组件对应的 DOM 插入到父元素中
      insert(parentElm, vnode.elm, refElm)
      if (isTrue(isReactivated)) {
        reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
      }
      return true
    }
  }
}
  • 调用钩子函数,设置局部作用于样式
function initComponent (vnode, insertedVnodeQueue) {
  if (isDef(vnode.data.pendingInsert)) {
    insertedVnodeQueue.push.apply(insertedVnodeQueue, vnode.data.pendingInsert)
    vnode.data.pendingInsert = null
  }
  vnode.elm = vnode.componentInstance.$el
  if (isPatchable(vnode)) {
    // 调用钩子函数
    invokeCreateHooks(vnode, insertedVnodeQueue)
    // 设置局部作用于样式
    setScope(vnode)
  } else {
    // empty component root.
    // skip all element-related modules except for ref (#3455)
    registerRef(vnode)
    // make sure to invoke the insert hook
    insertedVnodeQueue.push(vnode)
  }
}
  • 调用钩子函数
  function invokeCreateHooks (vnode, insertedVnodeQueue) {
    // 调用 VNode 的钩子函数
    for (let i = 0; i < cbs.create.length; ++i) {
      cbs.create[i](emptyNode, vnode)
    }
    i = vnode.data.hook // Reuse variable
    // 调用组件的钩子函数
    if (isDef(i)) {
      if (isDef(i.create)) i.create(emptyNode, vnode)
      if (isDef(i.insert)) insertedVnodeQueue.push(vnode)
    }
  }

总结

  • 组件实例的创建过程是从上而下(父组件->子组件)
  • 组件实例的挂载过程是从下而上(子组件->父组件)

由此我们可以总结出来,组件的粒度不是越小越好,因为嵌套一层组件,就会重复执行一遍组件的创建过程,比较消耗性能。组件的抽象过程要合理,比如侧边栏组件,它内部的导航如果外部没有再次使用的话,可以把侧边栏和内部的导航设计成一个组件,减少组件重新创建的过程。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值