Vue2 中组件的本质、组件的实例化、组件实例的挂载

这篇博客的内容是讲讲在 Vue2 中,组件在底层的本质。

在这里,直接抛出结论:组件的本质就是一个个的构造函数,这些函数以组件的定义 options 对象为参数,在 Vue2 中,最顶级的组件就是我们从 vue.js 中导入的 Vue 函数,而子组件是 Vue 底层通过 extend 函数创建出来的 VueComponent 函数。通过 new 这些组件的构造函数,我们可以创建出组件实例。

1,顶级组件(Vue 构造函数)有哪些重要的属性和方法

1-1,Vue 的静态属性

Vue 通过 initGlobalAPI(Vue) 向 Vue 上赋值静态属性,源码如下所示:

// 向 Vue 上挂载一些静态的属性和方法
initGlobalAPI(Vue)
export function initGlobalAPI (Vue: GlobalAPI) {
  // 应用于 Vue 源码内部的工具函数,不建议程序员直接使用。
  Vue.util = {
    warn,
    extend,
    mergeOptions,
    defineReactive
  }

  // 定义 options 对象,该对象用于存储一系列的资源,如:组件、指令和过滤器
  Vue.options = Object.create(null)
  ASSET_TYPES.forEach(type => {
    Vue.options[type + 's'] = Object.create(null)
  })

  // 将与平台无关的内建组件存储到 options.components 中
  extend(Vue.options.components, builtInComponents)
}

在这里,比较重要的是名为 options 的静态属性,这个属性保存着组件能够使用的资源(组件、指令、过滤器),并且我们声明的组件描述对象(.vue 文件中导出的对象)中的信息也会被保存到 options 静态属性中。

1-2,Vue 的静态方法

Vue 通过 initGlobalAPI(Vue) 向 Vue 上赋值静态方法,源码如下所示:

// 向 Vue 上挂载一些静态的属性和方法
initGlobalAPI(Vue)
export function initGlobalAPI (Vue: GlobalAPI) {
  // 定义全局 API。set、delete、nextTick
  Vue.set = set
  Vue.delete = del
  Vue.nextTick = nextTick

  // 初始化 Vue.use()
  initUse(Vue)
  // 初始化 Vue.mixin()
  initMixin(Vue)
  // 初始化 Vue.extend()
  initExtend(Vue)
  // 初始化 Vue.component()、Vue.directive()、Vue.filter(),用于向 Vue 中注册资源
  initAssetRegisters(Vue)
}

export function initUse (Vue: GlobalAPI) {
  Vue.use = function (plugin: Function | Object) {
    ......
  }
}

1-3,Vue 的原型方法

Vue 原型方法的定义在 src/core/instance/index.js 文件中,源码如下所示:

function Vue (options) {
  // 如果当前的环境不是生产环境,并且当前命名空间中的 this 不是 Vue 的实例的话,
  // 发出警告,Vue 必须通过 new Vue({}) 使用,而不是把 Vue 当做函数使用
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  // 执行 vm 原型上的 _init 方法,该方法在 initMixin 方法中定义
  this._init(options)
}

// 下面函数的作用是:往 Vue 的原型上写入原型函数,这些函数是给 Vue 的实例使用的
// 这些函数分为两类:一类是 Vue 内部使用的,特征是函数名以 '_' 开头;
//                 还有一类是给用户使用的,特征是函数名以 '$' 开头,这些函数可以在 Vue 的官方文档中看到;
// 写入 vm._init
initMixin(Vue)
// 写入 vm.$set、vm.$delete、vm.$watch
stateMixin(Vue)
// 写入 vm.$on、vm.$once、vm.$off、vm.$emit
eventsMixin(Vue)
// 写入 vm._update、vm.$forceUpdate、vm.$destroy
lifecycleMixin(Vue)
// 写入 vm.$nextTick、vm._render
renderMixin(Vue)

export default Vue

在这里,vue 首次声明了 Vue 构造函数,函数的主体是执行原型上的 _init 方法进行实例的初始化。

然后在下面以 Vue 构造函数为参数执行一系列的函数,这些函数的作用是:往 Vue 构造函数的原型对象上添加新的函数,这些函数是给组件实例调用的,以 initMixin() 函数为例,源码如下所示:

export function initMixin (Vue: Class) {
  Vue.prototype._init = function (options?: Object) {
    ......  
  }
}

 

2,子组件(VueComponent 构造函数)的创建

在底层,子组件是借助 extend 方法创建出来的,这部分源码可以看我的这篇博客。

3,组件的实例化

组件(构造函数)只有一个,但是我们可以在模板字符串中多次使用某一个组件,每使用一次组件,Vue 的底层便会通过 new Ctor(options) 创建出一个对应的组件实例。

我们以一个简单的例子来看看组件的实例化。

