vue的原始碼學習之五——3.資料驅動(Vue 例項掛載的實現)
-
介紹
版本:2.5.17。
我們使用vue-vli建立基於Runtime+Compiler的vue腳手架。
學習文件:https://ustbhuangyi.github.io/vue-analysis/data-driven/mounted.html
-
掛載到DOM
src/core/instance/init.js : 在初始化的最後,檢測到如果有 el 屬性,則呼叫 vm.$mount 方法掛載 vm,掛載的目標就是把模板渲染成最終的 DOM。
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
-
$mount
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
這個方法的實現是和平臺、構建方式都相關的。先來看一下
src/platform/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 }) const mount = Vue.prototype.$mount Vue.prototype.$mount = function ( el?: string | Element, hydrating?: boolean ): Component { el = el && query(el) /* istanbul ignore if */ 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 // resolve template/el and convert to render function if (!options.render) { let template = options.template if (template) { if (typeof template === 'string') { if (template.charAt(0) === '#') { 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 = 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) { /* istanbul ignore if */ 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 = 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') } } } 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
這段程式碼首先快取了原型上的
$mount
方法,再重新定義該方法,我們先來分析這段程式碼。首先,它對el
做了限制,Vue 不能掛載在body
、html
這樣的根節點上。我們會發現該js重新定義了Vue.prototype.$mount方法,而該方法來自於src/platform/web/runtime/index.js :
-
重新定義Vue.prototype.$mount方法
const mount = Vue.prototype.$mount Vue.prototype.$mount = function ( el?: string | Element, hydrating?: boolean ): Component { el = el && query(el) /* istanbul ignore if */ 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 // resolve template/el and convert to render function if (!options.render) { let template = options.template if (template) { if (typeof template === 'string') { if (template.charAt(0) === '#') { 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 = 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) { /* istanbul ignore if */ 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 = 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') } } } return mount.call(this, el, hydrating) }
那是因為,src/platform/web/entry-runtime-with-compiler.js 中的是適用於 Runtime+Compiler 版本的。而src/platform/web/runtime/index.js 中的 Vue.prototype.$mount 方法是適用於 Runtime Only 版本的,
我們可以看到對於Vue.prototype.$mount引數是可以傳遞 字串 和 DOM物件的。
我們來看一下 query 方法 ,src/platform/web/util/index.js:-
將傳入的引數轉為DOM
Vue.prototype.$mount = function ( el?: string | Element, hydrating?: boolean ): Component { el = el && query(el)
我們可以看到對於Vue.prototype.$mount引數是可以傳遞 字串 和 DOM物件的。
我們來看一下 query 方法 :src/platform/web/util/index.js:export function query (el: string | Element): Element { if (typeof el === 'string') { const selected = document.querySelector(el) if (!selected) { process.env.NODE_ENV !== 'production' && warn( 'Cannot find element: ' + el ) return document.createElement('div') } return selected } else { return el } }
這個方法是說如果說是字串,就是用 document.querySelector(el) 方法獲得字串代表的DOM物件,如果發現沒有,就會抱一個錯誤並且返回一個空div。所以 el = el && query(el) 代表的一定是一個DOM
-
不得掛載在body和html上
/* istanbul ignore if */ 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 }
-
判斷是否有render函式
如果沒有定義
render
方法,則會把el
或者template
字串轉換成render
方法。這裡我們要牢記,在 Vue 2.0 版本中,所有 Vue 的元件的渲染最終都需要render
方法,無論我們是用單檔案 .vue 方式開發元件,還是寫了el
或者template
屬性,最終都會轉換成render
方法,那麼這個過程是 Vue 的一個“線上編譯”的過程,它是呼叫compileToFunctions
方法實現的const options = this.$options // resolve template/el and convert to render function if (!options.render) { let template = options.template if (template) { if (typeof template === 'string') { if (template.charAt(0) === '#') { 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 = 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) { /* istanbul ignore if */ 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 = 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') } } }
上文說道el是DOM物件,如果el 是 body 或者 html元素的話就會報錯,那是因為掛載是覆蓋的,如果掛載在body或html上, 那麼整個HTML文件就不對了。 所以我們一般採用的都是掛載在div上的形式。
如果沒有render函式,則獲取template,template可以是#id、模板字串、dom元素,如果沒有template,則獲取el以及其子內容作為模板。 compileToFunctions是對我們最後生成的模板進行解析,生成render函式。
compileToFunctions對生成的模板進行解析
該方法來自於:.src/platform/compiler/index.js,如果我們的例子是:
<div id="app"> <p>{{message}}</p> </div> <script type="text/javascript"> var vm = new Vue({ el: '#app', data: { message: '第一個vue例項' } }) </script>
1.解析template,生成ast。
{ type: 1, tag: 'div', plain: false, parent: undefined, attrs: [{name:'id', value: '"app"'}], attrsList: [{name:'id', value: 'app'}], attrsMap: {id: 'app'}, children: [{ type: 1, tag: 'p', plain: true, parent: ast, attrs: [], attrsList: [], attrsMap: {}, children: [{ expression: "_s(message)", text: "{{message}}", type: 2 }] }
2.對ast進行優化,分析出靜態不變的內容部分,增加了部分屬性:
因為我們這裡只有一個動態的{{message}},所以static和staticRoot都是false。{ type: 1, tag: 'div', plain: false, parent: undefined, attrs: [{name:'id', value: '"app"'}], attrsList: [{name:'id', value: 'app'}], attrsMap: {id: 'app'}, static: false, staticRoot: false, children: [{ type: 1, tag: 'p', plain: true, parent: ast, attrs: [], attrsList: [], attrsMap: {}, static: false, staticRoot: false, children: [{ expression: "_s(message)", text: "{{message}}", type: 2, static: false }] }
3.ast生成render函式和staticRenderFns陣列。
render = function () { with(this){return _c('div',{attrs:{"id":"app"}},[_c('p',[_v(_s(message))])])} }
4.在src/core/instance/render.js中,我們曾經新增過如下多個函式,這裡和render內返回值呼叫一一對應。
Vue.prototype._o = markOnce Vue.prototype._n = toNumber Vue.prototype._s = _toString Vue.prototype._l = renderList Vue.prototype._t = renderSlot Vue.prototype._q = looseEqual Vue.prototype._i = looseIndexOf Vue.prototype._m = renderStatic Vue.prototype._f = resolveFilter Vue.prototype._k = checkKeyCodes Vue.prototype._b = bindObjectProps Vue.prototype._v = createTextVNode Vue.prototype._e = createEmptyVNode Vue.prototype._u = resolveScopedSlots
這裡的staticRenderFns目前是一個空陣列,其實它是用來儲存template中,靜態內容的render,比如我們把例子中的模板改為:
<div id="app"> <p>這是<span>靜態內容</span></p> <p>{{message}}</p> </div>
staticRenderFns就會變為:
staticRenderFns = function () { with(this){return _c('p',[_v("這是"),_c('span',[_v("靜態內容")])])} }
-
呼叫原先原型上的 $mount 方法掛載
mount.call(this, el, hydrating)
我們知道該js儲存了mount = Vue.prototype.mount,然後又重新定義了Vue.prototype.上的方法
該js的最後又呼叫了mount方法。原先原型上的$mount
方法在src/platform/web/runtime/index.js
中定義,之所以這麼設計完全是為了複用,因為它是可以被runtime only
版本的 Vue 直接使用的。// public mount method Vue.prototype.$mount = function ( el?: string | Element, hydrating?: boolean ): Component { el = el && inBrowser ? query(el) : undefined return 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 = 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 {
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) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
hydrating = false
// manually mounted instance, call mounted on self
// mounted is called for render-created child components in its inserted hook
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}
mountComponent 核心就是先呼叫 vm._render 方法先生成虛擬 Node,再例項化一個渲染Watcher,在它的回撥函式中會呼叫 updateComponent 方法,最終呼叫 vm._update 更新 DOM。
1.做DOM物件的快取
vm.$el = el
2.判斷是否有render函式
如果使用者沒有寫render函式,並且template也沒有轉化為render函式,就會生成一個VNode節點,並在生成環境報警告。
if (!vm.$options.render) {
vm.$options.render = createEmptyVNode
if (process.env.NODE_ENV !== 'production') {...}
}
3.例項化一個渲染Watcher。
Watcher 在這裡起到兩個作用, 一個是初始化的時候會執行回撥函式,另一個是當 vm 例項中的監測的資料發生變化的時候執行回撥函式,這塊兒我們會在之後的章節中介紹。.new Watcher傳的引數1.vue例項,2.updateComponent函式,3.空函式, 4.物件,5布林值。
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
hydrating = false
Watcher定義在來自檔案:src/core/observer/watcher.js
constructor (
vm: Component,
// 表示式
expOrFn: string | Function,
// 回撥
cb: Function,
// 配置物件
options?: ?Object,
// 是否渲染Watcher的標準位
isRenderWatcher?: boolean
) {
this.vm = vm
// 如果渲染Watcher為true,則在 vm中新增_watcher
if (isRenderWatcher) {
vm._watcher = this
}
vm._watchers.push(this)
// options
if (options) {
...
} else {
...
}
this.cb = cb
...
// 如果是開發環境就將 expOrFn toString
this.expression = process.env.NODE_ENV !== 'production'
? expOrFn.toString()
: ''
// 將expOrFn函式轉化為getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = function () {}
process.env.NODE_ENV !== 'production' && warn(
`Failed watching path: "${expOrFn}" ` +
'Watcher only accepts simple dot-delimited paths. ' +
'For full control, use a function instead.',
vm
)
}
}
//計算屬性
if (this.computed) {
this.value = undefined
this.dep = new Dep()
} else {
// 呼叫this.get()
this.value = this.get()
}
}
get () {
pushTarget(this)
let value
const vm = this.vm
try {
// 呼叫this.getter,也就是呼叫expOrFn
value = this.getter.call(vm, vm)
} catch (e) {
} finally {
}
我們會把expOrFn也就是updateComponent賦值給this.getter,並且在獲取this.value的值時會呼叫this.get(),進而呼叫了updateComponent。
4.通過watcher回撥函式中會呼叫 updateComponent 方法,最終呼叫 vm._update 更新 DOM。
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
5.函式最後判斷為根節點的時候設定 vm._isMounted 為 true, 表示這個例項已經掛載了,同時執行 mounted 鉤子函式。 這裡注意 vm.$vnode 表示 Vue 例項的父虛擬 Node,所以它為 Null 則表示當前是根 Vue 的例項。
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm