調(diào)用createApp?時(shí)Vue工作過(guò)程原理
引言
在使用Vue3
時(shí),我們需要使用createApp
來(lái)創(chuàng)建一個(gè)應(yīng)用實(shí)例,然后使用mount
方法將應(yīng)用掛載到某個(gè)DOM
節(jié)點(diǎn)上。
那么在調(diào)用createApp
時(shí),Vue
再背后做了些什么事情呢?今天就來(lái)扒一扒Vue3
的源碼,看看調(diào)用createApp
發(fā)生了些什么。
尋找入口
在上一章中,我們我們已經(jīng)將Vue3
的源碼下載下來(lái)了,并且已經(jīng)知道如何編譯源碼了,先看一下Vue3
的源碼目錄:
packages
目錄下的包就是Vue3
的所有源碼了,編譯之后會(huì)在每個(gè)工程包下面生成一個(gè)dist
目錄,里面就是編譯后的文件。
這里我框出了vue
包,這個(gè)大家都熟悉,打開vue
包下的package.json
文件,可以看到unpkg
字段指向了dist/vue.global.js
文件,這個(gè)文件就是Vue3
的全局版本,我們可以直接在瀏覽器中引入這個(gè)文件來(lái)使用Vue3
。
代碼邏輯基本上都是相同的,用打包后的文件來(lái)分析源碼,可以更加直觀的看到源碼的邏輯,因?yàn)?code>Vue在設(shè)計(jì)的時(shí)候會(huì)考慮其他平臺(tái),如果直接通過(guò)源碼來(lái)查看會(huì)有額外的心智負(fù)擔(dān)。
具體如何使用每個(gè)打包后的文件,可以查看vue
包下的README.md
文件,如果只是想分析源碼,且不想那么麻煩,可以直接使用dist/vue.global.js
文件。
如果想了解Vue3
的目錄結(jié)構(gòu)和模塊劃分可以使用vue.esm-bundler.js
文件,這個(gè)文件是Vue3
的ESM
版本,會(huì)通過(guò)import
來(lái)引入其他模塊,這樣就可以直接看到Vue3
的模塊劃分。
本系列就會(huì)通過(guò)vue.esm-bundler.js
文件來(lái)分析Vue3
的源碼,并且會(huì)通過(guò)邊分析邊動(dòng)手的方式來(lái)學(xué)習(xí)Vue3
的源碼。
使用
我們先來(lái)看一下Vue3
的使用方式:
import {createApp} from 'vue' import App from './App.vue' const app = createApp(App) app.mount('#app')
在Vue3
中,我們需要使用createApp
來(lái)創(chuàng)建一個(gè)應(yīng)用實(shí)例,然后使用mount
方法將應(yīng)用掛載到某個(gè)DOM
節(jié)點(diǎn)上。
createApp
是從vue
包中導(dǎo)出的一個(gè)方法,它接收一個(gè)組件作為參數(shù),然后返回一個(gè)應(yīng)用實(shí)例。
入口 createApp
從vue
的package.json
可以看到,module
字段指向了dist/vue.esm-bundler.js
文件,這個(gè)文件是Vue3
的ESM
版本,我們可以直接使用import
來(lái)引入Vue3
。
而createApp
方法并不在這個(gè)包中,而是在runtime-dom
包中,這個(gè)文件是直接全部導(dǎo)出runtime-dom
包中的內(nèi)容:
export * from '@vue/runtime-dom';
不用懷疑@vue/runtime-dom
指向的就是runtime-dom
包,使用esm
版本就直接找xxx.esm-bundler.js
文件,使用cjs
版本就直接找xxx.cjs.js
文件,后面不會(huì)再提到這個(gè)問(wèn)題。
打開runtime-dom.esm-bundler.js
文件,可以看到createApp
方法:
import { } from '@vue/runtime-core'; export * from '@vue/runtime-core'; import { } from '@vue/shared'; // ... 省略n多代碼 function createApp(...args) { // ... } export {createApp};
可以看到runtime-dom
包中還引用了runtime-core
包和shared
包,現(xiàn)在找到入口文件了,在分析直接可以先搭建一個(gè)簡(jiǎn)單的代碼分析和測(cè)試的環(huán)境,這樣方便自己驗(yàn)證并且可以直接看到代碼的執(zhí)行結(jié)果。
demo
環(huán)境可以直接在本地搭建,也可以使用codesandbox
、stackblitz
等在線環(huán)境,這里使用codesandbox
,后續(xù)demo
的代碼都會(huì)放在codesandbox
上,文末會(huì)有鏈接。
當(dāng)然大家也可以直接在本地搭建一個(gè)demo
環(huán)境,這里就不再贅述了。
源碼分析
上面的環(huán)境都準(zhǔn)備好了之后就可以直接開始分析Vue3
的源碼了,我們先來(lái)看一下createApp
方法的實(shí)現(xiàn);
createApp
const createApp = (...args) => { const app = ensureRenderer().createApp(...args); const {mount} = app; app.mount = (containerOrSelector) => { // ... }; return app; }
createApp
方法接收一個(gè)組件作為參數(shù),然后調(diào)用ensureRenderer
方法;
這個(gè)方法的作用是確保渲染器存在,如果不存在就創(chuàng)建一個(gè)渲染器,然后調(diào)用渲染器的createApp
方法,這個(gè)方法的作用是創(chuàng)建一個(gè)應(yīng)用實(shí)例,然后將這個(gè)應(yīng)用實(shí)例返回,相當(dāng)于一個(gè)單例模式。
let renderer; const ensureRenderer = () => renderer || (renderer = createRenderer(rendererOptions));
這里的rendererOptions
是一些渲染器的配置,主要的作用是用來(lái)操作DOM
的,這里不做過(guò)多的介紹,后面會(huì)有專門的文章來(lái)介紹。
現(xiàn)在先簡(jiǎn)單的來(lái)認(rèn)識(shí)一下rendererOptions
,這個(gè)里面會(huì)有兩個(gè)方法后面會(huì)用到:
const rendererOptions = { insert: (child, parent, anchor) => { parent.insertBefore(child, anchor || null); }, createText: text => document.createTextNode(text), }
現(xiàn)在我們先簡(jiǎn)單的動(dòng)手實(shí)現(xiàn)一下createApp
方法,新建一個(gè)runtime-dom.js
文件,然后內(nèi)容如下:
import { createRenderer } from "./runtime-core"; const createApp = (...args) => { const rendererOptions = { insert: (child, parent, anchor) => { parent.insertBefore(child, anchor || null); }, createText: (text) => document.createTextNode(text) }; const app = createRenderer(rendererOptions).createApp(...args); const { mount } = app; app.mount = (containerOrSelector) => { //...后面分析再補(bǔ)上 }; return app; }; export { createApp };
現(xiàn)在可以看到我們?cè)趯?shí)現(xiàn)createApp
方法的時(shí)候,直接調(diào)用了createRenderer
方法,這個(gè)方法是創(chuàng)建渲染器的方法,這個(gè)方法的實(shí)現(xiàn)在runtime-core
包中;
所以我們需要補(bǔ)上runtime-core
包中的createRenderer
方法的實(shí)現(xiàn);
createRenderer
createRenderer
源碼實(shí)現(xiàn)如下:
function createRenderer(options) { return baseCreateRenderer(options); } // implementation function baseCreateRenderer(options, createHydrationFns) { // 省略 n 多代碼,都是函數(shù)定義,并會(huì)立即執(zhí)行,暫時(shí)對(duì)結(jié)果不會(huì)有影響 return { render, hydrate, createApp: createAppAPI(render, hydrate) }; }
createRenderer
內(nèi)部返回baseCreateRenderer
方法的執(zhí)行結(jié)果,這個(gè)方法的作用會(huì)返回render
、hydrate
、createApp
三個(gè)方法;
而我們最后需要調(diào)用的createApp
方法就是在這三個(gè)方法中的其中一個(gè),而createApp
方法的是通過(guò)createAppAPI
方法創(chuàng)建的,同時(shí)剩下的兩個(gè)方法render
和hydrate
也是在createAppAPI
方法中被調(diào)用的,所以我們還需要看一下createAppAPI
方法的實(shí)現(xiàn);
createAppAPI
createAppAPI
方法的實(shí)現(xiàn)如下:
function createAppContext() { return { app: null, config: { isNativeTag: NO, performance: false, globalProperties: {}, optionMergeStrategies: {}, errorHandler: undefined, warnHandler: undefined, compilerOptions: {} }, mixins: [], components: {}, directives: {}, provides: Object.create(null), optionsCache: new WeakMap(), propsCache: new WeakMap(), emitsCache: new WeakMap() }; } // 這個(gè)變量是用來(lái)統(tǒng)計(jì)創(chuàng)建的應(yīng)用實(shí)例的個(gè)數(shù) let uid$1 = 0; function createAppAPI(render, hydrate) { // 返回一個(gè)函數(shù),這里主要是通過(guò)閉包來(lái)緩存上面?zhèn)魅氲膮?shù) return function createApp(rootComponent, rootProps = null) { // rootComponent 就是我們傳入的根組件,這里會(huì)做一些校驗(yàn) // 如果傳遞的不是一個(gè)函數(shù),那么就做一個(gè)淺拷貝 if (!isFunction(rootComponent)) { rootComponent = Object.assign({}, rootComponent); } // rootProps 就是我們傳入的根組件的 props,這個(gè)參數(shù)必須是一個(gè)對(duì)象 if (rootProps != null && !isObject(rootProps)) { (process.env.NODE_ENV !== 'production') && warn(`root props passed to app.mount() must be an object.`); rootProps = null; } // 創(chuàng)建上下文對(duì)象,在上面定義,就是返回一個(gè)對(duì)象 const context = createAppContext(); // 通過(guò) use 創(chuàng)建的插件都存在這里 const installedPlugins = new Set(); // 是否已經(jīng)掛載 let isMounted = false; // 創(chuàng)建 app 對(duì)象 const app = (context.app = { _uid: uid$1++, _component: rootComponent, _props: rootProps, _container: null, _context: context, _instance: null, version, get config() { // ... }, set config(v) { // ... }, use(plugin, ...options) { // ... }, mixin(mixin) { // ... }, component(name, component) { // ... }, directive(name, directive) { // ... }, mount(rootContainer, isHydrate, isSVG) { // ... }, unmount() { // ... }, provide(key, value) { // ... } }); // 返回 app 對(duì)象 return app; }; }
看到這里,我們就可以知道,createApp
方法的實(shí)現(xiàn)其實(shí)就是在createAppAPI
方法中返回一個(gè)函數(shù),這個(gè)函數(shù)就是createApp
方法;
這個(gè)方法并沒有多么特殊,就是返回了一堆對(duì)象,這些對(duì)象就是我們?cè)谑褂?code>createApp方法時(shí),可以調(diào)用的方法;
這里可以看到我們常用的use
、mixin
、component
、directive
、mount
、unmount
、provide
等方法都是在app
對(duì)象上的,也是通過(guò)這個(gè)函數(shù)制造并返回的;
現(xiàn)在我們繼續(xù)完善我們的學(xué)習(xí)demo
代碼,現(xiàn)在新建一個(gè)runtime-core.js
文件夾,然后把上面的代碼復(fù)制進(jìn)去;
但是我們不能全都都直接照搬,上面的對(duì)象這么多的屬性我們只需要保留mount
,因?yàn)檫€需要掛載才能看到效果,demo
代碼如下:
function createRenderer(options) { // 先省略 render 和 hydrate 方法的實(shí)現(xiàn),后面會(huì)講到 return { render, hydrate, createApp: createAppAPI(render, hydrate) }; } function createAppAPI(render, hydrate) { return function createApp(rootComponent, rootProps = null) { // 省略參數(shù)校驗(yàn) rootComponent = Object.assign({}, rootComponent); // 省略上下文的創(chuàng)建 const context = { app: null } // 忽略其他函數(shù)的實(shí)現(xiàn),只保留 mount 函數(shù)和私有變量 let isMounted = false; const app = (context.app = { _uid: uid$1++, _component: rootComponent, _props: rootProps, _container: null, _context: context, _instance: null, mount(rootContainer, isHydrate, isSVG) { // ... }, }); return app; }; }
這樣我們就完成了createApp
函數(shù)的簡(jiǎn)化版實(shí)現(xiàn),接下來(lái)我們就可以開始掛載了;
mount 掛載
上面我們已經(jīng)學(xué)習(xí)到了createApp
函數(shù)的實(shí)現(xiàn),現(xiàn)在還需要通過(guò)mount
方法來(lái)掛載我們的根組件,才能驗(yàn)證我們的demo
代碼是否正確;
我們?cè)谡{(diào)用createApp
方法時(shí),會(huì)返回一個(gè)app
對(duì)象,這個(gè)對(duì)象上有一個(gè)mount
方法,我們需要通過(guò)這個(gè)方法來(lái)掛載我們的根組件;
在這之前,我們看到了createApp
的實(shí)現(xiàn)中重寫了mount
方法,如下:
const createApp = (...args) => { // ...省略其他代碼 // 備份 mount 方法 const { mount } = app; // 重寫 mount 方法 app.mount = (containerOrSelector) => { // 獲取掛載的容器 const container = normalizeContainer(containerOrSelector); if (!container) return; // _component 指向的是 createApp 傳入的根組件 const component = app._component; // 驗(yàn)證根組件是否是一個(gè)對(duì)象,并且有 render 和 template 兩個(gè)屬性之一 if (!isFunction(component) && !component.render && !component.template) { // __UNSAFE__ // Reason: potential execution of JS expressions in in-DOM template. // The user must make sure the in-DOM template is trusted. If it's // rendered by the server, the template should not contain any user data. // 確保模板是可信的,因?yàn)槟0蹇赡軙?huì)有 JS 表達(dá)式,具體可以翻譯上面的注釋 component.template = container.innerHTML; } // clear content before mounting // 掛載前清空容器 container.innerHTML = ''; // 正式掛載 const proxy = mount(container, false, container instanceof SVGElement); // 掛載完成 if (container instanceof Element) { // 清除容器的 v-cloak 屬性,這也就是我們經(jīng)??吹降?v-cloak 的作用 container.removeAttribute('v-cloak'); // 設(shè)置容器的 data-v-app 屬性 container.setAttribute('data-v-app', ''); } // 返回根組件的實(shí)例 return proxy; }; return app; }
上面重寫的mount
方法中,其實(shí)最主要的做的是三件事:
- 獲取掛載的容器
- 調(diào)用原本的
mount
方法掛載根組件 - 為容器設(shè)置
vue
的專屬屬性
現(xiàn)在到我們動(dòng)手實(shí)現(xiàn)一個(gè)簡(jiǎn)易版的mount
方法了;
// 備份 mount 方法 const { mount } = app; // 重寫 mount 方法 app.mount = (containerOrSelector) => { // 獲取掛載的容器 const container = document.querySelector(containerOrSelector); if (!container) return; const component = app._component; container.innerHTML = ''; // 正式掛載 return mount(container, false, container instanceof SVGElement); };
這里的掛載其實(shí)還是使用的是createApp
函數(shù)中的mount
方法,我們可以看到mount
方法的實(shí)現(xiàn)如下:
function mount(rootContainer, isHydrate, isSVG) { // 判斷是否已經(jīng)掛載 if (!isMounted) { // 這里的 #5571 是一個(gè) issue 的 id,可以在 github 上搜索,這是一個(gè)在相同容器上重復(fù)掛載的問(wèn)題,這里只做提示,不做處理 // #5571 if ((process.env.NODE_ENV !== 'production') && rootContainer.__vue_app__) { warn(`There is already an app instance mounted on the host container.\n` + ` If you want to mount another app on the same host container,` + ` you need to unmount the previous app by calling `app.unmount()` first.`); } // 通過(guò)在 createApp 中傳遞的參數(shù)來(lái)創(chuàng)建虛擬節(jié)點(diǎn) const vnode = createVNode(rootComponent, rootProps); // store app context on the root VNode. // this will be set on the root instance on initial mount. // 上面有注釋,在根節(jié)點(diǎn)上掛載 app 上下文,這個(gè)上下文會(huì)在掛載時(shí)設(shè)置到根實(shí)例上 vnode.appContext = context; // HMR root reload // 熱更新 if ((process.env.NODE_ENV !== 'production')) { context.reload = () => { render(cloneVNode(vnode), rootContainer, isSVG); }; } // 通過(guò)其他的方式掛載,這里不一定指代的是服務(wù)端渲染,也可能是其他的方式 // 這一塊可以通過(guò)創(chuàng)建渲染器的源碼可以看出,我們?nèi)粘T诳蛻舳虽秩?,不?huì)使用到這一塊,這里只是做提示,不做具體的分析 if (isHydrate && hydrate) { hydrate(vnode, rootContainer); } // 其他情況下,直接通過(guò) render 函數(shù)掛載 // render 函數(shù)在 createRenderer 中定義,傳遞到 createAppAPI 中,通過(guò)閉包緩存下來(lái)的 else { render(vnode, rootContainer, isSVG); } // 掛載完成后,設(shè)置 isMounted 為 true isMounted = true; // 設(shè)置 app 實(shí)例的 _container 屬性,指向掛載的容器 app._container = rootContainer; // 掛載的容器上掛載 app 實(shí)例,也就是說(shuō)我們可以通過(guò)容器找到 app 實(shí)例 rootContainer.__vue_app__ = app; // 非生產(chǎn)環(huán)境默認(rèn)開啟 devtools,也可以通過(guò)全局配置來(lái)開啟或關(guān)閉 // __VUE_PROD_DEVTOOLS__ 可以通過(guò)自己使用的構(gòu)建工具來(lái)配置,這里只做提示 if ((process.env.NODE_ENV !== 'production') || __VUE_PROD_DEVTOOLS__) { app._instance = vnode.component; devtoolsInitApp(app, version); } // 返回 app 實(shí)例,這里不做具體的分析 return getExposeProxy(vnode.component) || vnode.component.proxy; } // 如果已經(jīng)掛載過(guò)則輸出提示消息,在非生產(chǎn)環(huán)境下 else if ((process.env.NODE_ENV !== 'production')) { warn(`App has already been mounted.\n` + `If you want to remount the same app, move your app creation logic ` + `into a factory function and create fresh app instances for each ` + `mount - e.g. `const createMyApp = () => createApp(App)``); } }
通過(guò)上面的一通分析,其實(shí)掛載主要就是用的兩個(gè)函數(shù)將內(nèi)容渲染到容器中;
- createVNode 創(chuàng)建虛擬節(jié)點(diǎn)
- render 渲染虛擬節(jié)點(diǎn)
我們這里就實(shí)現(xiàn)一個(gè)簡(jiǎn)易版的mount
函數(shù),來(lái)模擬掛載過(guò)程,代碼如下:
function mount(rootContainer, isHydrate) { // createApp 中傳遞的參數(shù)在我們這里肯定是一個(gè)對(duì)象,所以這里不做創(chuàng)建虛擬節(jié)點(diǎn)的操作,而是模擬一個(gè)虛擬節(jié)點(diǎn) const vnode = { type: rootComponent, children: [], component: null, } // 通過(guò) render 函數(shù)渲染虛擬節(jié)點(diǎn) render(vnode, rootContainer); // 返回 app 實(shí)例 return vnode.component }
虛擬節(jié)點(diǎn)
虛擬節(jié)點(diǎn)在Vue
中已經(jīng)是非常常見的概念了,其實(shí)就是一個(gè)js
對(duì)象,包含了dom
的一些屬性,比如tag
、props
、children
等等;
在Vue3
中維護(hù)了一套自己的虛擬節(jié)點(diǎn),大概信息如下:
export interface VNode { __v_isVNode: true; __v_skip: true; type: VNodeTypes; props: VNodeProps | null; key: Key | null; ref: Ref<null> | null; scopeId: string | null; children: VNodeNormalizedChildren; component: ComponentInternalInstance | null; suspense: SuspenseBoundary | null; dirs: DirectiveBinding[] | null; transition: TransitionHooks<null> | null; el: RendererElement | null; anchor: RendererNode | null; target: RendererNode | null; targetAnchor: RendererNode | null; staticCount: number; shapeFlag: ShapeFlags; patchFlag: number; dynamicProps: string[] | null; dynamicChildren: VNode[] | null; appContext: AppContext | null; }
完整的type
信息太多,這里就只貼VNode
的相關(guān)定義,而且這些在Vue
的實(shí)現(xiàn)中也沒有那么簡(jiǎn)單,這一章不做具體的分析,只是做一個(gè)簡(jiǎn)單的概念介紹;
render
render
函數(shù)是在講createRenderer
的時(shí)候出現(xiàn)的,是在baseCreateRenderer
中定義的,具體源碼如下:
function baseCreateRenderer(options, createHydrationFns) { // ... // 創(chuàng)建 render 函數(shù) const render = (vnode, container, isSVG) => { // 如果 vnode 不存在,并且容器是發(fā)生過(guò)渲染,那么將執(zhí)行卸載操作 if (vnode == null) { // container._vnode 指向的是上一次渲染的 vnode,在這個(gè)函數(shù)的最后一行 if (container._vnode) { unmount(container._vnode, null, null, true); } } // 執(zhí)行 patch 操作,這里不做具體的分析,牽扯太大,后面會(huì)單獨(dú)講 else { patch(container._vnode || null, vnode, container, null, null, null, isSVG); } // 刷新任務(wù)隊(duì)列,通常指代的是各種回調(diào)函數(shù),比如生命周期函數(shù)、watcher、nextTick 等等 // 這里不做具體的分析,后面會(huì)單獨(dú)講 flushPreFlushCbs(); flushPostFlushCbs(); // 記錄 vnode,現(xiàn)在的 vnode 已經(jīng)是上一次渲染的 vnode 了 container._vnode = vnode; }; // ... return { render, hydrate, createApp: createAppAPI(render, hydrate) }; }
render
函數(shù)的主要作用就是將虛擬節(jié)點(diǎn)渲染到容器中,unmount
函數(shù)用來(lái)卸載容器中的內(nèi)容,patch
函數(shù)用來(lái)更新容器中的內(nèi)容;
現(xiàn)在來(lái)實(shí)現(xiàn)一個(gè)簡(jiǎn)易版的render
函數(shù):
const render = (vnode, container) => { patch(container._vnode || null, vnode, container); // 記錄 vnode,現(xiàn)在的 vnode 已經(jīng)是上一次渲染的 vnode 了 container._vnode = vnode; }
unmount
函數(shù)不是我們這次主要學(xué)習(xí)的內(nèi)容,所以這里不做具體的分析;
patch
函數(shù)是Vue
中最核心的函數(shù),這次也不做具體的分析,后面會(huì)單獨(dú)講,但是要驗(yàn)證我們這次的學(xué)習(xí)成果,所以我們需要一個(gè)只有掛載功能的patch
函數(shù),這里我們就自己實(shí)現(xiàn)一個(gè)簡(jiǎn)單的patch
函數(shù);
patch
patch
函數(shù)的主要作用就是將虛擬節(jié)點(diǎn)渲染到容器中,patch
函數(shù)也是在baseCreateRenderer
中定義的;
patch
函數(shù)這次就不看了,因?yàn)閮?nèi)部的實(shí)現(xiàn)會(huì)牽扯到非常多的內(nèi)容,這次只是它的出現(xiàn)只是走個(gè)過(guò)場(chǎng),后面會(huì)單獨(dú)講;
我們這次的目的只是驗(yàn)證我們這次源碼學(xué)習(xí)的成成果,所以我們只需要一個(gè)只有掛載功能的patch
函數(shù),這里我們就自己實(shí)現(xiàn)一個(gè)簡(jiǎn)單的patch
函數(shù);
// options 是在創(chuàng)建渲染器的時(shí)候傳入的,還記得在 createApp 的實(shí)現(xiàn)中,我們傳入了一個(gè)有 insert 和 createText 方法的對(duì)象嗎?不記得可以往上翻翻 const { insert: hostInsert, createText: hostCreateText} = options; // Note: functions inside this closure should use `const xxx = () => {}` // style in order to prevent being inlined by minifiers. /** * 簡(jiǎn)易版的實(shí)現(xiàn),只是刪除了一些不必要的邏輯 * @param n1 上一次渲染的 vnode * @param n2 當(dāng)前需要渲染的 vnode * @param container 容器 * @param anchor 錨點(diǎn), 用來(lái)標(biāo)記插入的位置 */ const patch = (n1, n2, container, anchor = null) => { // 上一次渲染的 vnode 和當(dāng)前需要渲染的 vnode 是同一個(gè) vnode,那么就不需要做任何操作 if (n1 === n2) { return; } // 獲取當(dāng)前需要渲染的 vnode 的類型 const { type } = n2; switch (type) { // 如果是文本節(jié)點(diǎn),那么就直接創(chuàng)建文本節(jié)點(diǎn),然后插入到容器中 case Text: processText(n1, n2, container, anchor); break; // 還會(huì)有其他的類型,這里不做具體的分析,后面會(huì)單獨(dú)講 // 其他的情況也會(huì)有很多種情況,這里統(tǒng)一當(dāng)做是組件處理 default: processComponent(n1, n2, container, anchor); } };
patch
函數(shù)的主要作用就是將虛擬節(jié)點(diǎn)正確的渲染到容器中,這里我們只實(shí)現(xiàn)了文本節(jié)點(diǎn)和組件的渲染,其他的類型的節(jié)點(diǎn),后面會(huì)單獨(dú)講;
而我們?cè)谑褂?code>createApp的時(shí)候,通常會(huì)傳入一個(gè)根組件,這個(gè)根組件就會(huì)走到processComponent
函數(shù)中;
所以我們這里還需要實(shí)現(xiàn)了一個(gè)簡(jiǎn)單的processComponent
函數(shù);
const processComponent = (n1, n2, container, anchor) => { if (n1 == null) { mountComponent(n2, container, anchor); } // else { // updateComponent(n1, n2, optimized); // } };
processComponent
函數(shù)也是定義在baseCreateRenderer
中的,這里還是和patch
函數(shù)一樣,只是實(shí)現(xiàn)了一個(gè)簡(jiǎn)單的功能,后面會(huì)單獨(dú)講;
processComponent
函數(shù)做了兩件事,一個(gè)是掛載組件,一個(gè)是更新組件,這里我們只實(shí)現(xiàn)了掛載組件的功能;
掛載組件是通過(guò)mountComponent
函數(shù)實(shí)現(xiàn)的,這個(gè)函數(shù)也是定義在baseCreateRenderer
中的,但是我們這次就不再繼續(xù)深入內(nèi)部調(diào)用了,直接實(shí)現(xiàn)一個(gè)簡(jiǎn)易的:
const mountComponent = (initialVNode, container, anchor) => { // 通過(guò)調(diào)用組件的 render 方法,獲取組件的 vnode const subTree = initialVNode.type.render.call(null); // 將組件的 vnode 渲染到容器中,直接調(diào)用 patch 函數(shù) patch(null, subTree, container, anchor); };
這樣我們就實(shí)現(xiàn)了一個(gè)簡(jiǎn)易版的掛載組件的功能,這里我們只是簡(jiǎn)單的調(diào)用了組件的render
方法,render
方法會(huì)返回一個(gè)vnode
,然后調(diào)用patch
函數(shù)將vnode
渲染到容器中;
現(xiàn)在回頭看看patch
函數(shù),還差一個(gè)processText
函數(shù)沒有實(shí)現(xiàn),這個(gè)函數(shù)也是定義在baseCreateRenderer
中的,這個(gè)比較簡(jiǎn)單,下面的代碼就是實(shí)現(xiàn)的processText
函數(shù):
const processText = (n1, n2, container, anchor) => { if (n1 == null) { hostInsert((n2.el = hostCreateText(n2.children)), container, anchor); } // else { // const el = (n2.el = n1.el); // if (n2.children !== n1.children) { // hostSetText(el, n2.children); // } // } };
我這里屏蔽掉了更新的操作,這里只管掛載,這里的hostInsert
和hostCreateText
函數(shù)就是在我們實(shí)現(xiàn)簡(jiǎn)易patch
函數(shù)的時(shí)候,在patch
函數(shù)實(shí)現(xiàn)的上面,通過(guò)解構(gòu)賦值獲取的,沒印象可以回去看看;
驗(yàn)證
現(xiàn)在我們已經(jīng)實(shí)現(xiàn)了一個(gè)簡(jiǎn)易版的createApp
函數(shù),并且我們可以通過(guò)createApp
函數(shù)創(chuàng)建一個(gè)應(yīng)用,然后通過(guò)mount
方法將應(yīng)用掛載到容器中;
我們可以通過(guò)下面的代碼來(lái)驗(yàn)證一下:
import { createApp } from "./runtime-dom"; const app = createApp({ render() { return { type: "Text", children: "hello world" }; } }); app.mount("#app");
源碼在codesandbox
上面,可以直接查看:codesandbox.io/s/gallant-s…
總結(jié)
我們通過(guò)閱讀Vue3
的源碼,了解了Vue3
的createApp
函數(shù)的實(shí)現(xiàn),createApp
函數(shù)是Vue3
的入口函數(shù),通過(guò)createApp
函數(shù)我們可以創(chuàng)建一個(gè)應(yīng)用;
createApp
的實(shí)現(xiàn)是借助了createRenderer
函數(shù),createRenderer
的實(shí)現(xiàn)就是包裝了baseCreateRenderer
;
baseCreateRenderer
函數(shù)是一個(gè)工廠函數(shù),通過(guò)baseCreateRenderer
函數(shù)我們可以創(chuàng)建一個(gè)渲染器;
baseCreateRenderer
函數(shù)接收一個(gè)options
對(duì)象,這個(gè)options
對(duì)象中包含了一些渲染器的配置,比如insert
、createText
等;
這些配置是在runtime-dom
中實(shí)現(xiàn)的,runtime-dom
中的createApp
函數(shù)會(huì)將這些配置透?jìng)鬟f給baseCreateRenderer
函數(shù),然后baseCreateRenderer
函數(shù)會(huì)返回一個(gè)渲染器,這個(gè)渲染器中有一個(gè)函數(shù)就是createApp
;
createApp
函數(shù)接收一個(gè)組件,然后返回一個(gè)應(yīng)用,這個(gè)應(yīng)用中有一個(gè)mount
方法,這個(gè)mount
方法就是用來(lái)將應(yīng)用掛載到容器中的;
在createApp
中重寫了mount
方法,內(nèi)部的實(shí)現(xiàn)是通過(guò)調(diào)用渲染器的mount
方法;
這個(gè)mount
方法是在baseCreateRenderer
函數(shù)中實(shí)現(xiàn)的,baseCreateRenderer
函數(shù)中的mount
方法會(huì)調(diào)用patch
函數(shù);
patch
函數(shù)內(nèi)部會(huì)做很多的事情,雖然我們這里只實(shí)現(xiàn)了掛載的邏輯,但是也是粗窺了patch
函數(shù)的內(nèi)部一些邏輯;
最后我們實(shí)現(xiàn)了一個(gè)精簡(jiǎn)版的createApp
函數(shù),通過(guò)這個(gè)函數(shù)我們可以創(chuàng)建一個(gè)應(yīng)用,然后通過(guò)mount
方法將應(yīng)用掛載到容器中,這個(gè)過(guò)程中我們也了解了Vue3
的一些實(shí)現(xiàn)細(xì)節(jié);
以上就是調(diào)用createApp 時(shí)Vue工作過(guò)程原理的詳細(xì)內(nèi)容,更多關(guān)于Vue調(diào)用createApp 的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
vue使用原生js實(shí)現(xiàn)滾動(dòng)頁(yè)面跟蹤導(dǎo)航高亮的示例代碼
這篇文章主要介紹了vue使用原生js實(shí)現(xiàn)滾動(dòng)頁(yè)面跟蹤導(dǎo)航高亮的示例代碼,滾動(dòng)頁(yè)面指定區(qū)域?qū)Ш礁吡?。小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-10-10Vue項(xiàng)目打包部署到apache服務(wù)器的方法步驟
這篇文章主要介紹了Vue項(xiàng)目打包部署到apache服務(wù)器,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2021-02-02vue3使用vueup/vue-quill富文本、并限制輸入字?jǐn)?shù)的方法處理
這篇文章主要介紹了vue3使用vueup/vue-quill富文本、并限制輸入字?jǐn)?shù),本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-03-03Vue通過(guò)v-for實(shí)現(xiàn)年份自動(dòng)遞增
這篇文章主要為大家詳細(xì)介紹了Vue通過(guò)v-for實(shí)現(xiàn)年份自動(dòng)遞增,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-09-09vue3+vite+ts?通過(guò)svg-sprite-loader?插件使用自定義圖標(biāo)的詳細(xì)步驟
這篇文章主要介紹了vue3+vite+ts通過(guò)svg-sprite-loader插件使用自定義圖標(biāo),本文分步驟給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-09-09Vue 封裝防刷新考試倒計(jì)時(shí)組件的實(shí)現(xiàn)
這篇文章主要介紹了Vue 封裝防刷新考試倒計(jì)時(shí)組件的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-06-06Vue 權(quán)限控制的兩種方法(路由驗(yàn)證)
這篇文章主要介紹了Vue 權(quán)限控制的兩種方法(路由驗(yàn)證),每種方法給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2019-08-08vue實(shí)現(xiàn)token過(guò)期自動(dòng)跳轉(zhuǎn)到登錄頁(yè)面
本文主要介紹了vue實(shí)現(xiàn)token過(guò)期自動(dòng)跳轉(zhuǎn)到登錄頁(yè)面,文中通過(guò)示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-10-10