<深入浅出Vuejs>生命周期_初始化阶段
前言
一、Vue构造函数入口
Vue
往往是从new
开始工作的,因此我们从构造函数开始探索Vue:
import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'
import { warn } from '../util/index'
function Vue (options) {
if (process.env.NODE_ENV !== 'production' &&
!(this instanceof Vue)
) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
this._init(options)
}
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)
export default Vue
构造函数逻辑十分简单,进行安全检查后(非生产环境调用Vue是否是new
出来的,不是则打印警告信息),调用this._init
(options
)来执行生命周期的初始化阶段。
在这之后有五个Mixin
方法,执行的时候传入构造函数Vue本身
,主要用于在不同的阶段给Vue原型链上添加不同的方法。
- initMixin:
- 合并配置
- 初始化组件实例关系以及内部属性:比如当前组件的
父组件(parent)、根组件(parent)、根组件(parent)、根组件(root)、子组件数组(children)容器、children)容器、children)容器、refs以及内部属性
等。 - 初始化
自定义事件
- 初始化
组件插槽信息
- 调用生命周期函数
beforeCreate
- 初始化组件的
inject
配置项 - 初始化状态。分别初始化
props、methods、data、computed、watch
(划重点!!!) - 初始化组件的
provide
配置项 - 调用生命周期函数
create
- 如果是根组件(自定义配置上有el选项)则调用
$mount
自动挂载
- stateMixin:
构造函数Vue原型链上初始化$data
、$props
属性,初始化$set
、$delete
、$watch
方法 - eventsMixin:
构造函数Vue原型链上初始化$on
、$once
、$off
、$emit
方法 - lifecycleMixin:
构造函数Vue原型链上初始化_update
、$forceUpdate
、$destroy
方法 - renderMixin:
构造函数Vue原型链上初始化$nextTick
、_render
方法
那么,_init方法在哪里定义?内部如何实现呢?
二、initMixin&_init
initMixin函数用于将_init挂载到Vue函数的原型链上,_init中执行具体的初始化逻辑
_init()
export function initMixin (Vue: Class<Component>) {
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
// a uid
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)
}
// 如果是Vue的实例,则不需要被observe
// a flag to avoid this being observed
vm._isVue = true
// merge options
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 {
// 合并配置接下来讨论,resolveConstructorOptions用于获取当前实例的options与父实例的options属性合并成新的options赋值给$options
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
// 第二步: renderProxy
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
initProxy(vm)
} else {
vm._renderProxy = vm
}
// expose real self
vm._self = vm
// 第三步: vm的生命周期相关变量初始化
initLifecycle(vm)
// 第四步: vm的事件监听初始化
initEvents(vm)
// 第五步: vm的编译render初始化
initRender(vm)
// 第六步: vm的beforeCreate生命钩子的回调,在此之前完成了initlifecycle initevents initrender
callHook(vm, 'beforeCreate')
// 第七步: vm在data/props初始化之前要进行绑定
initInjections(vm) // resolve injections before data/props
// 第八步: vm的sate状态初始化
initState(vm)
// 第九步: vm在data/props之后要进行提供
initProvide(vm) // resolve provide after data/props
// 第十步: vm的created生命钩子的回调
callHook(vm, '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)
}
/*
*如果实例化vue有el选项,则开启模板编译与挂载阶段
*如果没有el选项则不进入下一个阶段,用户通过$mount方法手动进入模板编译与挂载
*/
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
}
mergeOptions(合并配置)
配置合并就是将Vue
默认与用户自定义的options
进行合并后返回一个新的Object
,并赋值给vm的属性$options
。
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
它接受三个参数:
- resolveConstructorOption函数返回的Vue默认配置
- 用户自定义的默认配置。若无则传空对象
- 当前实例本身
让我们来看一下resolveConstructorOption函数:
//默认形况下,Ctor其实就是Vue构造函数
export function resolveConstructorOptions (Ctor: Class<Component>) {
let options = Ctor.options
// 首先需要判断该类是否是Vue的子类
if (Ctor.super) {
const superOptions = resolveConstructorOptions(Ctor.super)
const cachedSuperOptions = Ctor.superOptions
// 来判断父类中的options 有没有发生变化
if (superOptions !== cachedSuperOptions) {
// super option changed,
// need to resolve new options.
Ctor.superOptions = superOptions
// check if there are any late-modified/attached options (#4976)
const modifiedOptions = resolveModifiedOptions(Ctor)
// update base extend options
if (modifiedOptions) {
// 当为Vue混入一些options时,superOptions会发生变化,此时于之前子类中存储的cachedSuperOptions已经不相等,所以下面的操作主要就是更新sub.superOptions
extend(Ctor.extendOptions, modifiedOptions)
}
options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions)
if (options.name) {
options.components[options.name] = Ctor
}
}
}
return options
}
initLifecycle
export function initLifecycle (vm: Component) {
const options = vm.$options
// locate first non-abstract parent
let parent = options.parent
if (parent && !options.abstract) {
while (parent.$options.abstract && parent.$parent) {
parent = parent.$parent
}
parent.$children.push(vm)
}
vm.$parent = parent
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
}
- 定义一个常量options指向vm.$options;
- 搜索当前实例的父实例,如果没有父实例,则给当前实例的$parent设置为undefined,比如当前组件是new出来的,那么就是根组件,父辈肯定是undefined了;
- 如果当前parent存在且不是抽象组件,则将当前实例添加到父组件的$children中,成为他的孩子之一;
- 找到当前组件的根组件$root,其实就是父组件的根组件。为何这样就认为是根组件了呢?因为组件是树状关系,每一个“出生”的孩子在出生的时候就认准了自己的祖先。
- 接下来就是定义子组件容器$ children、$refs等,都是与生命周期有关的数据。
initEvents
export function initEvents (vm: Component) {
vm._events = Object.create(null)
vm._hasHookEvent = false
// init parent attached events
const listeners = vm.$options._parentListeners
if (listeners) {
updateComponentListeners(vm, listeners)
}
}
初始化事件就是指将父组件中使用v-on注册的事件添加到子组件的事件系统中。
如果v-on定义在平台标签上(如DOM),则事件被注册在浏览器事件中
- 初始化自定义事件,开始在实例上定义了一个内部原型为null的空对象,用来存储事件。
- 然后又增加一个内部属性_hasHookEvent,初始值为false。之后是判断$ options上是否含有_parentListeners,若存在则执行updateComponentListeners方法。(在模板编译阶段,解析到子组件标签时会实例化子组件,同时将标签上注册的事件解析成object,这些事件会保存在vm.$options._parentListeners中)
- updateComponentListeners方法:循环——parentListeners并使用v-on注册到this._event中即可。
initState
我们经常使用的属性,包括像是data,props,methods,watch,computed等都是在这个方法中进行初始化的。该方法介于beforeCreate和created两个钩子之间,所以在beforeCreate的时候我们还无法访问到Vue实例上的这些属性.
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)
}
}
- 首先新增属性
_watchers
,用来保存组件中所有的watcher
. - 判断是否存在
props,methods,data
等属性,如果有就调用相应的方法进行初始化 - 如果
没有data
属性就使用observe观察空对象
- 判断是否存在
computed
属性,如果有就调用相应的方法进行初始化 - 判断是否存在
watch
属性,如果有就调用相应的方法进行初始化
initProps
function initProps (vm: Component, propsOptions: Object) {
// 存放父组件传入子组件的props
const propsData = vm.$options.propsData || {}
// 存放经过转换后的最终的props的对象
const props = vm._props = {}
// 一个存放props的key的数组,就算props的值是空的,key也会存在里面
const keys = vm.$options._propKeys = []
const isRoot = !vm.$parent
// 如果不是跟组件就不需要将数据转换成响应式的
if (!isRoot) {
toggleObserving(false)
}
for (const key in propsOptions) {
keys.push(key)
// 校验props,包括对类型的校验以及产生最后的属性值
const value = validateProp(key, propsOptions, propsData, vm)
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
const hyphenatedKey = hyphenate(key)
if (isReservedAttribute(hyphenatedKey) ||
config.isReservedAttr(hyphenatedKey)) {
warn(
`"${hyphenatedKey}" is a reserved attribute and cannot be used as component prop.`,
vm
)
}
// 将props变成可响应的,非生产环境中,如果用户修改props,发出警告
defineReactive(props, key, value, () => {
if (!isRoot && !isUpdatingChildComponent) {
warn(
`Avoid mutating a prop directly since the value will be ` +
`overwritten whenever the parent component re-renders. ` +
`Instead, use a data or computed property based on the prop's ` +
`value. Prop being mutated: "${key}"`,
vm
)
}
})
} else {
defineReactive(props, key, value)
}
if (!(key in vm)) {
proxy(vm, `_props`, key)
}
}
toggleObserving(true)
}
首先遍历propsOptions(options中的props),将key字段push到keys数组里,然后通过validateProp方法对prop进行校验,主要是对prop的类型校验,以及判断父组件是否有传入相应的值,如果没有,则查看子组件中有没有声明default属性,有则将default产生的值作为props的value。
在校验完props之后就是使用defineReactive将prop变成可响应的,这样当prop发生变化的时候,对应的依赖组件可以同步变化。在非生产环境中,如果修改prop的值则会发出警告。
initMethods
该方法比较简单
function initMethods (vm: Component, methods: Object) {
const props = vm.$options.props
for (const key in methods) {
if (process.env.NODE_ENV !== 'production') {
if (methods[key] == null) {
warn(
`Method "${key}" has an undefined value in the component definition. ` +
`Did you reference the function correctly?`,
vm
)
}
if (props && hasOwn(props, key)) {
warn(
`Method "${key}" has already been defined as a prop.`,
vm
)
}
if ((key in vm) && isReserved(key)) {
warn(
`Method "${key}" conflicts with an existing Vue instance method. ` +
`Avoid defining component methods that start with _ or $.`
)
}
}
// 把每个methods[key]绑定到vm上,并将methods[key]的this指向vm
vm[key] = methods[key] == null ? noop : bind(methods[key], vm)
}
}
这个方法就是遍历一遍option中的methods并把每一个method绑定到当前vue实例上,修改这些函数的this指针指向vue实例。
initDatas
function initData (vm: Component) {
let data = vm.$options.data
//判断是不是方法,因为组件只能只能传入方法(每个组件有自己单独的变量空间)
data = vm._data = typeof data === 'function'
? getData(data, vm)
: 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
const keys = Object.keys(data)
const props = vm.$options.props
const methods = vm.$options.methods
let i = keys.length
while (i--) {
const key = keys[i]
if (process.env.NODE_ENV !== 'production') {
if (methods && hasOwn(methods, key)) {
warn(
`Method "${key}" has already been defined as a data property.`,
vm
)
}
}
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)) {
proxy(vm, `_data`, key)
}
}
//将data转为响应式数据
observe(data, true /* asRootData */)
}
将对data中的每一个属性进行判断,包括判断属性是否存在methods中和属性是否存在props中,如果都不存在,则将data[key]使用proxy代理到vm上。
initComputed
function initComputed (vm: Component, computed: Object) {
// $flow-disable-line
const watchers = vm._computedWatchers = Object.create(null)
// computed properties are just getters during SSR
const isSSR = isServerRendering()
for (const key in computed) {
const userDef = computed[key]
const getter = typeof userDef === 'function' ? userDef : userDef.get
if (process.env.NODE_ENV !== 'production' && getter == null) {
warn(
`Getter is missing for computed property "${key}".`,
vm
)
}
if (!isSSR) {
// create internal watcher for the computed property.
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
)
}
// component-defined computed properties are already defined on the
// component prototype. We only need to define computed properties defined
// at instantiation here.
if (!(key in vm)) {
defineComputed(vm, key, userDef)
} else if (process.env.NODE_ENV !== 'production') {
if (key in vm.$data) {
warn(`The computed property "${key}" is already defined in data.`, vm)
} else if (vm.$options.props && key in vm.$options.props) {
warn(`The computed property "${key}" is already defined as a prop.`, vm)
} else if (vm.$options.methods && key in vm.$options.methods) {
warn(`The computed property "${key}" is already defined as a method.`, vm)
}
}
}
}
initWatch
watch原理比较简单,就是新建一个watcher,并将watcher放入到对应数据的Observer的Dep中就可以了,这样当监听的数据变化时,就会通知我们的Watcher执行我们传入的函数。
function initWatch (vm: Component, watch: Object) {
for (const key in watch) {
const handler = watch[key]
if (Array.isArray(handler)) {
//遍历传入的watch对象,通过createWatcher创建一个个的watcher。
for (let i = 0; i < handler.length; i++) {
createWatcher(vm, key, handler[i])
}
} else {
createWatcher(vm, key, handler)
}
}
}
让我们来看看createWatcher的代码:
function createWatcher (
vm: Component,
expOrFn: string | Function,
handler: any,
options?: Object
) {
if (isPlainObject(handler)) {
options = handler
handler = handler.handler
}
if (typeof handler === 'string') {
handler = vm[handler]
}
return vm.$watch(expOrFn, handler, options)
}
createWatcher就是对hander的一些判断(字符串 函数 对象),如果是函数则不做任何处理,是对象则将options设置为handler,并将handler设置为handler对象的handler方法,如果是字符串则从vue实例中取出方法设置成handler。然后使用$watch方法监听数据,并且执行handler。
Vue.prototype.$watch = function (
expOrFn,
cb,
options
) {
var vm = this;
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options)
}
options = options || {};
options.user = true;
var watcher = new Watcher(vm, expOrFn, cb, options);
if (options.immediate) {
cb.call(vm, watcher.value);
}
return function unwatchFn () {
watcher.teardown();
}
};
总结
本文章仅为本人学习总结,如果有不足还请各位指出!