当前位置:   article > 正文

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

vue实例化组件

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

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

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

1-1,Vue 的静态属性

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

  1. // 向 Vue 上挂载一些静态的属性和方法
  2. initGlobalAPI(Vue)
  1. export function initGlobalAPI (Vue: GlobalAPI) {
  2. // 应用于 Vue 源码内部的工具函数,不建议程序员直接使用。
  3. Vue.util = {
  4. warn,
  5. extend,
  6. mergeOptions,
  7. defineReactive
  8. }
  9. // 定义 options 对象,该对象用于存储一系列的资源,如:组件、指令和过滤器
  10. Vue.options = Object.create(null)
  11. ASSET_TYPES.forEach(type => {
  12. Vue.options[type + 's'] = Object.create(null)
  13. })
  14. // 将与平台无关的内建组件存储到 options.components 中
  15. extend(Vue.options.components, builtInComponents)
  16. }

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

1-2,Vue 的静态方法

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

  1. // 向 Vue 上挂载一些静态的属性和方法
  2. initGlobalAPI(Vue)
  1. export function initGlobalAPI (Vue: GlobalAPI) {
  2. // 定义全局 API。set、delete、nextTick
  3. Vue.set = set
  4. Vue.delete = del
  5. Vue.nextTick = nextTick
  6. // 初始化 Vue.use()
  7. initUse(Vue)
  8. // 初始化 Vue.mixin()
  9. initMixin(Vue)
  10. // 初始化 Vue.extend()
  11. initExtend(Vue)
  12. // 初始化 Vue.component()、Vue.directive()、Vue.filter(),用于向 Vue 中注册资源
  13. initAssetRegisters(Vue)
  14. }
  15. export function initUse (Vue: GlobalAPI) {
  16. Vue.use = function (plugin: Function | Object) {
  17. ......
  18. }
  19. }

1-3,Vue 的原型方法

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

  1. function Vue (options) {
  2. // 如果当前的环境不是生产环境,并且当前命名空间中的 this 不是 Vue 的实例的话,
  3. // 发出警告,Vue 必须通过 new Vue({}) 使用,而不是把 Vue 当做函数使用
  4. if (process.env.NODE_ENV !== 'production' &&
  5. !(this instanceof Vue)
  6. ) {
  7. warn('Vue is a constructor and should be called with the `new` keyword')
  8. }
  9. // 执行 vm 原型上的 _init 方法,该方法在 initMixin 方法中定义
  10. this._init(options)
  11. }
  12. // 下面函数的作用是:往 Vue 的原型上写入原型函数,这些函数是给 Vue 的实例使用的
  13. // 这些函数分为两类:一类是 Vue 内部使用的,特征是函数名以 '_' 开头;
  14. // 还有一类是给用户使用的,特征是函数名以 '$' 开头,这些函数可以在 Vue 的官方文档中看到;
  15. // 写入 vm._init
  16. initMixin(Vue)
  17. // 写入 vm.$set、vm.$delete、vm.$watch
  18. stateMixin(Vue)
  19. // 写入 vm.$on、vm.$once、vm.$off、vm.$emit
  20. eventsMixin(Vue)
  21. // 写入 vm._update、vm.$forceUpdate、vm.$destroy
  22. lifecycleMixin(Vue)
  23. // 写入 vm.$nextTick、vm._render
  24. renderMixin(Vue)
  25. export default Vue

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

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

  1. export function initMixin (Vue: Class<Component>) {
  2. Vue.prototype._init = function (options?: Object) {
  3. ......
  4. }
  5. }

 

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

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

