初始化流程
new Vue
我們?cè)谑褂?Vue 的時(shí)候,首頁就是先 new Vue(...) ;在上一章中通過分析構(gòu)建流程,我們得出入口文件 src/platforms/web/entry-runtime-with-compiler.js ,通過入口文件,我們一步一步找到 Vue 構(gòu)造函數(shù)定義所在:
// src/platforms/web/entry-runtime-with-compiler.js
// ...
import Vue from './runtime/index'
// ...
// src/platforms/web/runtime/index.js
import Vue from 'core/index'
// ...
// src/core/index.js
import Vue from './instance/index'
import { initGlobalAPI } from './global-api/index'
import { isServerRendering } from 'core/util/env'
import { FunctionalRenderContext } from 'core/vdom/create-functional-component'
// 初始化全局 API
initGlobalAPI(Vue)
// ...
// src/core/instance/index.js
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'
// Vue 構(gòu)造函數(shù)
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')
}
// 調(diào)用 Vue.prototype_init 方法,該方法是在 initMixin 中定義的
this._init(options)
}
// 定義 Vue.prototype_init 方法
initMixin(Vue)
/**
* 定義:
* Vue.prototype.$data
* Vue.prototype.$props
* Vue.prototype.$set
* Vue.prototype.$delete
* Vue.prototype.$watch
*/
stateMixin(Vue)
/**
* 定義 事件相關(guān)的 方法:
* Vue.prototype.$on
* Vue.prototype.$once
* Vue.prototype.$off
* Vue.prototype.$emit
*/
eventsMixin(Vue)
/**
* 定義:
* Vue.prototype._update
* Vue.prototype.$forceUpdate
* Vue.prototype.$destroy
*/
lifecycleMixin(Vue)
/**
* 定義:
* Vue.prototype.$nextTick
* Vue.prototype._render
*/
renderMixin(Vue)
export default Vue
_init
// src/core/instance/init.js
export function initMixin (Vue: Class<Component>) {
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
// 每個(gè)實(shí)例都保存一個(gè) _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)
}
// a flag to avoid this being observed
vm._isVue = true
// 處理組件配置項(xiàng)
if (options && options._isComponent) {
// 每個(gè)子組件初始化時(shí)走這里,這里只做了一些性能優(yōu)化
// 將組件配置對(duì)象上的一些深層次屬性放到 vm.$options 選項(xiàng)中,以提高代碼的執(zhí)行效率
initInternalComponent(vm, options)
} else {
// 合并選項(xiàng),合并默認(rèn)選項(xiàng)和自定義選項(xiàng)
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
// 設(shè)置代理,將 vm 實(shí)例上的屬性代理到 vm._renderProxy
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
initProxy(vm)
} else {
vm._renderProxy = vm
}
// expose real self
vm._self = vm
// 初始化實(shí)例關(guān)系屬性,$parent、$children、$refs、$root等
initLifecycle(vm)
// 初始化自定義事件,處理父組件傳遞的事件和回調(diào)
initEvents(vm)
// 解析組件的插槽信息,得到 vm.$slot,處理渲染函數(shù)(_render),得到 vm.$createElement 方法,即 h 函數(shù)
initRender(vm)
// 調(diào)用 beforeCreate 鉤子函數(shù)
callHook(vm, 'beforeCreate')
// 初始化組件的 inject 配置項(xiàng),得到 result[key] = val 形式的配置對(duì)象,然后對(duì)結(jié)果數(shù)據(jù)進(jìn)行響應(yīng)式處理,并代理每個(gè) key 到 vm 實(shí)例
initInjections(vm)
// 數(shù)據(jù)響應(yīng)式核心,處理 props、methods、data、computed、watch
initState(vm)
// 解析組件配置項(xiàng)上的 provide 對(duì)象,將其掛載到 vm._provided 屬性上
initProvide(vm) // resolve provide after data/props
// 調(diào)用 created 鉤子函數(shù)
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)
}
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
}
上面代碼很清晰的看出初始化都做了哪些事情,在初始化的最后,如果有 el 屬性,則會(huì)自動(dòng)調(diào)用 vm.$mount 進(jìn)行掛載,否則我們就需要手動(dòng)調(diào)用 $mount。接下里就進(jìn)入了掛載階段。
Vue 實(shí)例掛載
$mount
入口文件 src/platforms/web/entry-runtime-with-compiler.js :
/* @flow */
import config from 'core/config'
import { warn, cached } from 'core/util/index'
import { mark, measure } from 'core/util/perf'
import Vue from './runtime/index'
import { query } from './util/index'
import { compileToFunctions } from './compiler/index'
import { shouldDecodeNewlines, shouldDecodeNewlinesForHref } from './util/compat'
const idToTemplate = cached(id => {
const el = query(id)
return el && el.innerHTML
})
/**
* 編譯器的入口
* 進(jìn)行預(yù)編譯,最終將模版編譯成 render 函數(shù)
*/
// 緩存原型上的方法
const mount = Vue.prototype.$mount
// 重新定義該方法
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && query(el)
// 不能掛載在 body、html 這樣的根節(jié)點(diǎn)上
if (el === document.body || el === document.documentElement) {
process.env.NODE_ENV !== 'production' && warn(
`Do not mount Vue to <html> or <body> - mount to normal elements instead.`
)
return this
}
const options = this.$options
/**
* 若沒有 render方法,則解析 template 和 el,并轉(zhuǎn)換為 render 函數(shù)
* 優(yōu)先級(jí):render > template > el
*/
if (!options.render) {
let template = options.template
// template
if (template) {
if (typeof template === 'string') {
if (template.charAt(0) === '#') {
// { template: '#app' },以 id 為 ‘a(chǎn)pp’ 的節(jié)點(diǎn),作為掛載節(jié)點(diǎn)
template = idToTemplate(template)
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && !template) {
warn(
`Template element not found or is empty: ${options.template}`,
this
)
}
}
} else if (template.nodeType) {
// template 是一個(gè)正常的元素,獲取其 innerHtml 作為模版
template = template.innerHTML
} else {
if (process.env.NODE_ENV !== 'production') {
warn('invalid template option:' + template, this)
}
return this
}
} else if (el) {
// el
template = getOuterHTML(el)
}
if (template) {
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile')
}
// 編譯模版,得到動(dòng)態(tài)渲染函數(shù)和靜態(tài)渲染函數(shù)
const { render, staticRenderFns } = compileToFunctions(template, {
// 在非生產(chǎn)環(huán)境下,編譯時(shí)記錄標(biāo)簽屬性在模版字符串中開始和結(jié)束的位置索引
outputSourceRange: process.env.NODE_ENV !== 'production',
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
// 界定符,默認(rèn) {{}}
delimiters: options.delimiters,
// 是否保留注釋
comments: options.comments
}, this)
// 將兩個(gè)渲染函數(shù)放到 this.$options 上
options.render = render
options.staticRenderFns = staticRenderFns
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile end')
measure(`vue ${this._name} compile`, 'compile', 'compile end')
}
}
}
// 調(diào)用原型上方法掛載
return mount.call(this, el, hydrating)
}
/**
* Get outerHTML of elements, taking care
* of SVG elements in IE as well.
*/
function getOuterHTML (el: Element): string {
if (el.outerHTML) {
return el.outerHTML
} else {
const container = document.createElement('div')
container.appendChild(el.cloneNode(true))
return container.innerHTML
}
}
Vue.compile = compileToFunctions
export default Vue
從上面代碼可以看出,不管定義 render 方法還是 el 和 template 屬性,最終的目的就是得到 render 渲染函數(shù)。然后保存在 options 上。
編譯模板,得到 render 渲染函數(shù),通過調(diào)用 compileToFunctions 方法,這個(gè)到編譯器的時(shí)候再一塊看。
最后調(diào)用原型上的 $mount ,定義在 src/platform/web/runtime/index.js 中
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
實(shí)際調(diào)用 mountComponent ,定義在 src/core/instance/lifecycle.js
mountComponent
// src/core/instance/lifecycle.js
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
if (!vm.$options.render) {
vm.$options.render = createEmptyVNode
if (process.env.NODE_ENV !== 'production') {
/* istanbul ignore if */
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
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && 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 {
// 執(zhí)行 vm._render() 函數(shù),得到 虛擬 DOM,并將 vnode 傳遞給 _update 方法,接下來就該到 patch 階段了
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
}
// we set this to vm._watcher inside the watcher's constructor
// since the watcher's initial patch may call $forceUpdate (e.g. inside child
// component's mounted hook), which relies on vm._watcher being already defined
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
hydrating = false
// vm.$vnode 表示 Vue 實(shí)例的父虛擬 Node,所以它為 Null 則表示當(dāng)前是根 Vue 的實(shí)例
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}
mountComponent 內(nèi)定義了 updateComponent 方法,然后實(shí)例化一個(gè)Watcher,同時(shí)將 updateComponent 作為參數(shù)傳入,在 Watcher 的回調(diào)函數(shù)中被調(diào)用。Watcher 在這里主要是初始化和數(shù)據(jù)變化時(shí),執(zhí)行回調(diào)函數(shù)。
最后設(shè)置 vm._isMounted = true ,表示實(shí)例已掛載。
updateComponent 的調(diào)用會(huì)執(zhí)行 vm._update 和 vm._render 。vm._render 獲取虛擬DOM,vm._update 更新視圖。
上面代碼出現(xiàn)了三個(gè)生命周期鉤子 beforeMount 、beforeUpdate、mounted ;也就是說,在執(zhí)行 vm._render() 之前,執(zhí)行了 beforeMount 鉤子函數(shù);在執(zhí)行完 vm._update() 把虛擬DOM轉(zhuǎn)換真實(shí) DOM 后,執(zhí)行 mounted 鉤子函數(shù);后續(xù)若數(shù)據(jù)變化時(shí),通過 _isMounted 標(biāo)記,表示已掛載則執(zhí)行 beforeUpdate 鉤子函數(shù)。
這里值得注意的是,在
mounted鉤子執(zhí)行前有個(gè)判斷,只有在父虛擬 Node 為null的時(shí)候執(zhí)行。只有new Vue才會(huì)走到這里,如果是組件的話,它的父虛擬 Node 是存在的。組件的mounted在別的地方。
相關(guān)鏈接
如果覺得還湊合的話,給個(gè)贊吧!?。∫部梢詠砦业膫€(gè)人博客逛逛 https://www.mingme.net/