十一、组件化

Vue

# 组件化回顾

组件化是Vue中非常重要的一个概念,Vue的核心组成只有数据绑定和组件化,Vue官方文档中关于Vue的组件很简单,一个Vue组件就是一个拥有预定义选项的Vue实例,所以一个组件就是一个Vue实例.

一个组件可以组成页面上一个功能完备的区域,组件可以包含脚本、样式、模板.我们可以把一个页面抽象成若干个组件,也可以把这些组件组合成一个页面.

组件化可以让我们方便的把页面拆分成多个可重用的组件,使用组件可以让我们重用某一个区域,另外组件之间是可以嵌套的.

# 组件注册

vue中注册组件的两种方式

  • 全局组件,任何位置都能使用
  • 局部组件,只能在当前注册的范围中使用
<div id="app"></div>
<script>
    // 注册全局组件comp
    const comp = Vue.component('comp', {
      template: '<div>I am a comp</div>'
    })
    const vm = new Vue({
      el: '#app',
      // 通过h函数创建组件对应的vnode
      render (h) {
          return h(comp)
      }
    })
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14

下面看一下Vue.component的源码实现,其函数定义在core/global-api/index.js中

// 这个函数注册了 Vue.directive()\Vue.component()\Vue.filter()
initAssetRegisters(Vue)
1
2

这个函数在同级的assets.js中定义

/* @flow */

import { ASSET_TYPES } from 'shared/constants'
import { isPlainObject, validateComponentName } from '../util/index'

