Vue 源碼淺析:模板掛載-兩個 $mount 方法


Vue 源碼淺析:模板掛載-兩個 $mount 方法

兩個 $mount 方法?

編譯時的 $mount

在 Vue 初始化的最後部分,我們看到 $mount 的調用,它是在哪裡定義的呢?

<code>if (vm.$options.el) {
vm.$mount(vm.$options.el)
}/<code>


對於一般使用,將會選擇完整版的 Vue 文件(編譯器 + 運行時),所以必將包含如下文件內容:

<code>//source-code\\vue\\src\\platforms\\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)
//...
const options = this.$options
if (!options.render) {
let template = options.template
if (template) {
//...
}
if (template) {
const { render, staticRenderFns } = compileToFunctions(template, {
outputSourceRange: process.env.NODE_ENV !== 'production',
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,

comments: options.comments
}, this)
options.render = render
options.staticRenderFns = staticRenderFns
}
return mount.call(this, el, hydrating)
}/<code>

上述代碼出現兩個 Vue.prototype.$mount,前者是運行時部分,後者是編譯時部分,並且最終引用時,使用的是後者,內部 return 時,使用之前 Vue.prototype.$mount 定義賦值 mount 變量。


注意在這個 Vue.prototype.$mount 中會獲取 Vue.options 屬性上的 template 選項,然後將其傳入 compileToFunctions 函數,得到 render staticRenderFns 渲染有關的方法。


對應 template 相關獲取邏輯代碼沒有貼出來,具體可以參照生命週期圖譜理解:

Vue 源碼淺析:模板掛載-兩個 $mount 方法


運行時的 $mount

我們來看第一次 Vue.prototype.$mount 的代碼:

<code>//source-code\\vue\\src\\platforms\\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)
}/<code>
<code>export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
if (!vm.$options.render) {
vm.$options.render = createEmptyVNode
}
callHook(vm, 'beforeMount')

let updateComponent
updateComponent = () => {
vm._update(vm._render(), hydrating)
}

new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
hydrating = false

if (vm.$vnode == null) {
vm._isMounted = true

callHook(vm, 'mounted')
}
return vm
}/<code>

在這裡定義了我們數據響應中看到的 Watcher 對象,其中有個重要的 updateComponent 方法,並且這個 Watcher 對象創建在 beforeMount mounted 之間。

updateComponent 方法中涉及了 Vue 中 _update _render 內部方法,我們後續細看他們有什麼作用(在 _render 中會調用到之前 $mount 方法生成 vm.$options.render 方法)

這篇將圍繞這兩個 $mount 方法開始。

compileToFunctions 主流程

此方法是在編譯版本(compiler )中的 $mount 方法內出現,通過 compileToFunctions 方法將獲取到 { render, staticRenderFns } 這兩個方法:

<code>Vue.prototype.$mount = function (el?: string | Element,hydrating?: boolean): Component {
el = el && query(el)
const options = this.$options
// resolve template/el and convert to render function
if (!options.render) {
//...
const { render, staticRenderFns } = compileToFunctions(template, {
outputSourceRange: process.env.NODE_ENV !== 'production',
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this)
options.render = render
options.staticRenderFns = staticRenderFns
}
return mount.call(this, el, hydrating)
} /<code>

因為 compileToFunctions 調用相對複雜,我們看下主要流程:


編譯器 createComiler

compileToFunctions 屬性方法,是由 createComiler 方法執行後返回的結果之一 :

<code>const { compile, compileToFunctions } = createCompiler(baseOptions)/<code>


編譯器工廠 createCompilerCreator

createComiler 其實就是 createCompilerCreator 工廠方法,內部包含一個 baseCompile (這將在 AST 章節中說明):

<code>export const createCompiler = createCompilerCreator(function baseCompile (
template: string,
options: CompilerOptions
): CompiledResult {
const ast = parse(template.trim(), options)
if (options.optimize !== false) {
optimize(ast, options)
}
const code = generate(ast, options)
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
})/<code>

下面試編譯器工廠方法的主要代碼:

<code>export function createCompilerCreator (baseCompile: Function): Function {
return function createCompiler (baseOptions: CompilerOptions) {
function compile (
template: string,
options?: CompilerOptions
): CompiledResult {
//...
const compiled = baseCompile(template.trim(), finalOptions)
//...
return compiled
}
return {
compile,
compileToFunctions: createCompileToFunctionFn(compile)
}
}
}/<code>

createCompilerCreator 工廠方法會傳入一個基礎編譯方法 baseCompile

,其內部包含 AST 語法樹的解析。

其內部會定義一個核心編譯函數 compile,內部將根據 baseCompile 處理 template 內容,用來得到 compiled 已編譯的結果。

最後工廠方法將 return {comile,compileToFunctions} 結果,交給 $mount 使用,賦值到 vue.$options 上。

這一系列是個高階函數的調用,最核心的還是需要明白 baseCompile 內部的工作流程,已經它返回的結果。


updateComponent

updateComponent 方法出現在運行時 $mount 方法中,是通過 mountComponent 方法來觸發。

<code>export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
if (!vm.$options.render) {
vm.$options.render = createEmptyVNode
}
callHook(vm, 'beforeMount')

let updateComponent

updateComponent = () => {
vm._update(vm._render(), hydrating)
}

new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
hydrating = false

if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}/<code>

