]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
wip: root mount api compat
authorEvan You <yyx990803@gmail.com>
Mon, 5 Apr 2021 15:54:35 +0000 (11:54 -0400)
committerEvan You <yyx990803@gmail.com>
Wed, 7 Apr 2021 20:19:24 +0000 (16:19 -0400)
packages/runtime-core/src/apiCreateApp.ts
packages/runtime-core/src/component.ts
packages/runtime-core/src/componentPublicInstance.ts
packages/runtime-core/src/renderer.ts
packages/runtime-dom/src/index.ts
packages/shared/src/deprecations.ts
packages/vue-compat/src/apiGlobal.ts

index 92cce58435b08081196dd06612de7ec2a53b766d..8ef0a2a22c64cafec8fb2657d53d18fd630c0ec0 100644 (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
   }
 }
index 6e2c1d53d787c97c2689b0f4cb956d41f290efb3..635bccc4a0ef225aca02a95e98c62a90fec0d030 100644 (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)
index fd0351f64d7d6637005a4f56f625b75fea39177d..c2cc5c16ede4655741ad4f037675e48617abd1e1 100644 (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,
index 2c1b2bbde1507001d117e002185ae0d15721cd32..ea065d580b51aa9e63badd4c2c6c62411e885dee 100644 (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
index b2a87a60adad0f1b5b0514b3d5049cdb9c42ce9d..1a4265aa7e858af18580a19a37d5c010f3a8d93b 100644 (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)
index f3d23750ac43b48f03aab353b8ea004480bd06e2..ff097f3d2ada8ed1871c635e43a60de659a5d083 100644 (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`
   }
 }
 
index 10ad71e99f96e6777527d33c8ebcb80dfc540e8b..1a9f95e290ac80b87311ed7a08a615950c99e2f2 100644 (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) => {