我们首先定义了一个组件配置对象 componentOption,至此我们还没有使用 Vue,只是按照 Vue 的规范声明了一个组件配置对象。

接下来,我们以 componentOption 对象为参数执行 new Vue(),这会创建出一个组件实例。Vue 构造函数的函数体如下所示:

function Vue (options) {
  // 如果当前的环境不是生产环境,并且当前命名空间中的 this 不是 Vue 的实例的话,
  // 发出警告,Vue 必须通过 new Vue({}) 使用,而不是把 Vue 当做函数使用
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  // 执行 vm 原型上的 _init 方法,该方法在 initMixin 方法中定义
  this._init(options)
}

Vue 函数内部借助 _init 原型函数进行组件实例的初始化,接下来看 _init 函数。

export function initMixin (Vue: Class) {
  Vue.prototype._init = function (options?: Object) {
    // vm 就是 Vue 的实例对象,在 _init 方法中会对 vm 进行一系列的初始化操作
    const vm: Component = this
    // 赋值唯一的 id
    vm._uid = uid++

    // a flag to avoid this being observed
    // 一个标记,用于防止 vm 变成响应式的数据
    vm._isVue = true
    // 下面这个 if else 分支需要注意一下。
    // 在 Vue 中,有两个时机会创建 Vue 实例,一个是 main.js 中手动执行的 new Vue({}),还有一个是当我们
    // 在模板中使用组件时,每使用一个组件,就会创建与之相对应的 Vue 实例。也就是说 Vue 的实例有两种,一种是
    // 手动调用的 new Vue,还有一种是组件的 Vue 实例。组件的 Vue 实例会进入下面的 if 分支,而手动调用的
    // new Vue 会进入下面的 else 分支。
    //
    // 合并 options,options 用于保存当前 Vue 组件能够使用的各种资源和配置,例如:组件、指令、过滤器等等
    if (options && options._isComponent) {
      // optimize internal component instantiation
      // since dynamic options merging is pretty slow, and none of the
      // internal component options needs special treatment.
      initInternalComponent(vm, options)
    } else {
      // options 中保存的是当前组件能够使用资源和配置,这些都是当前组件私有的。
      // 但还有一些全局的资源,例如:使用 Vue.component、Vue.filter 等注册的资源,
      // 这些资源都是保存到 Vue.options 中,因为是全局的资源,所以当前的组件也要能访问到,
      // 所以在这里,将这个保存全局资源的 options 和当前组件的 options 进行合并,并保存到 vm.$options
      vm.$options = mergeOptions(
        // resolveConstructorOptions 函数的返回值是 Vue 的 options
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }
    /* istanbul ignore else */
    if (process.env.NODE_ENV !== 'production') {
      initProxy(vm)
    } else {
      vm._renderProxy = vm
    }
    // expose real self
    vm._self = vm
    // 初始化与生命周期有关的内容
    initLifecycle(vm)
    // 初始化与事件有关的属性以及处理父组件绑定到当前组件的方法
    initEvents(vm)
    // 初始化与插槽和渲染有关的内容
    initRender(vm)
    // 在 beforeCreate 回调函数中,访问不到实例中的数据,因为这些数据还没有初始化
    // 执行 beforeCreate 生命周期函数
    callHook(vm, 'beforeCreate')
    // 解析初始化当前组件的 inject
    initInjections(vm) // resolve injections before data/props
    // 初始化 state,包括 props、methods、data、computed、watch
    initState(vm)
    // 初始化 provide
    initProvide(vm) // resolve provide after data/props
    // 在 created 回调函数中,可以访问到实例中的数据
    // 执行 created 回调函数
    callHook(vm, 'created')
    // beforeCreate 和 created 生命周期的区别是:能否访问到实例中的变量

    // 如果配置中有 el 的话,则自动执行挂载操作
    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }
}

源码的具体解释看注释即可,我写的很详细,_init 函数的主要作用是对 vm 组件实例进行一系列的初始化操作,我们以 data 的初始化为例。

// 初始化 state,包括 props、methods、data、computed、watch
initState(vm)
export function initState (vm: Component) {
  const opts = vm.$options
  ......
  if (opts.data) {
    initData(vm)
  }
  ......
}
// 初始化我们配置中写的 data 对象,传递的参数(vm)是当前 Vue 的实例
function initData (vm: Component) {
  // 取出配置对象中的 data 字段
  let data = vm.$options.data
  // data 字段有两种写法:(1)函数类型;(2)普通对象类型
  // 在这一步,还会将 data 赋值给 vm._data
  data = vm._data = typeof data === 'function'
    // 如果 data 是函数类型的话,借助 getData 函数拿到最终的 data 对象
    ? getData(data, vm)
    // 否则的话,直接返回 data 对象,如果没有配置 data 的话,就返回后面的 {}
    : data || {}
  // 如果 data 不是普通的对象({k:v})的话
  if (!isPlainObject(data)) {
    // 将 data 设为 {}
    data = {}
    // 如果实在开发环境的话,打印出警告
    process.env.NODE_ENV !== 'production' && warn(
      'data functions should return an object:\n' +
      'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
      vm
    )
  }
  // proxy data on instance
  // 拿到 data 对象中的 key
  const keys = Object.keys(data)
  // 拿到我们定义的 props 和 methods
  const props = vm.$options.props
  const methods = vm.$options.methods
  // 获取 data 中 key 的个数
  let i = keys.length
  // 遍历 data 中的 key
  while (i--) {
    // 拿到当前的key
    const key = keys[i]
    if (process.env.NODE_ENV !== 'production') {
      if (methods && hasOwn(methods, key)) {
        // 如果是在开发模式下,并且我们自定义的 methods 中有和 key 同名的方法时,在这发出警告
        warn(
          `Method "${key}" has already been defined as a data property.`,
          vm
        )
      }
    }
    if (props && hasOwn(props, key)) {
      // 如果是在开发模式下,并且 props 有和 key 同名的属性时,在此发出警告
      process.env.NODE_ENV !== 'production' && warn(
        `The data property "${key}" is already declared as a prop. ` +
        `Use prop default value instead.`,
        vm
      )
      // isReserved 函数用于检查字符串是不是 $ 或者 _ 开头的
      // 如果不是 $ 和 _ 开头的话
    } else if (!isReserved(key)) {
      // 将 vm.key 代理到 vm['_data'].key
      // 也就是说当我们访问 this.message 的时候,实际上值是从 this['_data'].message 中获取到的(假设 data 中有 message 属性)
      proxy(vm, `_data`, key)
    }
  }
  // observe data
  observe(data, true /* asRootData */)
}

initData 主要做了三件事:

  1. 判断 data 是不是函数,如果是的话则执行函数获取到数据对象,获取到的数据对象被赋值到 data 和 vm._data 上。
  2. 将 vm.key 代理到 vm['_data'].key。data 数据实际上是保存在 vm._data 属性上的,但我们在开发中,可以直接通过 this.propertyKey 获取到数据,这是因为 Vue 在这里做了一层代理。
  3. 调用 observe(data) 将 data 数据转换成响应式数据,这部分可以看我的这篇博客。

4,组件实例的挂载

通过 new Vue() 我们创建出了一个组件实例,如果我们在组件配置对象中声明了 el 属性的话,Vue 会自动的帮我们进行组件实例的挂载,源码如下所示:

export function initMixin (Vue: Class) {
  Vue.prototype._init = function (options?: Object) {
    // vm 就是 Vue 的实例对象,在 _init 方法中会对 vm 进行一系列的初始化操作
    const vm: Component = this

    ......

    // 如果配置中有 el 的话,则自动执行挂载操作
    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }
}

如果我们没有声明 el 属性的话,则需要我们手动的执行 $mount() 函数进行挂载。

let app = new Vue(componentOption)
// 调用 $mount 函数进行页面挂载操作
app.$mount('#app')

接下来一起看看 $mount 方法,这个函数是一个原型方法,源码如下所示:

// 运行时版本代码使用的 $mount 函数。调用这个 $mount 函数,模板字符串必须已经编译成 render 函数
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}

