深入了解Vue之組件的生命周期流程
生命周期流程圖
每個Vue實例在創(chuàng)建時都要經過一系列初始化, 例如設置數據監(jiān)聽、編譯模板、將實例掛載到DOM并在數據變化時更新DOM等. 同時, 也會運行一些叫作生命周期鉤子的函數, 這給了我們在不同階段添加自定義代碼的機會. 接下來讓我們一起來探索Vue實例被創(chuàng)建時都經歷了什么.
生命周期圖示(貼自vue官網)
new Vue()被調用時發(fā)生了什么
想要了解new Vue()被調用時發(fā)生了什么, 我們需要知道在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函數來執(zhí)行生命周期的初始化流程,那么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) // 初始化狀態(tài)相關 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) { // 如果為抽象組件 繼續(xù)往上找 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方法的實現, 我們可以看到初始化之后會執(zhí)行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屬性優(yōu)先使用 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源碼中我們可以知道m(xù)ount的核心是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對象, 這里主要有兩個作用:
- 初始化的時候會執(zhí)行expOrFn, 也就是updateComponent.
- 當vm實例中監(jiān)測的數據發(fā)生變化時會執(zhí)行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 } }
我們可以看到組件初始化渲染時會執(zhí)行vm.__patch__方法, vm.__patch__的具體實現本章就不細說了, 后面在虛擬DOM章節(jié)中再一起學習下, 這里就大概說下__patch__方法主要做了什么:
- 循環(huán)遍歷vm.children, 執(zhí)行createElm方法
- createElm方法執(zhí)行時如果當前child元素是組件則創(chuàng)建組件, 并執(zhí)行組件init方法和mount方法(就是重新走一遍前面流程), 然后插入當前元素, 執(zhí)行組件mounted鉤子
最后
以上就是vue組件生命周期主要流程, 從源碼實現中, 我們可以知道父子組件初始化生命周期鉤子執(zhí)行順序:
beforeCreate(父) -> created(父) -> beforeMount(父) -> beforeCreate(子) -> created(子) -> beforeMount(子) -> mounted(子) -> mounted(父)
以上就是深入了解Vue之組件的生命周期流程的詳細內容,更多關于Vue 生命周期的資料請關注腳本之家其它相關文章!
相關文章
vite2.x實現按需加載ant-design-vue@next組件的方法
這篇文章主要介紹了vite2.x實現按需加載ant-design-vue@next組件的方法,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2021-03-03vue.js給動態(tài)綁定的radio列表做批量編輯的方法
下面小編就為大家分享一篇vue.js給動態(tài)綁定的radio列表做批量編輯的方法,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2018-02-02詳解keep-alive + vuex 讓緩存的頁面靈活起來
這篇文章主要介紹了keep-alive + vuex 讓緩存的頁面靈活起來,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2019-04-04Vue3+Vite項目引入Bootstrap5、bootstrap-icons方式
這篇文章主要介紹了Vue3+Vite項目引入Bootstrap5、bootstrap-icons方式,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2023-10-10vue中this.$router.go(-1)失效(路由改變了,界面未刷新)
本文主要介紹了vue中this.$router.go(-1)失效(路由改變了,界面未刷新),文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2023-12-12vue + any-touch實現一個iscroll 實現拖拽和滑動動畫效果
這篇文章主要介紹了vue + any-touch實現一個iscroll實現拖拽和滑動動畫效果,本文通過實例代碼給大家介紹的非常詳細,具有一定的參考借鑒價值,需要的朋友可以參考下2019-04-04vue2移動端使用vue-qrcode-reader實現掃一掃功能的步驟
最近在使用vue開發(fā)的h5移動端想要實現一個調用攝像頭掃描二維碼的功能,所以下面這篇文章主要給大家介紹了關于vue2移動端使用vue-qrcode-reader實現掃一掃功能的相關資料,需要的朋友可以參考下2023-06-06解決vue安裝less報錯Failed to compile with 1 errors的問題
這篇文章主要介紹了解決vue安裝less報錯Failed to compile with 1 errors的問題,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-10-10