wip: root mount api compat

This commit is contained in:
Evan You 2021-04-05 11:54:35 -04:00
parent 24850a99c6
commit e2d6ff845b
7 changed files with 228 additions and 30 deletions

View File

@ -2,19 +2,28 @@ import {
ConcreteComponent,
Data,
validateComponentName,
Component
Component,
createComponentInstance,
setupComponent,
finishComponentSetup
} from './component'
import { ComponentOptions } from './componentOptions'
import { ComponentPublicInstance } from './componentPublicInstance'
import { Directive, validateDirectiveName } from './directives'
import { RootRenderFunction } from './renderer'
import { InjectionKey } from './apiInject'
import { isFunction, NO, isObject } from '@vue/shared'
import { warn } from './warning'
import { createVNode, cloneVNode, VNode } from './vnode'
import { RootHydrateFunction } from './hydration'
import { devtoolsInitApp, devtoolsUnmountApp } from './devtools'
import { version } from '.'
import {
isFunction,
NO,
isObject,
warnDeprecation,
DeprecationTypes
} from '@vue/shared'
export interface App<HostElement = any> {
version: string
@ -39,6 +48,11 @@ export interface App<HostElement = any> {
_props: Data | null
_container: HostElement | null
_context: AppContext
/**
* @internal 2.x compat only
*/
_createRoot?(options: ComponentOptions): ComponentPublicInstance
}
export type OptionMergeFunction = (
@ -298,6 +312,129 @@ export function createAppAPI<HostElement>(
}
})
if (__COMPAT__) {
/**
* Vue 2 supports the behavior of creating a component instance but not
* mounting it, which is no longer possible in Vue 3 - this internal
* function simulates that behavior.
*/
app._createRoot = options => {
const vnode = createVNode(
rootComponent as ConcreteComponent,
options.propsData || null
)
vnode.appContext = context
const hasNoRender =
!isFunction(rootComponent) &&
!rootComponent.render &&
!rootComponent.template
const emptyRender = () => {}
// create root instance
const instance = createComponentInstance(vnode, null, null)
// suppress "missing render fn" warning since it can't be determined
// until $mount is called
if (hasNoRender) {
instance.render = emptyRender
}
setupComponent(instance, __NODE_JS__)
vnode.component = instance
// $mount & $destroy
// these are defined on ctx and picked up by the $mount/$destroy
// public property getters on the instance proxy.
// Note: the following assumes DOM environment since the compat build
// only targets web. It essentially includes logic for app.mount from
// both runtime-core AND runtime-dom.
instance.ctx._compat_mount = (selectorOrEl: string | Element) => {
if (isMounted) {
__DEV__ && warn(`Root instance is already mounted.`)
return
}
let container: Element
if (typeof selectorOrEl === 'string') {
// eslint-disable-next-line
const result = document.querySelector(selectorOrEl)
if (!result) {
__DEV__ &&
warn(
`Failed to mount root instance: selector "${selectorOrEl}" returned null.`
)
return
}
container = result
} else {
if (!selectorOrEl) {
__DEV__ &&
warn(
`Failed to mount root instance: invalid mount target ${selectorOrEl}.`
)
return
}
container = selectorOrEl
}
const isSVG = container instanceof SVGElement
// HMR root reload
if (__DEV__) {
context.reload = () => {
const cloned = cloneVNode(vnode)
// compat mode will use instance if not reset to null
cloned.component = null
render(cloned, container, isSVG)
}
}
// resolve in-DOM template if component did not provide render
// and no setup/mixin render functions are provided (by checking
// that the instance is still using the placeholder render fn)
if (hasNoRender && instance.render === emptyRender) {
// root directives check
if (__DEV__) {
for (let i = 0; i < container.attributes.length; i++) {
const attr = container.attributes[i]
if (attr.name !== 'v-cloak' && /^(v-|:|@)/.test(attr.name)) {
warnDeprecation(DeprecationTypes.DOM_TEMPLATE_MOUNT)
break
}
}
}
instance.render = null
;(rootComponent as ComponentOptions).template = container.innerHTML
finishComponentSetup(instance, __NODE_JS__, true /* skip options */)
}
// clear content before mounting
container.innerHTML = ''
// TODO hydration
render(vnode, container, isSVG)
if (container instanceof Element) {
container.removeAttribute('v-cloak')
container.setAttribute('data-v-app', '')
}
isMounted = true
app._container = container
// for devtools and telemetry
;(container as any).__vue_app__ = app
if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
devtoolsInitApp(app, version)
}
return instance.proxy!
}
instance.ctx._compat_destroy = app.unmount
return instance.proxy!
}
}
return app
}
}

View File