$mount 函数的内部通过 mountComponent 函数进行组件的挂载,mountComponent 的源码如下所示:

export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  // 将 el 设值到 vm 中的 $el
  vm.$el = el

  // 触发执行 beforeMount 生命周期函数(挂载之前)
  callHook(vm, 'beforeMount')

  // 一个更新渲染组件的方法
  let updateComponent = () => {
    // vm._render() 函数的执行结果是一个 VNode
    // vm._update() 函数执行虚拟 DOM 的 patch 方法来执行节点的比对与渲染操作
    vm._update(vm._render(), hydrating)
  }
 
  // 这里的 Watcher 实例是一个渲染 Watcher,组件级别的
  vm._watcher = new Watcher(vm, updateComponent, noop)
  hydrating = false

  // manually mounted instance, call mounted on self
  // mounted is called for render-created child components in its inserted hook
  if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')
  }
  return vm
}

这里首先声明了一个 updateComponent 函数,这个函数的功能是:执行组件实例的 render 函数获取到最新的 vnode,然后以最新的 vnode 为参数执行 update 函数进行组件真实 DOM 的挂载或更新渲染。

然后以 updateComponent 函数为参数实例化一个渲染 Watcher,在实例化渲染 Watcher 的过程中,底层会执行 updateComponent 函数以及进行依赖的收集,这部分内容可以看我的这篇博客。

你可能感兴趣的:(vue杂谈,vue.js,前端,javascript)