3,组件的实例化

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

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

  1. <script>
  2. // 声明一个组件
  3. let componentOption = {
  4. data: {
  5. name: 'tom',
  6. age: 27
  7. },
  8. mounted(){
  9. setTimeout(() => {
  10. this.updateAge()
  11. }, 2000)
  12. },
  13. methods: {
  14. updateAge(){
  15. this.age++
  16. }
  17. },
  18. template: `
  19. <div id="app">
  20. {{name}} {{age}}
  21. </div>
  22. `,
  23. render() {
  24. // 这里的 this 是指 Vue 组件实例
  25. with (this) {
  26. return _c(
  27. 'div',
  28. {attrs: {"id": "app"}},
  29. [_v(_s(name) + " " + _s(age))]
  30. )
  31. }
  32. }
  33. }
  34. // 创建一个组件实例
  35. let app = new Vue(componentOption)
  36. // 调用 $mount 函数进行页面挂载操作
  37. app.$mount('#app')
  38. </script>

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

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

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

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

  1. export function initMixin (Vue: Class<Component>) {
  2. Vue.prototype._init = function (options?: Object) {
  3. // vm 就是 Vue 的实例对象,在 _init 方法中会对 vm 进行一系列的初始化操作
  4. const vm: Component = this
  5. // 赋值唯一的 id
  6. vm._uid = uid++
  7. // a flag to avoid this being observed
  8. // 一个标记,用于防止 vm 变成响应式的数据
  9. vm._isVue = true
  10. // 下面这个 if else 分支需要注意一下。
  11. // 在 Vue 中,有两个时机会创建 Vue 实例,一个是 main.js 中手动执行的 new Vue({}),还有一个是当我们
  12. // 在模板中使用组件时,每使用一个组件,就会创建与之相对应的 Vue 实例。也就是说 Vue 的实例有两种,一种是
  13. // 手动调用的 new Vue,还有一种是组件的 Vue 实例。组件的 Vue 实例会进入下面的 if 分支,而手动调用的
  14. // new Vue 会进入下面的 else 分支。
  15. //
  16. // 合并 options,options 用于保存当前 Vue 组件能够使用的各种资源和配置,例如:组件、指令、过滤器等等
  17. if (options && options._isComponent) {
  18. // optimize internal component instantiation
  19. // since dynamic options merging is pretty slow, and none of the
  20. // internal component options needs special treatment.
  21. initInternalComponent(vm, options)
  22. } else {
  23. // options 中保存的是当前组件能够使用资源和配置,这些都是当前组件私有的。
  24. // 但还有一些全局的资源,例如:使用 Vue.component、Vue.filter 等注册的资源,
  25. // 这些资源都是保存到 Vue.options 中,因为是全局的资源,所以当前的组件也要能访问到,
  26. // 所以在这里,将这个保存全局资源的 options 和当前组件的 options 进行合并,并保存到 vm.$options
  27. vm.$options = mergeOptions(
  28. // resolveConstructorOptions 函数的返回值是 Vue 的 options
  29. resolveConstructorOptions(vm.constructor),
  30. options || {},
  31. vm
  32. )
  33. }
  34. /* istanbul ignore else */
  35. if (process.env.NODE_ENV !== 'production') {
  36. initProxy(vm)
  37. } else {
  38. vm._renderProxy = vm
  39. }
  40. // expose real self
  41. vm._self = vm
  42. // 初始化与生命周期有关的内容
  43. initLifecycle(vm)
  44. // 初始化与事件有关的属性以及处理父组件绑定到当前组件的方法
  45. initEvents(vm)
  46. // 初始化与插槽和渲染有关的内容
  47. initRender(vm)
  48. // 在 beforeCreate 回调函数中,访问不到实例中的数据,因为这些数据还没有初始化
  49. // 执行 beforeCreate 生命周期函数
  50. callHook(vm, 'beforeCreate')
  51. // 解析初始化当前组件的 inject
  52. initInjections(vm) // resolve injections before data/props
  53. // 初始化 state,包括 props、methods、data、computed、watch
  54. initState(vm)
  55. // 初始化 provide
  56. initProvide(vm) // resolve provide after data/props
  57. // 在 created 回调函数中,可以访问到实例中的数据
  58. // 执行 created 回调函数
  59. callHook(vm, 'created')
  60. // beforeCreate 和 created 生命周期的区别是:能否访问到实例中的变量
  61. // 如果配置中有 el 的话,则自动执行挂载操作
  62. if (vm.$options.el) {
  63. vm.$mount(vm.$options.el)
  64. }
  65. }
  66. }

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

  1. // 初始化 state,包括 props、methods、data、computed、watch
  2. initState(vm)
  1. export function initState (vm: Component) {
  2. const opts = vm.$options
  3. ......
  4. if (opts.data) {
  5. initData(vm)
  6. }
  7. ......
  8. }
  1. // 初始化我们配置中写的 data 对象,传递的参数(vm)是当前 Vue 的实例
  2. function initData (vm: Component) {
  3. // 取出配置对象中的 data 字段
  4. let data = vm.$options.data
  5. // data 字段有两种写法:(1)函数类型;(2)普通对象类型
  6. // 在这一步,还会将 data 赋值给 vm._data
  7. data = vm._data = typeof data === 'function'
  8. // 如果 data 是函数类型的话,借助 getData 函数拿到最终的 data 对象
  9. ? getData(data, vm)
  10. // 否则的话,直接返回 data 对象,如果没有配置 data 的话,就返回后面的 {}
  11. : data || {}
  12. // 如果 data 不是普通的对象({k:v})的话
  13. if (!isPlainObject(data)) {
  14. // 将 data 设为 {}
  15. data = {}
  16. // 如果实在开发环境的话,打印出警告
  17. process.env.NODE_ENV !== 'production' && warn(
  18. 'data functions should return an object:\n' +
  19. 'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
  20. vm
  21. )
  22. }
  23. // proxy data on instance
  24. // 拿到 data 对象中的 key
  25. const keys = Object.keys(data)
  26. // 拿到我们定义的 props 和 methods
  27. const props = vm.$options.props
  28. const methods = vm.$options.methods
  29. // 获取 data 中 key 的个数
  30. let i = keys.length
  31. // 遍历 data 中的 key
  32. while (i--) {
  33. // 拿到当前的key
  34. const key = keys[i]
  35. if (process.env.NODE_ENV !== 'production') {
  36. if (methods && hasOwn(methods, key)) {
  37. // 如果是在开发模式下,并且我们自定义的 methods 中有和 key 同名的方法时,在这发出警告
  38. warn(
  39. `Method "${key}" has already been defined as a data property.`,
  40. vm
  41. )
  42. }
  43. }
  44. if (props && hasOwn(props, key)) {
  45. // 如果是在开发模式下,并且 props 有和 key 同名的属性时,在此发出警告
  46. process.env.NODE_ENV !== 'production' && warn(
  47. `The data property "${key}" is already declared as a prop. ` +
  48. `Use prop default value instead.`,
  49. vm
  50. )
  51. // isReserved 函数用于检查字符串是不是 $ 或者 _ 开头的
  52. // 如果不是 $ 和 _ 开头的话
  53. } else if (!isReserved(key)) {
  54. // 将 vm.key 代理到 vm['_data'].key
  55. // 也就是说当我们访问 this.message 的时候,实际上值是从 this['_data'].message 中获取到的(假设 data 中有 message 属性)
  56. proxy(vm, `_data`, key)
  57. }
  58. }
  59. // observe data
  60. observe(data, true /* asRootData */)
  61. }

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 会自动的帮我们进行组件实例的挂载,源码如下所示:

  1. export function initMixin (Vue: Class<Component>) {
  2. Vue.prototype._init = function (options?: Object) {
  3. // vm 就是 Vue 的实例对象,在 _init 方法中会对 vm 进行一系列的初始化操作
  4. const vm: Component = this
  5. ......
  6. // 如果配置中有 el 的话,则自动执行挂载操作
  7. if (vm.$options.el) {
  8. vm.$mount(vm.$options.el)
  9. }
  10. }
  11. }

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

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

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

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

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

  1. export function mountComponent (
  2. vm: Component,
  3. el: ?Element,
  4. hydrating?: boolean
  5. ): Component {
  6. // 将 el 设值到 vm 中的 $el
  7. vm.$el = el
  8. // 触发执行 beforeMount 生命周期函数(挂载之前)
  9. callHook(vm, 'beforeMount')
  10. // 一个更新渲染组件的方法
  11. let updateComponent = () => {
  12. // vm._render() 函数的执行结果是一个 VNode
  13. // vm._update() 函数执行虚拟 DOM 的 patch 方法来执行节点的比对与渲染操作
  14. vm._update(vm._render(), hydrating)
  15. }
  16. // 这里的 Watcher 实例是一个渲染 Watcher,组件级别的
  17. vm._watcher = new Watcher(vm, updateComponent, noop)
  18. hydrating = false
  19. // manually mounted instance, call mounted on self
  20. // mounted is called for render-created child components in its inserted hook
  21. if (vm.$vnode == null) {
  22. vm._isMounted = true
  23. callHook(vm, 'mounted')
  24. }
  25. return vm
  26. }

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

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

声明:本文内容由网友自发贡献,转载请注明出处:【wpsshop博客】
推荐阅读
  

闽ICP备14008679号