Vue 实例化过程

9/17/2021 Vue


# 实例化场景

这里说的实例化,并非单单指 Vue 根实例的实例化,其中也包括组件的实例化过程。

总的来说,其实可以分为两种场景,两种场景下的实例化过程存在着差异,这两种场景大体如下:

# 手动调用构造器

这种场景下,其实又可以分两种情况:

  • new Vue()

这是最常用的一种实例化方式,就是调用 Vue 构造器创建根 Vue 实例的过程

import App from './app'

new Vue({
  data: {
    name: 'main.js',
  },
  created() {
    console.log('this is main.js')
  },
  // 渲染函数
  render(h) {
    return h(App)
  },
}).$mount('#app')
1
2
3
4
5
6
7
8
9
10
11
12
13
14
  • Vue.extend

这种方式其实跟调用 Vue 构造器的方式差不多,不同之处在于构造器不同,通过 Vue.extend 方法创建的构造器继承自 Vue 构造器。

let Message = Vue.extend({
  name: 'Message',
  template: '<p>{{msg}}</p>',
  data() {
    return {
      type: 'success',
      msg: 'this is Message',
    }
  },
  created() {
    console.log(this.type + ':' + this.msg)
  },
})

new Message().$mount('#message')
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 自动调用构造器创建

这其实是 vue 组件的构造场景,其实也可以分为两种情况

# 使用 Vue.component 的方式注册的全局组件

Vue.component('Message', {
  data() {
    return {
      msg: 'this is message',
    }
  },
})
1
2
3
4
5
6
7

其实 Vue.component 方法的本质也是在内部调用了 Vue.extend 方法









 
 







Vue['component'] = function (id, definition) {
  // 只传递组件名称,则是查找组件
  if (!definition) {
    return this.options['components'][id]
  } else {
    validateComponentName(id)
    if (isPlainObject(definition)) {
      definition.name = definition.name || id
      // 其实就是  definition = Vue.extend(definition)
      definition = this.options._base.extend(definition)
    }
    // 全局注册的组件,其定义是一个 组件构造器 函数
    this.options['components'][id] = definition
    return definition
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

值得注意的是,使用 Vue.component()方法注册的全局组件,其定义是一个组件构造器函数,而下面要说的,在组件内部通过 components 选项注册的组件则是一个组件对象

# 在组件内部通过 components 选项注册的局部组件

export default {
  name: 'MessageBox',
  components: {
    Message,
  },
  render(h) {
    return h(Message)
  },
}
1
2
3
4
5
6
7
8
9

其实这种方式的本质,同样是需要调用 Vue.extend 方法来创建组件的构造器

var baseCtor = context.$options._base

// plain options object: turn it into a constructor
if (isObject(Ctor)) {
  Ctor = baseCtor.extend(Ctor)
}
1
2
3
4
5
6
  • 这里的 baseCtor 其实就是 Vue
  • Ctor 则是当前组件对象

关于组件的实例化过程,后面在组件相关的部分再说,这里先记录下 Vue 根实例的构造过程。

# 示例

首先从一个示例开始

import App from './app'

new Vue({
  el: '#app',
  data: {
    name: 'main.js',
  },
  created() {
    console.log('this is main.js')
  },
  // 渲染函数
  render(h) {
    return h(App)
  },
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# Vue

// Vue 构造器
function Vue(options) {
  this._init(options)
}
1
2
3
4

当调用 new Vue(options) 的时候,构造器内部就会调用 Vue.prototype._init 方法,并将传入的组件选项对象 options 传入进去,也就是这里的

{
  el: '#app',
  data: {
    name: 'main.js',
  },
  // 渲染函数
  render(h) {
    return h(App)
  },
}
1
2
3
4
5
6
7
8
9
10

更多的选项配置参见选项列表 (opens new window)

# Vue.prototype._init

Vue.prototype._init = function (options?: Object) {
  const vm: Component = this

  // 每一个组件实例都会有一个唯一的uid
  vm._uid = uid++

  // 防止被转换成 响应式对象 的一个标记
  vm._isVue = true

  if (options && options._isComponent) {
    initInternalComponent(vm, options)
  } else {
    // 配置合并:将子组件上的配置与根组件上的配置合并到一起,挂在 当前组件实例的 $options 上
    vm.$options = mergeOptions(
      resolveConstructorOptions(vm.constructor),
      options || {},
      vm
    )
  }

  vm._renderProxy = vm
  // expose real self
  vm._self = vm
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# 配置合并

首先这里有一个判断,就是当 options._isComponent 这个属性存在的时候,就会走到 initInternalComponent 这个方法中去, 不存在的话,就会走到 else 逻辑,进行配置合并操作。

_isComponent 这个属性是一个内部属性,也就是在 Vue 内部使用的。其实在 Vue 中有这样一个规范,

  1. $ 开头的属性,是提供给用户使用的
  2. _ 开头的属性,则是在 Vue 内部使用的

那么这个属性,

  • 是什么时候被添加到 options 上的呢?
  • 这个属性的作用又是什么呢?

这里可以先说结论,这部分的处理流程其实与上面提到的自动调用构造器也就是组件的构造场景相关,后面再细说

  • 这个属性是在源码 core/vdom/create-component.js 这个文件中的 createComponentInstanceForVnode 这个方法中添加的

这个方法会在组件 __patch__ 过程中被调用

export function createComponentInstanceForVnode(vnode, parent): Component {
  // 就是这里
  const options = {
    _isComponent: true,
    _parentVnode: vnode,
    parent,
  }

  // ...省略

  return new vnode.componentOptions.Ctor(options)
}
1
2
3
4
5
6
7
8
9
10
11
12

BTW这里的vnode.componentOptions.Ctor 其实就是通过 Vue.extend 方法继承来的子组件构造器

const Sub = function VueComponent(options) {
  this._init(options)
}
// 寄生式组合继承方式
Sub.prototype = Object.create(Super.prototype)
Sub.prototype.constructor = Sub
Sub.cid = cid++
Sub.options = mergeOptions(Super.options, extendOptions)
Sub['super'] = Super
1
2
3
4
5
6
7
8
9
  • 这个属性的作用其实就是用来标记当前的 vue 实例,是一个 组件实例,即通过 VueComponent 构造的实例,而不是根 vue 实例,即通过 Vue 构造的实例

# 初始化

再往后面会进行一系列初始化,并在合适的时机调用 beforeCreate & created 钩子函数 最后,通过判断 options.el 选项是否传入,来决定下一步是不是要进行组件的挂载操作,也就是 vm.$mount

Vue.prototype._init = function (options?: Object) {
  // ...

  // 生命周期初始化
  initLifecycle(vm)
  // 事件中心初始化
  initEvents(vm)
  // 初始化渲染器
  initRender(vm)
  // 调用 beforeCreate 钩子函数
  callHook(vm, 'beforeCreate')
  // 初始化inject
  initInjections(vm)
  // 初始化props/methods/data/computed/watcher
  initState(vm)
  // 初始化provide
  initProvide(vm)
  // 调用 created 钩子函数
  callHook(vm, 'created')

  // 如果提供了 el 选项,就直接进行挂载操作
  if (vm.$options.el) {
    // 组件挂载
    vm.$mount(vm.$options.el)
  }
}
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
  • 初始化生命周期
  • 初始化事件中心
  • 初始化渲染函数
  • 调用 beforeCreate 钩子
  • 初始化 inject
  • 初始化 state(包括 props/methods/data/computed/watch) 等等
  • 初始化 provide
  • 调用 created 钩子

这里有一个需要注意的地方,就是在 beforeCreate 钩子里面是没有办法获取到 state 的,原因就是 state 根本还没有开始初始化

# 生命周期初始化

这里涉及到一个抽象组件的概念,像内置的 keep-alive 就是一个抽象组件,抽象组件本身不渲染任何 DOM。

可以看到这里在构建父子组件关系的时候,是不会将抽象组件添加到这种关系中去的

function initLifecycle(vm: Component) {
  const options = vm.$options

  // 找到第一个非抽象组件
  let parent = options.parent
  if (parent && !options.abstract) {
    while (parent.$options.abstract && parent.$parent) {
      parent = parent.$parent
    }
    // 找到第一个非抽象的父组件 并将自身添加到父组件的 $children 中
    parent.$children.push(vm)
  }
  // 子组件会把父实例挂载到自身的 $parent 属性上
  // 根 vue 实例的 $parent 属性为 undefined
  vm.$parent = parent
  // 根 vue 实例的 $root 属性为 实例自身
  vm.$root = parent ? parent.$root : vm

  // 初始化实例上的一些属性
  vm.$children = []
  vm.$refs = {}

  // 初始化内部属性
  vm._watcher = null
  vm._inactive = null
  vm._directInactive = false
  vm._isMounted = false
  vm._isDestroyed = false
  vm._isBeingDestroyed = false
}
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

# 事件中心初始化

这里没什么好说的,

  • 初始化了一个 _events 属性,值是一个空对象,用来保存组件中定义的事件处理函数
  • 初始化了一个 _hasHookEvent 属性,用来标记组件是否包含 生命周期有关的事件钩子定义 ·
function initEvents(vm: Component) {
  // 初始化内部 _events 属性
  vm._events = Object.create(null)
  vm._hasHookEvent = false
  // 下述流程不涉及
  // init parent attached events
  const listeners = vm.$options._parentListeners
  if (listeners) {
    updateComponentListeners(vm, listeners)
  }
}
1
2
3
4
5
6
7
8
9
10
11

# 初始化渲染函数

  • vm._c 这个方法用于将模板编译成 render 函数的时候使用
  • vm.$createElement 也就是 h 函数,这个是用户手写 render 的时候调用

二者的区别仅在最后一个参数 alwaysNormalize,即是否总是需要对传入的第三个参数,即 children VNode 进行规范化处理。

其中,通过模板编译成的 render 函数,这个参数的值是 false,而用户手写 render 函数的情况,就是 true。

function initRender(vm) {
  vm._vnode = null // the root of the child tree
  vm._staticTrees = null // v-once cached trees
  var options = vm.$options
  var parentVnode = (vm.$vnode = options._parentVnode) // the placeholder node in parent tree
  var renderContext = parentVnode && parentVnode.context
  vm.$slots = resolveSlots(options._renderChildren, renderContext)
  vm.$scopedSlots = emptyObject
  // 参数顺序: tag, data, children, normalizationType, alwaysNormalize
  // 供内部组件使用
  vm._c = function (a, b, c, d) {
    return createElement(vm, a, b, c, d, false)
  }
  // normalization is always applied for the public version, used in
  // 给用户写的render方法使用
  vm.$createElement = function (a, b, c, d) {
    return createElement(vm, a, b, c, d, true)
  }

  // $attrs & $listeners are exposed for easier HOC creation.
  // they need to be reactive so that HOCs using them are always updated
  var parentData = parentVnode && parentVnode.data

  // 添加 $attrs & $listeners 响应式对象属性
  defineReactive(
    vm,
    '$attrs',
    (parentData && parentData.attrs) || emptyObject,
    null,
    true
  )
  defineReactive(
    vm,
    '$listeners',
    options._parentListeners || emptyObject,
    null,
    true
  )
}
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

# 调用 beforeCreate 钩子

这里有两种情况,根据定义的方式不同,触发的方式也有所不同

  • 一个是在选项对象中定义的 beforeCreate 钩子
  • 一个是通过 @hook:beforeCreate 的方式注册的事件处理钩子
function callHook(vm, hook) {
  // #7573 disable dep collection when invoking lifecycle hooks
  pushTarget()
  // 从 $options 上获取到 beforeCreate 钩子的定义
  var handlers = vm.$options[hook]
  var info = hook + ' hook'
  // 如果组件中有定义这个钩子,就会被调用
  if (handlers) {
    for (var i = 0, j = handlers.length; i < j; i++) {
      invokeWithErrorHandling(handlers[i], vm, null, vm, info)
    }
  }
  // 这里就是第二种定义的触发方式
  if (vm._hasHookEvent) {
    vm.$emit('hook:' + hook)
  }
  popTarget()
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 初始化 inject

inject 跟 provide 是配对一起使用的,文档中是这样说的:

这对选项需要一起使用,以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在上下游关系成立的时间里始终生效

而且还有一点要注意:

provide 和 inject 绑定并不是响应式的。如果传入了一个可监听的对象,那么其对象的属性还是可响应的

function initInjections(vm) {
  var result = resolveInject(vm.$options.inject, vm)
  if (result) {
    // 控制 defineReactive 方法是否将属性定义为响应式的,false 为非响应式
    toggleObserving(false)
    Object.keys(result).forEach(function (key) {
      defineReactive(vm, key, result[key])
    })
    toggleObserving(true)
  }
}

function resolveInject(inject, vm) {
  if (inject) {
    var result = Object.create(null)
    var keys = hasSymbol ? Reflect.ownKeys(inject) : Object.keys(inject)

    for (var i = 0; i < keys.length; i++) {
      var key = keys[i]
      // 已经是响应式的 则跳过
      if (key === '__ob__') {
        continue
      }
      var provideKey = inject[key].from
      var source = vm
      while (source) {
        // 获取定义在 vm 上的 _provided 属性的值
        if (source._provided && hasOwn(source._provided, provideKey)) {
          result[key] = source._provided[provideKey]
          break
        }
        // 这里保证了 祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深
        source = source.$parent
      }
      if (!source) {
        if ('default' in inject[key]) {
          var provideDefault = inject[key].default
          result[key] =
            typeof provideDefault === 'function'
              ? provideDefault.call(vm)
              : provideDefault
        } else if (process.env.NODE_ENV !== 'production') {
          warn('Injection "' + key + '" not found', vm)
        }
      }
    }
    return result
  }
}
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

# 初始化 state

这里需要关注一下,state 的处理顺序:

  • props -> methods -> data -> computed -> watch
export function initState(vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)
  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) {
    initData(vm)
  } else {
    observe((vm._data = {}), true /* asRootData */)
  }
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

以 initData 为例,

  • data 选项必须是对象或返回对象的函数
  • data 中定义的 key 如果与 methods 以及 props 中的 key 重复,开发环境下会报警告
function initData(vm) {
  var data = vm.$options.data
  data = vm._data = typeof data === 'function' ? getData(data, vm) : data || {}
  // 如果data 返回的不是一个对象,开发环境下警告
  if (!isPlainObject(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
  var keys = Object.keys(data)
  var props = vm.$options.props
  var methods = vm.$options.methods
  var i = keys.length
  while (i--) {
    var key = keys[i]
    // 如果data中的key 和 methods中的key重复,开发环境报警告
    if (process.env.NODE_ENV !== 'production') {
      if (methods && hasOwn(methods, key)) {
        warn(
          'Method "' + key + '" has already been defined as a data property.',
          vm
        )
      }
    }
    // 如果data中的key 和 props 中的key重复,开发环境报警告
    if (props && hasOwn(props, key)) {
      process.env.NODE_ENV !== 'production' &&
        warn(
          'The data property "' +
            key +
            '" is already declared as a prop. ' +
            'Use prop default value instead.',
          vm
        )
    } else if (!isReserved(key)) {
      // 将 vm[key] 代理到 vm._data[key] 上来
      proxy(vm, '_data', key)
    }
  }
  // 将data转换为 响应式对象
  observe(data, true /* asRootData */)
}
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
  • 为什么可以通过 this[key] 这种方式访问定义在 data 中的属性,原理就是通过 proxy 代理过去的
// proxy
var sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop,
}

function proxy(target, sourceKey, key) {
  sharedPropertyDefinition.get = function proxyGetter() {
    return this[sourceKey][key]
  }
  sharedPropertyDefinition.set = function proxySetter(val) {
    this[sourceKey][key] = val
  }
  Object.defineProperty(target, key, sharedPropertyDefinition)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 初始化 provide

通过获取定义在 $options 上的 provide 的值,然后定义到 vm 的 _provided 属性上,这个值的使用是在上面提到的 初始化 inject 部分

function initProvide(vm) {
  var provide = vm.$options.provide
  if (provide) {
    vm._provided = typeof provide === 'function' ? provide.call(vm) : provide
  }
}
1
2
3
4
5
6

思考个问题: 为什么是先处理的 inject 然后才是 provide,这样的话在初始化 reject 的时候,vm._provided 都还没有值啊?

其实看 inject 初始化的时候,它是从当前组件逐层向上不断查找他的父组件的 provide 的一个过程,而在 vue 组件的创建过程中,都是父组件先完成 created 这个生命周期,所以这就保证了父组件的 provide 已经被求值了。

# 调用 created 钩子

最后就是在完成一些列配置合并以及初始化操作后,调用 created 生命周期钩子函数,完成初始化过程,钩子的调用和 beforeCreate 钩子类似,不再赘述。

还有一点要注意的就是,在 created 这个钩子函数里,现在已经可以获取到定义在组件对象的 data/props/methods 等等这些数据了。

# $mount

最后就是挂载操作了,可以说挂载操作是整个实例化过程中最复杂的了,其中涉及到有关组件 VNode 的创建以及组件的 patch 过程,这个后面会分开来说。

下一篇先说一下

  • 配置合并的过程
  • 针对不同的选项配置合并的策略都有哪些
Last Updated: 10/21/2024, 4:15:17 PM