这篇博客的内容是讲讲在 Vue2 中,组件在底层的本质。
在这里,直接抛出结论:组件的本质就是一个个的构造函数,这些函数以组件的定义 options 对象为参数,在 Vue2 中,最顶级的组件就是我们从 vue.js 中导入的 Vue 函数,而子组件是 Vue 底层通过 extend 函数创建出来的 VueComponent 函数。通过 new 这些组件的构造函数,我们可以创建出组件实例。
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 静态属性中。
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) {
......
}
}
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) {
......
}
}
在底层,子组件是借助 extend 方法创建出来的,这部分源码可以看我的这篇博客。
组件(构造函数)只有一个,但是我们可以在模板字符串中多次使用某一个组件,每使用一次组件,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 主要做了三件事:
通过 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 函数以及进行依赖的收集,这部分内容可以看我的这篇博客。