我的开源库:
- fly-barrage 前端弹幕库,项目官网:https://fly-barrage.netlify.app/,可实现类似于 B 站的弹幕效果,并提供了完整的 DEMO,Gitee 推荐项目;
- fly-gesture-unlock 手势解锁库,项目官网:https://fly-gesture-unlock.netlify.app/,在线体验:https://fly-gesture-unlock-online.netlify.app/,可高度自定义锚点的数量、样式以及尺寸;
在讲正文之前,最好先了解一下 new 操作符的运行机制。
在 Vue源码阅读之路(4) 中,我们知道,Vue 函数定义在 src/core/instance/index.js 文件中,先看一下 Vue 函数的定义。
1,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 函数,正确的使用方式是使用 new 关键词进行调用。如果没有使用 new 调用并且是非生产环境的话,则打印出相应的警告。
然后调用 Vue 函数实例的 _init 方法,该方法定义在 Vue.prototype 中。我们可以看到 Vue 函数定义的下面,以 Vue 为参数执行了一系列的函数,这些函数的作用是向 Vue.prototype 写入原型方法,这些方法是给 Vue 函数的实例使用的。上面说的 _init 方法就被定义在 initMixin() 中。
2,src/core/instance/init.js ==> initMixin
export function initMixin (Vue: Class<Component>) {
// _init 方法会在 new Vue() 的时候调用,看下面的代码:
// function Vue (options) {
// this._init(options)
// }
Vue.prototype._init = function (options?: Object) {
// vm 就是 Vue 的实例对象,在 _init 方法中会对 vm 进行一系列的初始化操作
const vm: Component = this
// 赋值唯一的 id
vm._uid = uid++
let startTag, endTag
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
startTag = `vue-perf-start:${vm._uid}`
endTag = `vue-perf-end:${vm._uid}`
mark(startTag)
}
// a flag to avoid this being observed
// 一个标记,用于防止 vm 变成响应式的数据
vm._isVue = true
// 合并 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 生命周期的区别是:能否访问到实例中的变量
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
vm._name = formatComponentName(vm, false)
mark(endTag)
measure(`vue ${vm._name} init`, startTag, endTag)
}
// 如果配置中有 el 的话,则自动执行挂载操作
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
}
主要做了以下几件事:
- 合并配置,合并当前组件的 option 和保存在 Vue 中的全局 option。
- 通过调用 initLifecycle、initEvents、initRender、initInjections、initState 和 initProvide 进行生命周期的初始化、事件的初始化、渲染的初始化、inject 的初始化、状态的初始化和 provide 的初始化。这些在 vm 中初始化的内容会在后续的操作中使用到。
- 在适当的时机,通过调用 callHook 执行生命周期函数。
- 检查是否配置了 el 属性,如果配置了的话,就进行挂载渲染真实 DOM 的操作。
2-1,callHook 的实现
// 执行 Vue 实例(vm)中的生命周期函数
// 内容也很简单,就是从 vm.$options 中取出指定生命周期的回调函数数组
// 然后遍历执行数组中的函数
export function callHook (vm: Component, hook: string) {
// 取出回调函数数组
const handlers = vm.$options[hook]
if (handlers) {
// 遍历执行每一个函数
for (let i = 0, j = handlers.length; i < j; i++) {
try {
handlers[i].call(vm)
} catch (e) {
handleError(e, vm, `${hook} hook`)
}
}
}
if (vm._hasHookEvent) {
vm.$emit('hook:' + hook)
}
}
生命周期函数都保存在 $options 中,存储形式如下所示:
vm.$options = {
// 数组中保存生命周期函数
created: [],
mounted: [],
......
}
callHook 首先从 vm.$options 中取出对应生命周期的函数数组,然后遍历数组,执行各个生命周期函数。
也许你会有疑问?组件的某一个特定的生命周期函数不是只有一个吗?为什么还用数组进行存储?这是因为我们可以借助 Vue.mixin() 或者 mixins 配置选项混入其他的生命周期函数,所以组件的某一个生命周期的函数有可能有多个,所以需要用数组存储生命周期函数,然后遍历数组执行函数。
2-2,initState 的实现
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)
// 初始化监听属性
// nativeWatch的作用:Firefox has a "watch" function on Object.prototype...
if (opts.watch && opts.watch !== nativeWatch) {
// 进行侦听属性的初始化过程
initWatch(vm, opts.watch)
}
}
从上往下依次初始化 props、methods、data、computed、watch。至于初始化的具体细节,会在后续具体讲到这个特性的时候,再具体分析。
3,总结
这篇博客主要总结分析 new Vue 的大致流程,其中具体的操作暂不做分析。在后续的博客中,如果讲到了某个特性,再进行细致的解析。