Vue 中我們是通過 $mount 實例方法去掛載 vm 的,$mount 方法在多個文件中都有定義,如 src/platform/web/entry-runtime-with-compiler.js、src/platform/web/runtime/index.js、src/platform/weex/runtime/index.js。因為 $mount 這個方法的實現是和平臺、構建方式都相關的。接下來我們重點分析帶 compiler 版本的 $mount 實現,因為拋開 webpack 的 vue-loader,我們在純前端瀏覽器環境分析 Vue 的工作原理,有助于我們對原理理解的深入。
compiler 版本的 $mount 實現非常有意思,先來看一下 src/platform/web/entry-runtime-with-compiler.js 文件中定義:
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (el?: string | Element,hydrating?: boolean
): Component {el = el && query(el)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.$optionsif (!options.render) {let template = options.templateif (template) {if (typeof template === 'string') {if (template.charAt(0) === '#') {template = idToTemplate(template)if (process.env.NODE_ENV !== 'production' && !template) {warn(`Template element not found or is empty: ${options.template}`,this)}}} else if (template.nodeType) {template = template.innerHTML} else {if (process.env.NODE_ENV !== 'production') {warn('invalid template option:' + template, this)}return this}} else if (el) {template = getOuterHTML(el)}if (template) {if (process.env.NODE_ENV !== 'production' && config.performance && mark) {mark('compile')}const { render, staticRenderFns } = compileToFunctions(template, {shouldDecodeNewlines,shouldDecodeNewlinesForHref,delimiters: options.delimiters,comments: options.comments}, this)options.render = renderoptions.staticRenderFns = staticRenderFnsif (process.env.NODE_ENV !== 'production' && config.performance && mark) {mark('compile end')measure(`vue ${this._name} compile`, 'compile', 'compile end')}}}return mount.call(this, el, hydrating)
}
這段代碼首先緩存了原型上的 $mount 方法,再重新定義該方法,我們先來分析這段代碼。首先,它對 el 做了限制,Vue 不能掛載在 body、html 這樣的根節點上。接下來的是很關鍵的邏輯 , 如果沒有定義 render 方法,則會把 el 或者 template 字符串轉換成 render 方法。這里我們要牢記,在 Vue 2.0 版本中,所有 Vue 的組件的渲染最終都需要 render 方法,無論我們是用單文件 .vue 方式開發組件,還是寫了 el 或者 template 屬性,最終都會轉換成 render 方法,那么這個過程是 Vue 的一個“在線編譯”的過程,它是調用 compileToFunctions 方法實現的,編譯過程我們之后會介紹。最后,調用原先原型上的 $mount 方法掛載。
原先原型上的 $mount 方法在 src/platform/web/runtime/index.js 中定義,之所以這么設計完全是為了復用,因為它是可以被 runtime only 版本的 Vue 直接使用的。
Vue.prototype.$mount = function (el?: string | Element,hydrating?: boolean
): Component {el = el && inBrowser ? query(el) : undefinedreturn mountComponent(this, el, hydrating)
}
$mount 方法支持傳入 2 個參數,第一個是 el,它表示掛載的元素,可以是字符串,也可以是 DOM 對象,如果是字符串在瀏覽器環境下會調用 query 方法轉換成 DOM 對象的。第二個參數是和服務端渲染相關,在瀏覽器環境下我們不需要傳第二個參數。
$mount 方法實際上會去調用 mountComponent 方法,這個方法定義在 src/core/instance/lifecycle.js 文件中:
export function mountComponent(vm: Component,el: ?Element,hydrating?: boolean): Component {vm.$el = elif (!vm.$options.render) {vm.$options.render = createEmptyVNodeif (process.env.NODE_ENV !== 'production') {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 updateComponentif (process.env.NODE_ENV !== 'production' && config.performance && mark) {updateComponent = () => {const name = vm._nameconst id = vm._uidconst 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)}}new Watcher(vm, updateComponent, noop, {before() {if (vm._isMounted) {callHook(vm, 'beforeUpdate')}}}, true )hydrating = falseif (vm.$vnode == null) {vm._isMounted = truecallHook(vm, 'mounted')}return vm
}
從上面的代碼可以看到,mountComponent 核心就是先實例化一個渲染Watcher,在它的回調函數中會調用 updateComponent 方法,在此方法中調用 vm._render 方法先生成虛擬 Node,最終調用 vm._update 更新 DOM。
Watcher 在這里起到兩個作用,一個是初始化的時候會執行回調函數,另一個是當 vm 實例中的監測的數據發生變化的時候執行回調函數,這塊兒我們會在之后的章節中介紹。
函數最后判斷為根節點的時候設置 vm._isMounted 為 true, 表示這個實例已經掛載了,同時執行 mounted 鉤子函數。 這里注意 vm.$vnode 表示 Vue 實例的父虛擬 Node,所以它為 Null 則表示當前是根 Vue 的實例。
總結
mountComponent 方法的邏輯也是非常清晰的,它會完成整個渲染工作,接下來我們要重點分析其中的細節,也就是最核心的 2 個方法:vm._render 和 vm._update。