@ -674,9 +674,10 @@ export function registerRuntimeCompiler(_compile: any) {
compile = _compile
}
function finishComponentSetup(
export function finishComponentSetup(
instance: ComponentInternalInstance,
isSSR: boolean
isSSR: boolean,
skipOptions?: boolean
) {
const Component = instance.type as ComponentOptions
@ -719,7 +720,7 @@ function finishComponentSetup(
}
// support for 2.x options
if (__FEATURE_OPTIONS_API__) {
if (__FEATURE_OPTIONS_API__ && !(__COMPAT__ && skipOptions)) {
currentInstance = instance
pauseTracking()
applyOptions(instance, Component)

View File

@ -11,7 +11,9 @@ import {
isGloballyWhitelisted,
NOOP,
extend,
isString
isString,
warnDeprecation,
DeprecationTypes
} from '@vue/shared'
import {
ReactiveEffect,
@ -233,6 +235,25 @@ const publicPropertiesMap: PublicPropertiesMap = extend(Object.create(null), {
$watch: i => (__FEATURE_OPTIONS_API__ ? instanceWatch.bind(i) : NOOP)
} as PublicPropertiesMap)
if (__COMPAT__) {
extend(publicPropertiesMap, {
$mount: i => {
if (__DEV__) {
warnDeprecation(DeprecationTypes.$MOUNT)
}
// root mount override from apiCreateApp.ts
return i.ctx._compat_mount || NOOP
},
$destroy: i => {
if (__DEV__) {
warnDeprecation(DeprecationTypes.$DESTROY)
}
// root destroy override from apiCreateApp.ts
return i.ctx._compat_destroy || NOOP
}
} as PublicPropertiesMap)
}
const enum AccessTypes {
SETUP,
DATA,

View File

@ -1292,11 +1292,16 @@ function baseCreateRenderer(
isSVG,
optimized
) => {
const instance: ComponentInternalInstance = (initialVNode.component = createComponentInstance(
initialVNode,
parentComponent,
parentSuspense
))
// 2.x compat may pre-creaate the component instance before actually
// mounting
const compatMountInstance = __COMPAT__ && initialVNode.component
const instance: ComponentInternalInstance =
compatMountInstance ||
(initialVNode.component = createComponentInstance(
initialVNode,
parentComponent,
parentSuspense
))
if (__DEV__ && instance.type.__hmrId) {
registerHMR(instance)
@ -1313,12 +1318,14 @@ function baseCreateRenderer(
}
// resolve props and slots for setup context
if (__DEV__) {
startMeasure(instance, `init`)
}
setupComponent(instance)
if (__DEV__) {
endMeasure(instance, `init`)
if (!(__COMPAT__ && compatMountInstance)) {
if (__DEV__) {
startMeasure(instance, `init`)
}
setupComponent(instance)
if (__DEV__) {
endMeasure(instance, `init`)
}
}
// setup() is async. This component relies on async logic to be resolved

View File

@ -72,17 +72,6 @@ export const createApp = ((...args) => {
const container = normalizeContainer(containerOrSelector)
if (!container) return
// 2.x compat check
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)) {
warnDeprecation(DeprecationTypes.DOM_TEMPLATE_MOUNT)
break
}
}
}
const component = app._component
if (!isFunction(component) && !component.render && !component.template) {
// __UNSAFE__
@ -90,7 +79,18 @@ export const createApp = ((...args) => {
// 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.
component.template = container.innerHTML
// 2.x compat check
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)) {
warnDeprecation(DeprecationTypes.DOM_TEMPLATE_MOUNT)
break
}
}
}
}
// clear content before mounting
container.innerHTML = ''
const proxy = mount(container, false, container instanceof SVGElement)

View File

@ -1,5 +1,7 @@
export const enum DeprecationTypes {
DOM_TEMPLATE_MOUNT
DOM_TEMPLATE_MOUNT,
$MOUNT,
$DESTROY
}
type DeprecationData = {
@ -14,6 +16,18 @@ const deprecations: Record<DeprecationTypes, DeprecationData> = {
`In Vue 3, the container is no longer considered part of the template ` +
`and will not be processed/replaced.`,
link: `https://v3.vuejs.org/guide/migration/mount-changes.html`
},
[DeprecationTypes.$MOUNT]: {
message:
`vm.$mount() has been deprecated. ` +
`Use createApp(RootComponent).mount() instead.`,
link: `https://v3.vuejs.org/guide/migration/global-api.html#mounting-app-instance`
},
[DeprecationTypes.$DESTROY]: {
message: `vm.$destroy() has been deprecated. Use app.unmount() instead.`,
link: `https://v3.vuejs.org/api/application-api.html#unmount`
}
}

View File

@ -13,6 +13,7 @@ import {
RenderFunction,
isRuntimeOnly
} from '@vue/runtime-dom'
import { extend } from '@vue/shared'
// TODO make these getter/setters and trigger deprecation warnings
export type LegacyConfig = AppConfig & {
@ -89,6 +90,7 @@ export type GlobalVue = Pick<App, 'version' | 'component' | 'directive'> & {
export const Vue: GlobalVue = function Vue(options: ComponentOptions = {}) {
const app = createApp(options)
// copy over global config mutations
for (const key in singletonApp.config) {
if (
@ -99,8 +101,13 @@ export const Vue: GlobalVue = function Vue(options: ComponentOptions = {}) {
app.config[key] = singletonApp.config[key]
}
}
// TODO copy prototype augmentations as config.globalProperties
if (options.el) {
return app.mount(options.el)
} else {
return app._createRoot!(options)
}
} as any
@ -109,7 +116,18 @@ const singletonApp = createApp({})
Vue.version = __VERSION__
Vue.config = singletonApp.config
Vue.extend = defineComponent
Vue.extend = ((baseOptions: ComponentOptions = {}) => {
return function ExtendedVueConstructor(inlineOptions?: ComponentOptions) {
if (!inlineOptions) {
return new Vue(baseOptions)
} else {
const mergedOptions = extend({}, baseOptions)
mergedOptions.mixins = [inlineOptions, ...(mergedOptions.mixins || [])]
return new Vue(mergedOptions)
}
}
}) as any
Vue.nextTick = nextTick
Vue.set = (target, key, value) => {