Evan You 5 лет назад
Родитель
Сommit
183f9b0013

+ 29 - 0
packages/runtime-core/src/compat/deprecations.ts

@@ -1,5 +1,7 @@
+import { hasOwn, isArray } from '@vue/shared/src'
 import {
   ComponentInternalInstance,
+  ComponentOptions,
   formatComponentName,
   getComponentName,
   getCurrentInstance,
@@ -52,6 +54,7 @@ export const enum DeprecationTypes {
 
   COMPONENT_ASYNC = 'COMPONENT_ASYNC',
   COMPONENT_FUNCTIONAL = 'COMPONENT_FUNCTIONAL',
+  COMPONENT_V_MODEL = 'COMPONENT_V_MODEL',
 
   RENDER_FUNCTION = 'RENDER_FUNCTION'
 }
@@ -345,6 +348,32 @@ const deprecationData: Record<DeprecationTypes, DeprecationData> = {
     link: `https://v3.vuejs.org/guide/migration/functional-components.html`
   },
 
+  [DeprecationTypes.COMPONENT_V_MODEL]: {
+    message: (comp: ComponentOptions) => {
+      const configMsg =
+        `opt-in to ` +
+        `Vue 3 behavior on a per-component basis with \`compatConfig: { ${
+          DeprecationTypes.COMPONENT_V_MODEL
+        }: false }\`.`
+      if (
+        comp.props && isArray(comp.props)
+          ? comp.props.includes('modelValue')
+          : hasOwn(comp.props, 'modelValue')
+      ) {
+        return (
+          `Component delcares "modelValue" prop, which is Vue 3 usage, but ` +
+          `is running under Vue 2 compat v-model behavior. You can ${configMsg}`
+        )
+      }
+      return (
+        `v-model usage on component has changed in Vue 3. Component that expects ` +
+        `to work with v-model should now use the "modelValue" prop and emit the ` +
+        `"update:modelValue" event. You can update the usage and then ${configMsg}`
+      )
+    },
+    link: `https://v3.vuejs.org/guide/migration/v-model.html`
+  },
+
   [DeprecationTypes.RENDER_FUNCTION]: {
     message:
       `Vue 3's render function API has changed. ` +

+ 71 - 0
packages/runtime-core/src/compat/vModel.ts

@@ -0,0 +1,71 @@
+import { ShapeFlags } from '@vue/shared'
+import { ComponentInternalInstance, ComponentOptions } from '../component'
+import { callWithErrorHandling, ErrorCodes } from '../errorHandling'
+import { VNode } from '../vnode'
+import { popWarningContext, pushWarningContext } from '../warning'
+import { isCompatEnabled } from './compatConfig'
+import { DeprecationTypes, warnDeprecation } from './deprecations'
+
+const defaultModelMapping = {
+  prop: 'value',
+  event: 'input'
+}
+
+export const compatModelEventPrefix = `onModelCompat:`
+
+const warnedTypes = new WeakSet()
+
+export function convertLegacyVModelProps(vnode: VNode) {
+  const { type, shapeFlag, props, dynamicProps } = vnode
+  if (shapeFlag & ShapeFlags.COMPONENT && props && 'modelValue' in props) {
+    if (
+      !isCompatEnabled(
+        DeprecationTypes.COMPONENT_V_MODEL,
+        // this is a special case where we want to use the vnode component's
+        // compat config instead of the current rendering instance (which is the
+        // parent of the component that exposes v-model)
+        { type } as any
+      )
+    ) {
+      return
+    }
+
+    if (__DEV__ && !warnedTypes.has(type as ComponentOptions)) {
+      pushWarningContext(vnode)
+      warnDeprecation(DeprecationTypes.COMPONENT_V_MODEL, { type } as any, type)
+      popWarningContext()
+      warnedTypes.add(type as ComponentOptions)
+    }
+
+    const { prop, event } = (type as any).model || defaultModelMapping
+    props[prop] = props.modelValue
+    delete props.modelValue
+    // important: update dynamic props
+    if (dynamicProps) {
+      dynamicProps[dynamicProps.indexOf('modelValue')] = prop
+    }
+
+    props[compatModelEventPrefix + event] = props['onUpdate:modelValue']
+    delete props['onUpdate:modelValue']
+  }
+}
+
+export function compatModelEmit(
+  instance: ComponentInternalInstance,
+  event: string,
+  args: any[]
+) {
+  if (!isCompatEnabled(DeprecationTypes.COMPONENT_V_MODEL, instance)) {
+    return
+  }
+  const props = instance.vnode.props
+  const modelHandler = props && props[compatModelEventPrefix + event]
+  if (modelHandler) {
+    callWithErrorHandling(
+      modelHandler,
+      instance,
+      ErrorCodes.COMPONENT_EVENT_HANDLER,
+      args
+    )
+  }
+}

+ 2 - 2
packages/runtime-core/src/component.ts

@@ -687,9 +687,9 @@ export function finishComponentSetup(
   if (
     __COMPAT__ &&
     Component.render &&
-    isCompatEnabled(DeprecationTypes.RENDER_FUNCTION)
+    isCompatEnabled(DeprecationTypes.RENDER_FUNCTION, instance)
   ) {
-    warnDeprecation(DeprecationTypes.RENDER_FUNCTION)
+    warnDeprecation(DeprecationTypes.RENDER_FUNCTION, instance)
     const originalRender = Component.render
     Component.render = function compatRender() {
       return originalRender.call(this, compatH)

+ 12 - 3
packages/runtime-core/src/componentEmits.ts

@@ -21,7 +21,8 @@ import { warn } from './warning'
 import { UnionToIntersection } from './helpers/typeUtils'
 import { devtoolsComponentEmit } from './devtools'
 import { AppContext } from './apiCreateApp'
-import { emit as compatEmit } from './compat/instanceEventEmitter'
+import { emit as compatInstanceEmit } from './compat/instanceEventEmitter'
+import { compatModelEventPrefix, compatModelEmit } from './compat/vModel'
 
 export type ObjectEmitsOptions = Record<
   string,
@@ -57,7 +58,14 @@ export function emit(
       propsOptions: [propsOptions]
     } = instance
     if (emitsOptions) {
-      if (!(event in emitsOptions) && !event.startsWith('hook:')) {
+      if (
+        !(event in emitsOptions) &&
+        !(
+          __COMPAT__ &&
+          (event.startsWith('hook:') ||
+            event.startsWith(compatModelEventPrefix))
+        )
+      ) {
         if (!propsOptions || !(toHandlerKey(event) in propsOptions)) {
           warn(
             `Component emitted event "${event}" but it is neither declared in ` +
@@ -151,7 +159,8 @@ export function emit(
   }
 
   if (__COMPAT__) {
-    return compatEmit(instance, event, args)
+    compatModelEmit(instance, event, args)
+    return compatInstanceEmit(instance, event, args)
   }
 }
 

+ 1 - 1
packages/runtime-core/src/componentProps.ts

@@ -348,7 +348,7 @@ function resolvePropValue(
           value = propsDefaults[key] = defaultValue.call(
             __COMPAT__ &&
             __DEV__ &&
-            isCompatEnabled(DeprecationTypes.PROPS_DEFAULT_THIS)
+            isCompatEnabled(DeprecationTypes.PROPS_DEFAULT_THIS, instance)
               ? createPropsDefaultThis(key)
               : null,
             props

+ 5 - 0
packages/runtime-core/src/vnode.ts

@@ -42,6 +42,7 @@ import { NULL_DYNAMIC_COMPONENT } from './helpers/resolveAssets'
 import { hmrDirtyComponents } from './hmr'
 import { setCompiledSlotRendering } from './helpers/renderSlot'
 import { convertLegacyComponent } from './compat/component'
+import { convertLegacyVModelProps } from './compat/vModel'
 
 export const Fragment = (Symbol(__DEV__ ? 'Fragment' : undefined) as any) as {
   __isFragment: true
@@ -469,6 +470,10 @@ function _createVNode(
     currentBlock.push(vnode)
   }
 
+  if (__COMPAT__) {
+    convertLegacyVModelProps(vnode)
+  }
+
   return vnode
 }