能看到上兩個生命週期中間就只幹了 Watcher 對象的創建,當然結合生命週期圖譜,我們知道 Watcher 對象會觸發有關數據更新的邏輯。

Vue 源碼淺析:模板掛載-兩個 $mount 方法


我們已經大致在上篇瞭解了 Watcher 的工作大致原理,這裡著重看下 updateComponent 方法(也就是 Watcher 函數的 expression 入參的作用)


因為它在整個 Watcher 的執行過程中起著核心作用:

<code>export default class Watcher {
vm: Component;
expression: string;
//...

constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
//...
this.getter = expOrFn
///
}
get () {
//...
value = this.getter.call(vm, vm)
}
}/<code>

下面就 updateComponent 方法內兩個 Vue 函數(_update,_render)做說明:

_render

我們先從 _render 方法開始:

<code>updateComponent = () => {
vm._update(vm._render(), hydrating)
}/<code>


vm._renderrenderMixin 方法中被聲明,對應是 Vue.prototype._render 方法:

<code>//source-code\\vue\\src\\core\\instance\\render.js
export function renderMixin (Vue: Class<component>) {
//..
Vue.prototype.$nextTick = function (fn: Function) {
return nextTick(fn, this)
}

Vue.prototype._render = function (): VNode {
const vm: Component = this
const { render, _parentVnode } = vm.$options

//..
vm.$vnode = _parentVnode
// render self
let vnode
try {
currentRenderingInstance = vm
vnode = render.call(vm._renderProxy, vm.$createElement)
} catch (e) {
//..
} finally {
currentRenderingInstance = null
}
// if the returned array contains only a single node, allow it
if (Array.isArray(vnode) && vnode.length === 1) {
vnode = vnode[0]

}
//..
// set parent
vnode.parent = _parentVnode
return vnode
}
}
/<component>/<code>

能看到 _render 方法最終將返回一個 vnode 對象,這個對象就是根據我們之前 baseCompile 中,通過 generate 方法生成的 code 對象中的 render 方法執行後得到:

<code>vnode = render.call(vm._renderProxy, vm.$createElement)/<code>

這個 code 對象會在 createFunction 做函數的包裝,以讓我們可以調用:

<code>//source-code\\vue\\src\\compiler\\to-function.js
function createFunction (code, errors) {
try {
return new Function(code)
} catch (err) {
//...
}
}/<code>

當 call 時,會是這個樣子:

Vue 源碼淺析:模板掛載-兩個 $mount 方法


注意 with 的作用域都是在 this 下(即:vue 引用),所以 _c、_s、_e... 以及變量的getter/setter 操作都將觸發 vue 中前面設置好的方法。 _c 方法的入參都是在 AST 解析中拼接出來的參數列表。


如下是 _c 的相關代碼:

<code>vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)/<code>


內部調用 createElement 方法:

<code>export function createElement (
context: Component,
tag: any,
data: any,
children: any,
normalizationType: any,
alwaysNormalize: boolean
): VNode | Array<vnode> {
if (Array.isArray(data) || isPrimitive(data)) {
normalizationType = children
children = data
data = undefined
}
if (isTrue(alwaysNormalize)) {
normalizationType = ALWAYS_NORMALIZE
}
return _createElement(context, tag, data, children, normalizationType)
}/<vnode>/<code>

對於 createElement 不過多說明,最終將通過 VNode 對象創建 vnode 或者通過 createEmptyVNode 新建一個空的 vnode。


最終 vnode 會是如下結果:

Vue 源碼淺析:模板掛載-兩個 $mount 方法

_update

通過 _render 方法得到 vnode 對象,然後交給 _update 使用:

<code>Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
const vm: Component = this
const prevEl = vm.$el
const prevVnode = vm._vnode
const restoreActiveInstance = setActiveInstance(vm)
vm._vnode = vnode
// Vue.prototype.__patch__ is injected in entry points
// based on the rendering backend used.
if (!prevVnode) {
// initial render
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
} else {
// updates
vm.$el = vm.__patch__(prevVnode, vnode)
}
restoreActiveInstance()
// update __vue__ reference
if (prevEl) {
prevEl.__vue__ = null
}
if (vm.$el) {
vm.$el.__vue__ = vm
}
//...
}/<code>

_update 其實沒有太複雜邏輯,主要還是在內部調用的 __patch__ 方法,patch 方法內部涉及 diff 對虛擬節點的比較,將在後續章節說明。

總結

編譯版本中出現的 $mount 將對 template 做 AST 解析,往 vm.$options 對象上掛在 render 方法。

之後再運行時 $mount 方法中,會創建 Watch 對象,定義 expOrFn 方法,其就是執行前面的 render 方法,最終交給 update 方法,通過 patch 方法將新老節點進行 diff 對比,返回給頁面最後要顯示的內容。


分享到:


相關文章: