From creation to mount
Look at the demo:
<div id="app">
<input :value="input" @input="update" />
<div>{{output}}</div>
</div>
<script>
const { ref, computed, effect } = Vue
const APP = {
setup() {
const input = ref(0)
const output = computed(function computedEffect() { return input.value + 5})
// computedEffect & effect below is reexecuted
const update = _.debounce(e= > { input.value = e.target.value*1 }, 50)
effect(function callback() {
// Rely on collection
console.log(input.value)
})
return {
input,
output,
update
}
}
}
constapp = Vue.createApp(APP) app.. mount('#app')
</script>
Copy the code
The above code is a simple process for creating a component.
Create an app instance by calling createAPP API and mount the app instance to complete the component mount.
Let’s take a look at Vue3’s internal implementation from create App to mount.
CreateApp API
export const createApp = ((. args) = > {
// 👉 Call the ensureRenderer function to create an app instance
constapp = ensureRenderer().createApp(... args)if (__DEV__) {
injectNativeTagCheck(app)
injectCompilerOptionsCheck(app)
}
// 👉 constructs the original mount method
const { mount } = app
// 👉 override the mount function
app.mount = (containerOrSelector: Element | ShadowRoot | string) :any= > {
/ / check containerOrSelector
const container = normalizeContainer(containerOrSelector)
if(! container)return
The // _component property stores the parent component, so when we call createApp, the component passed in gets the component
const component = app._component
// Determine whether the component meets the conditions
// If component is not functional and there is no render function,
// Without template, we mount the innerHTML of the container as template
if(! isFunction(component) && ! component.render && ! component.template) { component.template = container.innerHTML// Vue2 compatibility processing
if (__COMPAT__ && __DEV__) {
for (let i = 0; i < container.attributes.length; i++) {
const attr = container.attributes[i]
if(attr.name ! = ='v-cloak' && /^(v-|:|@)/.test(attr.name)) {
compatUtils.warnDeprecation(
DeprecationTypes.GLOBAL_MOUNT_CONTAINER,
null
)
break}}}}// Empty the contents of the mount container before performing mount
container.innerHTML = ' '
/ / mounted container
// Execute the mount function, which renders and mounts the component
const proxy = mount(container, false, container instanceof SVGElement)
if (container instanceof Element) {
// Remove the V-cloak command
container.removeAttribute('v-cloak')
// Set the data-v-app command
container.setAttribute('data-v-app'.' ')}return proxy
}
// Return the app instance
return app
}) as CreateAppFunction<Element>
Copy the code
The ensureRenderer function creates an app instance, overwrites the mount method, and returns the app instance when one is called with the createApp ensureRenderer function.
For students with weak foundation, they may not understand the operation of structure mount and then rewrite mount. This is actually related to the way in which the JS traversal is stored, in JS functions are also objects. When overwriting a mount, you are actually pointing it to a new memory address.
The rewritten mount method does a few things:
- call
normalizeContainer
Function to verify the mount container or selector - To obtain
app
Component on the instance, to determine the component, if the condition does not meet, will containerinnerHTML
As atemplate
. - Empty the container
innerHTML
- The implementation structure comes out
mount
Method, hang on the component,mount
Method is responsible for rendering the mount component
Let’s look at the ensureRenderer function that creates app:
// lazy create the renderer - this makes core renderer logic tree-shakable
// in case the user only imports reactivity utilities from Vue.
// Create the renderer inertly - this makes the core renderer logic tree tree-shakable
// In some cases, users will only use Vue's responsive system
let renderer: Renderer<Element> | HydrationRenderer
function ensureRenderer() {
return renderer || (renderer = createRenderer<Node, Element>(rendererOptions))
}
Copy the code
The main reason Vue3 chose to layer the renderer was for tree-shakable, because users would probably only use Vue3’s responsive system.
As you can see from the code, inside is the renderer function created through the createRenderer API.
RendererOptions is a renderer configuration item that contains:
- right
DOM
Of nodes: Insert, move, create, set properties, clone - For node attributes,
class
,style
thepatch
export const render = ((. args) = >{ ensureRenderer().render(... args) })as RootRenderFunction<Element>
Copy the code
The Render API is also created with ensureRenderer.
Now let’s dive into the createRenderer API,
CreateRenderer functions; The renderer is created by calling baseCreateRenderer, and we know from the previous code that baseCreateRenderer returns an object with createAPP, Render,
export function createRenderer<
HostNode = RendererNode.HostElement = RendererElement> (options: RendererOptions<HostNode, HostElement>) {
return baseCreateRenderer<HostNode, HostElement>(options)
}
Copy the code
Let’s take a look at the shortened version of the baseCreateRenderer function, where we’ll look directly at the return result.
BaseCreateRenderer function:
function baseCreateRenderer(options: RendererOptions, createHydrationFns? :typeof createHydrationFunctions
) {
/** * omit some code... * /
const render: RootRenderFunction = (vnode, container, isSVG) = > {
if (vnode == null) {
if (container._vnode) {
unmount(container._vnode, null.null.true)}}else {
patch(container._vnode || null, vnode, container, null.null.null, isSVG)
}
flushPostFlushCbs()
container._vnode = vnode
}
return {
render,
hydrate, // Server render related
createApp: createAppAPI(render, hydrate)
}
}
Copy the code
Render API (createAPP, createAppAPI, createAppAPI, createAppAPI, createAppAPI)
EnsureRenderer indicates that createAppAPI returns a function that is the app instance created by that function. Now comes the key part. Let’s analyze the createAppAPI function.
In fact, I had planned to analyze it directly from the baseCreateRenderer function, but I realized that this analysis might not make sense to people. So with this paper, this paper kind of foreshadores the introduction of baseCreateRenderer’s analysis. The baseCreateRenderer function contains too much information
CreateAppAPI function:
export function createAppAPI<HostElement> (render: RootRenderFunction, hydrate? : RootHydrateFunction) :CreateAppFunction<HostElement> {
// createApp API
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
}
// Create App execution context, which is actually a JS object
const context = createAppContext()
// Plug-in collection
const installedPlugins = new Set(a)let isMounted = false
// App context instance
const app: App = (context.app = {
_uid: uid++,
_component: rootComponent as ConcreteComponent,
_props: rootProps,
_container: null._context: context,
_instance: null,
version,
get config() {
return context.config
},
set config(v) {
if (__DEV__) {
warn(
`app.config cannot be replaced. Modify individual options instead.`)}},// How to configure the plug-in
// During configuration, the system will check whether there is a duplicate. If there is already one, a prompt will be given
// Our plug-in can be a function or an instance of a class with the install method
// If the install type is used, the app instance and options are passed in
// If it is a function, this is passed to the app instance and the options configuration item
use(plugin: Plugin, ... options:any[]) {
if (installedPlugins.has(plugin)) {
__DEV__ && warn(`Plugin has already been applied to target app.`)}else if(plugin && isFunction(plugin.install)) { installedPlugins.add(plugin) plugin.install(app, ... options) }else if(isFunction(plugin)) { installedPlugins.add(plugin) plugin(app, ... options) }else if (__DEV__) {
warn(
`A plugin must either be a function or an object with an "install" ` +
`function.`)}return app
},
/ / with
mixin(mixin: ComponentOptions) {
if (__FEATURE_OPTIONS_API__) {
if(! context.mixins.includes(mixin)) { context.mixins.push(mixin) }else if (__DEV__) {
warn(
'Mixin has already been applied to target app' +
(mixin.name ? ` :${mixin.name}` : ' '))}}else if (__DEV__) {
warn('Mixins are only available in builds supporting Options API')}return app
},
// Configure global components
component(name: string, component? : Component):any {
if (__DEV__) {
validateComponentName(name, context.config)
}
if(! component) {return context.components[name]
}
if (__DEV__ && context.components[name]) {
warn(`Component "${name}" has already been registered in target app.`)
}
context.components[name] = component
return app
},
// Configure the global directive
directive(name: string, directive? : Directive) {
if (__DEV__) {
validateDirectiveName(name)
}
if(! directive) {return context.directives[name] as any
}
if (__DEV__ && context.directives[name]) {
warn(`Directive "${name}" has already been registered in target app.`)
}
context.directives[name] = directive
return app
},
// Mount methodmount( rootContainer: HostElement, isHydrate? :boolean, isSVG? :boolean) :any {
if(! isMounted) {/ / create a Vnode
const vnode = createVNode(
rootComponent as ConcreteComponent,
rootProps
)
// store app context on the root VNode.
// this will be set on the root instance on initial mount.
vnode.appContext = context
// HMR root reload
if (__DEV__) {
context.reload = () = > {
render(cloneVNode(vnode), rootContainer, isSVG)
}
}
if (isHydrate && hydrate) {
hydrate(vnode as VNode<Node, Element>, rootContainer as any)}else {
/ / render vnode
render(vnode, rootContainer, isSVG)
}
/ / finish mounted
isMounted = true
app._container = rootContainer
// for devtools and telemetry; (rootContaineras any).__vue_app__ = app
if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
app._instance = vnode.component
devtoolsInitApp(app, version)
}
// Return component Proxy
returnvnode.component! .proxy }else if (__DEV__) {
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)\``)}},// Uninstall the component
unmount() {
if (isMounted) {
// Vnode === null during uninstallation
render(null, app._container)
if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
app._instance = null
devtoolsUnmountApp(app)
}
delete app._container.__vue_app__
} else if (__DEV__) {
warn(`Cannot unmount an app that is not mounted.`)}},// Assign data to the provides property of the context
provide(key, value) {
if (__DEV__ && (key as string | symbol) in context.provides) {
warn(
`App already provides property with key "The ${String(key)}". ` +
`It will be overwritten with the new value.`)}// TypeScript doesn't allow symbols as index type
// https://github.com/Microsoft/TypeScript/issues/24587
context.provides[key as string] = value
return app
}
})
// Compatible processing
if (__COMPAT__) {
installAppCompatProperties(app, context, render)
}
return app
}
}
Copy the code
The app object contains functions such as use, mixin, Component, directive, mount, unmount, and provide, all of which eventually return the app object, so we can implement chain calls:
app.use().component().mixin().mount()
As you can see from the above code, createAPP ultimately returns an App configuration object. The createAPP API creates a Vnode by calling the createVnode method and the Render method in the baseCreateRenderer function. To complete the Vnode rendering.
When app.mount(‘#app’) is executed in the code, the Vnode will be created and rendered.
The createVnode function returns a Vnode that is the JS description object of a Vnode, which can be found at juejin.cn/post/704248…
CreateAppContext, instance context object:
export function createAppContext() :AppContext {
return {
app: null as any.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()}}Copy the code
conclusion
In the process of analyzing the whole link here, I also wondered why Utah made the link so deep. There are four or five layers of functions between createApp and createApp.
When I looked at some of the source code for the baseCreateRenderer function, it was mainly the separation of the code responsibilities. Let baseCreateRenderer copy Vnode’s patch and render tasks and createApp create the instance.
Use closures in the createApp function to access all methods defined in baseCreateRenderer.
The end of this paper is baseCreateRenderer’s learning analysis, which can fill in a lot of holes.