从new Vue开始来理解组件生命周期

生命周期流程图
每个Vue实例在创建时都要经过一系列初始化, 例如设置数据监听、编译模板、将实例挂载到DOM并在数据变化时更新DOM等. 同时, 也会运行一些叫作生命周期钩子的函数, 这给了我们在不同阶段添加自定义代码的机会. 接下来让我们一起来探索Vue实例被创建时都经历了什么.
生命周期图示(贴自vue官网)
new Vue()被调用时发生了什么
想要了解new Vue()被调用时发生了什么, 我们需要知道在Vue构造函数中实现了哪些逻辑。具体代码如下:
function Vue (options) {
if (process.env.NODE_ENV !== 'production' && !(this instanceof Vue)) {
warn('Vue is a constructor and should be called with `new` keyword')
}
this._init(options)
}
export default Vue
从上面代码我们可以看到调用了_init函数来执行生命周期的初始化流程,那么this._init是在哪里定义的,内部原理是怎样的呢?
_init方法的定义
import { initMixin } from './init'
function Vue (options) {
if (process.env.NODE_ENV !== 'production' && !(this instanceof Vue)) {
warn('Vue is a constructor and should be called with `new` keyword')
}
this._init(options)
}
initMixin(Vue)
export default Vue
将init.js文件导出的initMixin函数引入后,通过调用initMixin函数向Vue构造函数的原型中挂载一些方法。initMixin方法的实现代码如下:
export function initMixin (Vue) {
Vue.prototype._init = function (options) {
//
}
}
_init方法的内部原理
Vue.prototype._init = function (options) {
const vm = this
// uid初始化值为0
vm._uid = uid++
let startTag, endTag
if (__DEV__ && config.performance && mark) {
startTag = `vue-perf-start:${vm._uid}`
endTag = `vue-perf-end:${vm._uid}`
mark(startTag)
}
// vue实例标志
vm._Vue = true
// 避免observed
vm.__v_skip = true
vm._scope = new EffectScope(true /* detached */)
vm._scope._vm = true
if (options && options._isComponent) {
initInternalComponent(vm, options as any)
} else {
// 合并构造函数配置和参数配置
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor as any),
options || {},
vm
)
}
if (__DEV__) {
initProxy(vm)
} else {
vm._renderProxy = vm
}
// expose real self
vm._self = vm
// 初始化生命周期相关
initLifecycle(vm)
// 初始化事件相关
initEvents(vm)
// 初始化render相关
initRender(vm)
// 调用beforeCreate钩子
callHook(vm, 'beforeCreate', undefined, false)
// 初始化inject
initInjections(vm)
// 初始化状态相关
initState(vm)
// 初始化provide
initProvide(vm)
// 调用created钩子
callHook(vm, 'created')
if (__DEV__ && config.performance && mark) {
vm._name = formatComponentName(vm, false)
mark(endTag)
measure(`vue ${vm._name} init`, startTag, endTag)
}
// 如果有配置el选项 自动调用mount 否则需要手动调用mount
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
从_init方法的源码实现, 我们可以画出new Vue的主要流程图:
接下来我们来看下_init内部几个初始化方法的源码实现:
initLifecycle
function initLifecycle (vm) {
const options = vm.$options
let parent = options.parenet
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._provided = parent ? parent._provided : Object.create(null)
vm._watcher = null
vm._inactive = null
vm._directInactive = false
vm._isMounted = false
vm._isDestroyed = false
vm._isBeingDestroyed = false
}
initEvents
function initEvents (vm) {
vm._events = Object.create(null)
vm._hasHookEvent = false
const listeners = vm.$options._parentListeners
if (listeners) {
updateComponentListeners(vm, listeners)
}
}
initRender
function initRender(vm) {
vm._vnode = null
vm._staticTrees = null
const options = vm.$options
const parentVnode = (vm.$vnode = options._parentVnode!)
const renderContext = parentVnode && (parentVnode.context as Component)
vm.$slots = resolveSlots(options._renderChildren, renderContext)
vm.$scopedSlots = parentVnode
? normalizeScopedSlots(
vm.$parent!,
parentVnode.data!.scopedSlots,
vm.$slots
)
: emptyObject
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
const parentData = parentVnode && parentVnode.data
if (__DEV__) {
defineReactive(
vm,
'$attrs',
(parentData && parentData.attrs) || emptyObject,
() => {
!isUpdatingChildComponent && warn(`$attrs is readonly.`, vm)
},
true
)
defineReactive(
vm,
'$listeners',
options._parentListeners || emptyObject,
() => {
!isUpdatingChildComponent && warn(`$listeners is readonly.`, vm)
},
true
)
} else {
defineReactive(
vm,
'$attrs',
(parentData && parentData.attrs) || emptyObject,
null,
true
)
defineReactive(
vm,
'$listeners',
options._parentListeners || emptyObject,
null,
true
)
}
}
initInjections
function initInjections(vm: Component) {
const result = resolveInject(vm.$options.inject, vm)
if (result) {
toggleObserving(false)
Object.keys(result).forEach(key => {
if (__DEV__) {
defineReactive(vm, key, result[key], () => {
warn(
`Avoid mutating an injected value directly since the changes will be ` +
`overwritten whenever the provided component re-renders. ` +
`injection being mutated: "${key}"`,
vm
)
})
} else {
defineReactive(vm, key, result[key])
}
})
toggleObserving(true)
}
}
function resolveInject(
inject: any,
vm: Component
): Record<string, any> | undefined | null {
if (inject) {
const result = Object.create(null)
const keys = hasSymbol ? Reflect.ownKeys(inject) : Object.keys(inject)
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
if (key === '__ob__') continue
const provideKey = inject[key].from
if (provideKey in vm._provided) {
result[key] = vm._provided[provideKey]
} else if ('default' in inject[key]) {
const provideDefault = inject[key].default
result[key] = isFunction(provideDefault)
? provideDefault.call(vm)
: provideDefault
} else if (__DEV__) {
warn(`Injection "${key as string}" not found`, vm)
}
}
return result
}
}
initState
function initState (vm) {
const opts = vm.$options
if (opts.props) {
initProps(vm, opts.props)
}
// 组合式api
initSetup(vm)
if (opts.methods) {
initMethods(vm, opts.methods)
}
if (ops.data) {
initData(vm, opts.data)
} else {
const ob = observe(vm._data = {})
ob && ob.vmCount++
}
if (opts.compouted) {
initComputed(vm, opts.computed)
}
// Firefox Object.prototype有watch方法 nativeWatch = {}.watch
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
function initProps(vm, propsOptions) {
const propsData = vm.$options.propsData || {}
const props = (vm._props = shallowReactive({}))
// 用数组保存props的key 方便便利props
const keys: string[] = (vm.$options._propKeys = [])
const isRoot = !vm.$parent
if (!isRoot) {
toggleObserving(false)
}
for (const key in propsOptions) {
keys.push(key)
const value = validateProp(key, propsOptions, propsData, vm)
if (__DEV__) {
const hyphenatedKey = hyphenate(key)
if (
isReservedAttribute(hyphenatedKey) ||
config.isReservedAttr(hyphenatedKey)
) {
warn(
`"${hyphenatedKey}" is a reserved attribute and cannot be used as component prop.`,
vm
)
}
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)
}
function initData(vm) {
let data: any = vm.$options.data
data = vm._data = isFunction(data) ? getData(data, vm) : data || {}
if (!isPlainObject(data)) {
data = {}
__DEV__ &&
warn(
'data functions should return an object:\n' +
'https://v2.vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
vm
)
}
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 (__DEV__) {
if (methods && hasOwn(methods, key)) {
warn(`Method "${key}" has already been defined as a data property.`, vm)
}
}
if (props && hasOwn(props, key)) {
__DEV__ &&
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)
}
}
// observe data
const ob = observe(data)
ob && ob.vmCount++
}
function initMethods(vm, methods: Object) {
const props = vm.$options.props
for (const key in methods) {
if (__DEV__) {
if (typeof methods[key] !== 'function') {
warn(
`Method "${key}" has type "${typeof methods[
key
]}" 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 $.`
)
}
}
vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm)
}
}
function initWatch(vm: Component, watch: Object) {
for (const key in watch) {
const handler = watch[key]
if (isArray(handler)) {
for (let i = 0; i < handler.length; i++) {
createWatcher(vm, key, handler[i])
}
} else {
createWatcher(vm, key, handler)
}
}
}
function createWatcher(
vm: Component,
expOrFn: string | (() => any),
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)
}
initProvides
function provide<T>(key: InjectionKey<T> | string | number, value: T) {
if (!currentInstance) {
if (__DEV__) {
warn(`provide() can only be used inside setup().`)
}
} else {
resolveProvided(currentInstance)[key as string] = value
}
}
export function resolveProvided(vm: Component): Record<string, any> {
const existing = vm._provided
const parentProvides = vm.$parent && vm.$parent._provided
if (parentProvides === existing) {
return (vm._provided = Object.create(parentProvides))
} else {
return existing
}
}
mount实现
到这里, 初始化基本完成. 从前面_init方法的实现, 我们可以看到初始化之后会执行mount, 代码如下:
// 如果有配置el选项 自动调用mount 否则需要手动调用mount
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
接下来我们来看下vm.$mount的具体实现:
function query (el) {
if (typeof el === 'string') {
const selected = document.querySelector(el)
if (!selected) {
__DEV__ && warn('Cannot find element: ' + el)
return docuemnt.createElement('div')
}
return selected
} else {
return el
}
function cached (fn) {
// 模板缓存
const cache = Object.create(null)
return function (str) {
const hit = cache[str]
return hit || (cache[str] = fn(str))
}
}
const idToTemplate = cached(id => {
const el = query(id)
return el && el.innerHTML
})
function getOuterHTML (el) {
if (el.outerHTML) {
return el.outerHTML
} else {
const container = document.createElement('div')
container.appendChild(el.cloneNode(true))
return container.innerHTML
}
}
Vue.prototype.$mount = function (el, hydrating) {
el = el && inBrower ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (el, hydrating) {
el = el && query(el)
if (el === document.body || el === document.documentElement) {
__DEV__ &&
warn(`Do not mount Vue to <html> or <body> - mount to normal elements instead.`)
return this
}
const options = this.$options
// 如果options没有render 使用template属性
if (!options.render) {
let template = options.template
// template属性优先使用
if (template) {
if (typeof template === 'string') {
if (template.charAt(0) === '#') {
template = idToTemplate(template)
if (__DEV__ && !template) {
warn(
`Template element not found or is empty: ${options.template}`,
this
)
}
}
} else if (template.nodeType) {
template = template.innerHTML
} else {
if (__DEV__) {
warn('invalid template option:' + template, this)
}
return this
}
} else if (el) { // template属性不存在 再使用el属性
template = getOuterHTML(el)
}
if (template) {
if (__DEV__ && config.performance && mark) {
mark('compile')
}
const { render, staticRenderFns } = compileToFunctions(
template,
{
outputSourceRange: __DEV__,
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
},
this
)
options.render = render
options.staticRenderFns = staticRenderFns
if (__DEV__ && config.performance && mark) {
mark('compile end')
measure(`vue ${this._name} compile`, 'compile', 'compile end')
}
}
}
return mount.call(this, el, hydrating)
}
从$mount源码中我们可以知道mount的核心是mountComponent函数, 下面我们来看下mountComponent的实现:
function mountComponent (vm, el, hydrating) {
vm.$el = el
if (!vm.$options.render) {
vm.$options.render = createEmptyNode
if (__DEV__) {
if (
(vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
vm.$options.el ||
el
) {
warn(
'You are using the runtime-only build of Vue where the template ' +
'compiler is not available. Either pre-compile the templates into ' +
'render functions, or use the compiler-included build.',
vm
)
} else {
warn(
'Failed to mount component: template or render function not defined.',
vm
)
}
}
}
callHook(vm, 'beforeMount')
let updateComponent
if (__DEV__ && config.performance && mark) {
updateComponent = () => {
const name = vm._name
const id = vm._uid
const startTag = `vue-perf-start:${id}`
const endTag = `vue-perf-end:${id}`
mark(startTag)
const vnode = vm._render()
mark(endTag)
measure(`vue ${name} render`, startTag, endTag)
mark(startTag)
vm._update(vnode, hydrating)
mark(endTag)
measure(`vue ${name} patch`, startTag, endTag)
}
} else {
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
}
const watcherOptions: WatcherOptions = {
before() {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}
if (__DEV__) {
watcherOptions.onTrack = e => callHook(vm, 'renderTracked', [e])
watcherOptions.onTrigger = e => callHook(vm, 'renderTriggered', [e])
}
new Watcher(
vm,
updateComponent,
noop,
watcherOptions,
true /* isRenderWatcher */
)
hydrating = false
const preWatchers = vm._preWatchers
if (preWatchers) {
for (let i = 0; i < preWatchers.length; i++) {
preWatchers[i].run()
}
}
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}
mountComponent代码实现中会实例化一个Watcher对象, 这里主要有两个作用:
- 初始化的时候会执行expOrFn, 也就是updateComponent.
- 当vm实例中监测的数据发生变化时会执行updateComponent.
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
我们接着来看下vm._update的实现:
Vue.prototype._update = function (vnode, hydrating) {
const vm = this
const prevEl = vm.$el
const prevVnode = vm._vnode
const restoreActiveInstance = setActiveInstance(vm)
vm._vnode = vnode
if (!prevVnode) {
// 初始化渲染
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false)
} else {
// 组件更新时
vm.$el = vm.__patch__(prevVnode, vnode)
}
restoreActiveInstance()
if (prevEl) {
prevEl.__vue__ = null
}
if (vm.$el) {
vm.$el.__vue__ = vm
}
let wrapper = vm
while (
wrapper &&
wrapper.$vnode &&
wrapper.$parent &&
wrapper.$vnode === wrapper.$parent._vnode
) {
wrapper.$parent.$el = wrapper.$el
wrapper = wrapper.$parent
}
}
我们可以看到组件初始化渲染时会执行vm.__patch__方法, vm.__patch__的具体实现本章就不细说了, 后面在虚拟DOM章节中再一起学习下, 这里就大概说下__patch__方法主要做了什么:
- 循环遍历vm.children, 执行createElm方法
- createElm方法执行时如果当前child元素是组件则创建组件, 并执行组件init方法和mount方法(就是重新走一遍前面流程), 然后插入当前元素, 执行组件mounted钩子
最后
以上就是vue组件生命周期主要流程, 从源码实现中, 我们可以知道父子组件初始化生命周期钩子执行顺序:
beforeCreate(父) -> created(父) -> beforeMount(父) -> beforeCreate(子) -> created(子) -> beforeMount(子) -> mounted(子) -> mounted(父)
转载自:https://juejin.cn/post/7235458133506097213