//参数是Vue构造函数
export function initAssetRegisters (Vue: GlobalAPI) {
  /**
   * 没有直接定义而是遍历数组,ASSET_TYPES里面其实就是'component','directive','filter',这个是直接注册Vue.component,Vue.directive,Vue.filter
   */
  ASSET_TYPES.forEach(type => {
    // 这三个函数都接收两个参数,处理相似所以一起定义
    Vue[type] = function (
      id: string,
      definition: Function | Object
    ): Function | Object | void {
      // 没有传定义的话会找到之前options中定义的方法直接返回
      // id就是组件名称 or 指令名称
      if (!definition) {
        return this.options[type + 's'][id]
      } else {
        // 验证名称是否合法,不合法的话就直接报警告
        if (process.env.NODE_ENV !== 'production' && type === 'component') {
          validateComponentName(id)
        }
        // <————————这里重点重点重点看:————————————>
        /**
         * 判断类型是否是组件,且定义是否是原始的Object,判断其转换成字符串是不是'[object Object]'
         */
        if (type === 'component' && isPlainObject(definition)) {
          // 如果设置了组件名称name就用name,如果没有就用id作为组件名称
          definition.name = definition.name || id
          // this.options._base就是Vue构造函数
          // Vue.extend()就是把普通对象转换成了VueComponent构造函数
          // 官方文档中也可以直接传一个Vue.extend构造函数,如果第二个参数definition是Vue.extend的构造函数,那么就直接执行return的最后一句话
          definition = this.options._base.extend(definition)
        }
        //<———————————————————————————————>
        // 如果是指令
        ...
        // 所有的内容处理之后够会直接挂载到this.options下面去,通过这个注册的是全局的组件\指令等
        this.options[type + 's'][id] = definition
        return definition
      }
    }
  })
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48

下面看一下Vue.extend方法.这个方法把该组件的选项对象转换成Vue构造函数的子类,也就是对应组件的构造函数.

# Vue.extend

内部基于传入的选项对象,创建了组件的构造函数,组件的构造函数继承自Vue构造函数,所以组件对象拥有和实例一样的成员

export function initExtend (Vue: GlobalAPI) {
  /**
   * Each instance constructor, including Vue, has a unique
   * cid. This enables us to create wrapped "child
   * constructors" for prototypal inheritance and cache them.
   * 定义唯一cid的目的,是保证创建一个包裹的子构造函数,通过原型继承,并且能够缓存他们
   */
  Vue.cid = 0
  let cid = 1

  /**
   * Class inheritance
   * 参数是组件的选项,对象
   */
  Vue.extend = function (extendOptions: Object): Function {
    extendOptions = extendOptions || {}
    // Super是this是Vue构造函数,或者是组件的构造函数
    const Super = this
    const SuperId = Super.cid
    // 从缓存中加载组件的构造函数,如果有就直接返回,没有就初始化成一个空对象{}
    const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
    if (cachedCtors[SuperId]) {
      return cachedCtors[SuperId]
    }

    // 获取组件名称,开发环境验证组件名称是否合法
    // 在Vue.component中已经验证过一次,但是Vue.extend在外部可以直接使用,所以这里再验证一次
    const name = extendOptions.name || Super.options.name
    if (process.env.NODE_ENV !== 'production' && name) {
      // 如果是开发环境验证组件的名称
      validateComponentName(name)
    }

    // ----------核心代码-------------
    // 创建一个构造函数 VueComponent ,组件对应的构造函数
    const Sub = function VueComponent (options) {
      // 调用_init()初始化
      this._init(options)
    }
    // 改变了构造函数的原型,让其继承自Vue,故所有的组件都继承自Vue
    Sub.prototype = Object.create(Super.prototype)
    Sub.prototype.constructor = Sub
    //设置cid,后面缓存的时候用
    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.
    // 把Super中的成员拷贝到VueComponent构造函数中来
    // 初始化子组件的props,computed
    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.
    // 注册Vue.component\Vue.filter\Vue.directive方法
    ASSET_TYPES.forEach(function (type) {
      Sub[type] = Super[type]
    })
    // enable recursive self-lookup
    // 在选项的components中记录自己
    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
    // 最后返回组件的构造函数VueComponent
    return Sub
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95

# 调试组件注册过程

# 组件的创建过程

  1. 在core/vdom/create-element.js里面,createElement函数,里面调用了_createElement函数,在函数中,判断tag是不是string,不是说明是一个组件,这个时候调用了createComponent函数,并返回了其对应的vnode对象,下面看一下这个函数的内部
export function createComponent (
  // 组件类,构造函数,函数,对象
  Ctor: Class<Component> | Function | Object | void,
  // 创建vnode需要的数据
  data: ?VNodeData,
  // 创建上下文,Vue实例或者当前组件实例
  context: Component,
  // 子节点数组
  children: ?Array<VNode>,
  // 标签名称
  tag?: string
  // 返回值是创建好的VNode对象
): VNode | Array<VNode> | void {
  if (isUndef(Ctor)) {
    return
  }

  // 通过实例的选项获取Vue构造函数
  const baseCtor = context.$options._base

  // plain options object: turn it into a constructor
  // 如果 Ctor 是选项对象的话
  // 就调用extend,把选项对象转换成组件的构造函数
  if (isObject(Ctor)) {
    Ctor = baseCtor.extend(Ctor)
  }
  ...

  // async component
  // 处理异步组件,如果Ctor上没有cid,就是异步组件
  // 组件的构造函数中设置了cid,这里跳过
  let asyncFactory
  if (isUndef(Ctor.cid)) {
    ...
  }

  data = data || {}

  // 当组件构造函数创建完毕后,合并当前组件选项和通过Vue.mixins混入的选项
  resolveConstructorOptions(Ctor)

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

  ...
  // 创建组件vnode的核心位置
  // 安装组件的钩子函数 默认钩子函数init/prepatch/insert/destory
  installComponentHooks(data)

  // return a placeholder vnode
  // 获取组件的名称
  const name = Ctor.options.name || tag
  // 核心核心核心
  // 创建组件对应的VNode对象
  // { Ctor, propsData, listeners, tag, children },componentOptions有这些属性
  // 注意:Ctor在init钩子函数内部通过new VNode.componentOptions.Ctor创建了组件的对象
  const vnode = new VNode(
    `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
    data, undefined, undefined, undefined, context,
    { Ctor, propsData, listeners, tag, children },
    asyncFactory
  )
    ...

  // 最后返回vnode对象
  return vnode
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
  1. 这里看一下installComponentHooks函数,这个函数是创建组件vnode的核心
function installComponentHooks (data: VNodeData) {
  //获取data.hook,用户传入的组件钩子函数
  const hooks = data.hook || (data.hook = {})
  // 这里遍历hooksToMerge中的名字,init/prepatch/insert/destory
  for (let i = 0; i < hooksToMerge.length; i++) {
    const key = hooksToMerge[i]
    // 获取用户传入的钩子函数
    const existing = hooks[key]
    // componentVNodeHooks的钩子函数
    const toMerge = componentVNodeHooks[key]
    if (existing !== toMerge && !(existing && existing._merged)) {
      // 用mergeHook把两者的钩子函数合并到一起
      hooks[key] = existing ? mergeHook(toMerge, existing) : toMerge
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
  1. 这里面的hooksToMerge是啥?
const hooksToMerge = Object.keys(componentVNodeHooks)
1
  1. 找到这个发现是这个componentVNodeHooks对象的key的集合,那componentVNodeHooks是什么?里面有四个钩子函数,而init钩子函数中,通过createComponentInstanceForVnode函数创建组件的实例
const componentVNodeHooks = {
  init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
    if (...) {
      ...
    } 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) {
    ...
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
  1. 那createComponentInstanceForVnode这个方法内部是这样返回的
export function createComponentInstanceForVnode (
  ...
): Component {
  ...
  // 组件的构造函数,传入了options
  return new vnode.componentOptions.Ctor(options)
}
1
2
3
4
5
6
7

下面跳回2看一下mergeHook如何将两个钩子函数合并?

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
}
1
2
3
4
5
6
7
8
9
10

由此组件的创建过程就完毕了,createComponent这个函数中初始化了四个钩子函数,在init钩子函数中创建了组件对象.init钩子函数在什么时候调用的?在patch中调用.

# 组件的patch过程

没看懂,可以再看一遍

core/vdom/patch.js中定义了patch函数,在patch函数内部最终会调用createElm把vnode转换成真实DOM然后挂载到DOM树,那createElm中是如何处理组件中的vnode的呢?

createElm会调用createComponent来处理组件中的vnode

先创建父组件再创建子组件,先挂载子组件再挂载父组件

组件不是拆分的越多越好,因为嵌套一个组件,就会重复执行一遍组件的创建过程,比较消耗性能.组件的抽象过程要合理.

之后的看不懂了,没有记

# mountComponent之后的渲染过程

image

这里注意的点是,在createComponent的时候,创建父组件,父组件中遇到自定义组件,继续创子组件,子组件的内容返回,更新父组件的内容,层层向上

更新时间: 2022-01-26 22:04