先看一下vue-next官方文檔的介紹:
每個(gè) Vue 應(yīng)用都是通過用
createApp
函數(shù)創(chuàng)建一個(gè)新的應(yīng)用實(shí)例開始的
傳遞給
createApp
的選項(xiàng)用于配置根組件颇玷。當(dāng)我們掛載應(yīng)用時(shí)铐拐,該組件被用作渲染的起點(diǎn)件相。
一個(gè)應(yīng)用需要被掛載到一個(gè) DOM 元素中箱蝠。例如驰怎,如果我們想把一個(gè) Vue 應(yīng)用掛載到
<div id="app"></div>
吃粒,我們應(yīng)該傳遞#app
我們將分為兩部分進(jìn)行渲染過程的理解:
- 創(chuàng)建應(yīng)用實(shí)例巧娱,函數(shù)
createApp
的剖析 - 應(yīng)用實(shí)例掛載闺金, 函數(shù)
mount
方法掛載過程
本篇詳細(xì)講述調(diào)用方法createApp
過程
創(chuàng)建應(yīng)用實(shí)例 createApp
下面是一個(gè)簡單的demo
<!-- template -->
<div id="app">
<input v-model="value"/>
<p>雙向綁定:{{value}}</p>
<hello-comp person-name="zhangsan"/>
</div>
const { createApp } = Vue
const helloComp = {
name: 'hello-comp',
props: {
personName: {
type: String,
default: 'wangcong'
}
},
template: '<p>hello {{personName}}!</p>'
}
const app = {
data() {
return {
value: '',
info: {
name: 'tom',
age: 18
}
}
},
components: {
'hello-comp': helloComp
},
mounted() {
console.log(this.value, this.info)
},
}
createApp(app).mount('#app')
現(xiàn)在我們從createApp函數(shù)為入口逾滥,去了解應(yīng)用創(chuàng)建的過程。
查看官方文檔和上面的例子我們可以知道败匹,createApp
方法接收的是根組件對象作為參數(shù)寨昙,并返回了一個(gè)有mount
方法的應(yīng)用實(shí)例對象。
按照依賴關(guān)系可以找到createApp
方法出自packages/runtime-dom/src/index.ts
export const createApp = ((...args) => {
const app = ensureRenderer().createApp(...args)
if (__DEV__) {
injectNativeTagCheck(app)
}
const { mount } = app
app.mount = (containerOrSelector: Element | string): any => {
const container = normalizeContainer(containerOrSelector)
if (!container) return
const component = app._component
if (!isFunction(component) && !component.render && !component.template) {
component.template = container.innerHTML
}
// clear content before mounting
container.innerHTML = ''
const proxy = mount(container)
container.removeAttribute('v-cloak')
container.setAttribute('data-v-app', '')
return proxy
}
return app
}) as CreateAppFunction<Element>
這里做了兩件事情:
- 創(chuàng)建
app
應(yīng)用實(shí)例:ensureRenderer().createApp(...args)
- 重寫了
app.mount
方法掀亩。document.querySelector
方法獲取HTMLElement
對象作為參數(shù)傳入原mount
方法舔哪。該部分會在mount
段落詳細(xì)講解。
ensureRenderer
ensureRenderer
函數(shù)的目的是惰性創(chuàng)建renderer
對象槽棍,這樣做目的是在用戶只引入reactivity
模塊時(shí)捉蚤,對renderer核心邏輯部分可以進(jìn)行tree-shake。
renderer
對象包含三個(gè)屬性:
render
方法刹泄、 hydrate
( ssr客戶端激活相關(guān))外里、createApp
方法
renderer
對象實(shí)際上是方法createRenderer
函數(shù)返回的。
// nodeOps: dom節(jié)點(diǎn)增刪改查操作的原生api
const rendererOptions = extend({ patchProp, forcePatchProp }, nodeOps)
let renderer: Renderer<Element> | HydrationRenderer
function ensureRenderer() {
return renderer || (renderer = createRenderer<Node, Element>(rendererOptions))
}
createRenderer
這個(gè)方法在packages/runtime-core/src/renderer.ts
中定義特石。
export function createRenderer<
HostNode = RendererNode,
HostElement = RendererElement
>(options: RendererOptions<HostNode, HostElement>) {
return baseCreateRenderer<HostNode, HostElement>(options)
}
createRenderer
方法接受兩個(gè)通用的類型參數(shù)HostNode
和HostElement
盅蝗。其目的是在自定義渲染器中可以傳入特定于平臺的類型;
例如:
對于瀏覽器環(huán)境runtime-dom
姆蘸,HostNode
將是DOM Node
接口墩莫;HostElement
將是DOM Element
接口。
Element繼承了Node類逞敷,也就是說Element是Node多種類型中的一種狂秦,即當(dāng)NodeType為1時(shí)Node即為ElementNode,另外Element擴(kuò)展了Node推捐,Element擁有id裂问、class、children等屬性。
baseCreateRenderer
這個(gè)方法也在packages/runtime-core/src/renderer.ts
中定義
該方法比較長堪簿,暫時(shí)忽略中間代碼痊乾。該函數(shù)執(zhí)行返回了一個(gè)對象(上文中的renderer
對象):
function baseCreateRenderer(
options: RendererOptions,
createHydrationFns?: typeof createHydrationFunctions
): any {
...
return {
render,
hydrate,
createApp: createAppAPI(render, hydrate)
}
}
-
render
:接受兩個(gè)參數(shù)VNode
,Element
椭更;
const render: RootRenderFunction = (vnode, container) => {
if (vnode == null) {
if (container._vnode) {
unmount(container._vnode, null, null, true)
}
} else {
patch(container._vnode || null, vnode, container)
}
flushPostFlushCbs()
container._vnode = vnode
}
-
hydrate
:與ssr客戶端激活相關(guān) -
createApp
:接收方法createAppAPI(render, hydrate)
的返回值
createAppAPI
這個(gè)方法在packages/runtime-core/src/apiCreateApp.ts
中定義哪审。
在這里createAppAPI
的返回結(jié)果是一個(gè)函數(shù)createApp
,這里終于找到了demo中調(diào)用的那個(gè)接受跟組件對象的createApp
函數(shù)虑瀑。
createApp
返回了應(yīng)用實(shí)例app
對象湿滓,其中包含了我們比較熟悉的一些方法,例如:mixin
舌狗、component
叽奥、directive
等;
export function createAppAPI<HostElement>(
render: RootRenderFunction,
hydrate?: RootHydrateFunction
): CreateAppFunction<HostElement> {
return function createApp(rootComponent, rootProps = null) {
if (rootProps != null && !isObject(rootProps)) {
__DEV__ && warn(`root props passed to app.mount() must be an object.`)
rootProps = null
}
const context = createAppContext()
const installedPlugins = new Set()
let isMounted = false
const app: App = (context.app = {
_uid: uid++,
_component: rootComponent as ConcreteComponent,
_props: rootProps,
_container: null,
_context: context,
version,
get config() {
return context.config
},
set config(v) {
if (__DEV__) {
warn(
`app.config cannot be replaced. Modify individual options instead.`
)
}
},
// 插件注冊
use(plugin: Plugin, ...options: any[]) {
...
return app
},
mixin(mixin: ComponentOptions) {
...
return app
},
// 組件注冊
component(name: string, component?: Component): any {
...
return app
},
// 指令注冊
directive(name: string, directive?: Directive) {
...
return app
},
// dom掛載
mount(rootContainer: HostElement, isHydrate?: boolean): any {
...
return app
},
// 卸載
unmount() {
if (isMounted) {
render(null, app._container)
if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
devtoolsUnmountApp(app)
}
} else if (__DEV__) {
warn(`Cannot unmount an app that is not mounted.`)
}
},
// 注入
provide(key, value) {
...
return app
}
}
return app
}
}
下面就是在應(yīng)用實(shí)例app
還沒有調(diào)用mount
方法進(jìn)行掛載前的屬性:
這里強(qiáng)調(diào)一下app._component
引用的就是我們傳入createApp
的根組件對象
對比
2.x global API:
import Vue from 'vue'
import App from './App.vue'
Vue.config.ignoredElements = [/^app-/]
Vue.use(/* ... */)
Vue.mixin(/* ... */)
Vue.component(/* ... */)
Vue.directive(/* ... */)
Vue.prototype.customProperty = () => {}
new Vue({
render: h => h(App)
}).$mount('#app')
從技術(shù)上講把夸,Vue 2沒有“應(yīng)用”的概念而线。我們定義為應(yīng)用的只是通過創(chuàng)建的根Vue實(shí)例new Vue()
。從同一Vue構(gòu)造函數(shù)創(chuàng)建的每個(gè)根實(shí)例都共享相同的全局配置恋日。
Vue當(dāng)前的某些全局API和配置會永久更改全局狀態(tài)膀篮。這會導(dǎo)致一些問題:
- 全局配置更容易使測試過程中意外污染其他測試案例
- 影響每一個(gè)根實(shí)例
Vue.mixin({ /* ... */ })
const app1 = new Vue({ el: '#app-1' })
const app2 = new Vue({ el: '#app-2' })
vue3 應(yīng)用程序?qū)嵗?/h3>
createApp
返回一個(gè)提供應(yīng)用上下文的應(yīng)用實(shí)例。應(yīng)用實(shí)例掛載的整個(gè)組件樹共享同一個(gè)上下文岂膳。該上下文提供了先前在Vue 2.x中“全局”的配置誓竿。該實(shí)例不會被應(yīng)用于其他實(shí)例的任何全局配置所污染。共享實(shí)例屬性應(yīng)附加到應(yīng)用程序?qū)嵗?code>config.globalProperties
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
app.config.isCustomElement = tag => tag.startsWith('app-')
app.use(/* ... */)
app.mixin(/* ... */)
app.component(/* ... */)
app.directive(/* ... */)
app.provide(/* ... */)
app.config.globalProperties.customProperty = () => {}
app.mount(App, '#app')
完