深入了解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(父)
以上就是深入了解Vue之组件的生命周期流程的详细内容,更多关于Vue 生命周期的资料请关注脚本之家其它相关文章!