wip: compat integration progress

This commit is contained in:
Evan You 2021-04-20 09:25:12 -04:00
parent 7dc681c196
commit f6dee53270
10 changed files with 183 additions and 22 deletions

View File

@ -203,9 +203,10 @@ const deprecationData: Record<DeprecationTypes, DeprecationData> = {
[DeprecationTypes.INSTANCE_LISTENERS]: { [DeprecationTypes.INSTANCE_LISTENERS]: {
message: message:
`vm.$listeners has been removed. Parent v-on listeners are now ` + `vm.$listeners has been removed. In Vue 3, parent v-on listeners are ` +
`included in vm.$attrs and it is no longer necessary to separately use ` + `included in vm.$attrs and it is no longer necessary to separately use ` +
`v-on="$listeners" if you are already using v-bind="$attrs".`, `v-on="$listeners" if you are already using v-bind="$attrs". ` +
`(Note: the Vue 3 behavior only applies if this compat config is disabled)`,
link: `https://v3.vuejs.org/guide/migration/listeners-removed.html` link: `https://v3.vuejs.org/guide/migration/listeners-removed.html`
}, },

View File

@ -1,4 +1,4 @@
import { extend, NOOP } from '@vue/shared' import { extend, NOOP, toDisplayString, toNumber } from '@vue/shared'
import { PublicPropertiesMap } from '../componentPublicInstance' import { PublicPropertiesMap } from '../componentPublicInstance'
import { getCompatChildren } from './instanceChildren' import { getCompatChildren } from './instanceChildren'
import { import {
@ -11,6 +11,14 @@ import { off, on, once } from './instanceEventEmitter'
import { getCompatListeners } from './instanceListeners' import { getCompatListeners } from './instanceListeners'
import { shallowReadonly } from '@vue/reactivity' import { shallowReadonly } from '@vue/reactivity'
import { legacySlotProxyHandlers } from './component' import { legacySlotProxyHandlers } from './component'
import { compatH } from './renderFn'
import {
legacyBindObjectProps,
legacyRenderSlot,
legacyRenderStatic
} from './renderHelpers'
import { createCommentVNode, createTextVNode } from '../vnode'
import { renderList } from '../helpers/renderList'
export function installCompatInstanceProperties(map: PublicPropertiesMap) { export function installCompatInstanceProperties(map: PublicPropertiesMap) {
const set = (target: any, key: any, val: any) => { const set = (target: any, key: any, val: any) => {
@ -77,6 +85,19 @@ export function installCompatInstanceProperties(map: PublicPropertiesMap) {
$off: i => off.bind(null, i), $off: i => off.bind(null, i),
$children: getCompatChildren, $children: getCompatChildren,
$listeners: getCompatListeners $listeners: getCompatListeners,
// v2 render helpers
$createElement: () => compatH,
_self: i => i.proxy,
_c: () => compatH,
_n: () => toNumber,
_s: () => toDisplayString,
_l: () => renderList,
_t: i => legacyRenderSlot.bind(null, i),
_b: () => legacyBindObjectProps,
_e: () => createCommentVNode,
_v: () => createTextVNode,
_m: i => legacyRenderStatic.bind(null, i)
} as PublicPropertiesMap) } as PublicPropertiesMap)
} }

View File

@ -29,6 +29,8 @@ import {
} from '../vnode' } from '../vnode'
import { checkCompatEnabled, DeprecationTypes } from './compatConfig' import { checkCompatEnabled, DeprecationTypes } from './compatConfig'
const v3CompiledRenderFnRE = /^(?:function \w+)?\(_ctx, _cache/
export function convertLegacyRenderFn(instance: ComponentInternalInstance) { export function convertLegacyRenderFn(instance: ComponentInternalInstance) {
const Component = instance.type as ComponentOptions const Component = instance.type as ComponentOptions
const render = Component.render as InternalRenderFunction | undefined const render = Component.render as InternalRenderFunction | undefined
@ -38,8 +40,7 @@ export function convertLegacyRenderFn(instance: ComponentInternalInstance) {
return return
} }
const string = render.toString() if (v3CompiledRenderFnRE.test(render.toString())) {
if (string.startsWith('function render(_ctx') || string.startsWith('(_ctx')) {
// v3 pre-compiled function // v3 pre-compiled function
render._compatChecked = true render._compatChecked = true
return return
@ -128,9 +129,7 @@ export function compatH(
return convertLegacySlots(createVNode(type, null, propsOrChildren)) return convertLegacySlots(createVNode(type, null, propsOrChildren))
} }
} else { } else {
if (l > 3) { if (isVNode(children)) {
children = Array.prototype.slice.call(arguments, 2)
} else if (l === 3 && isVNode(children)) {
children = [children] children = [children]
} }
return convertLegacySlots( return convertLegacySlots(
@ -157,13 +156,20 @@ function convertLegacyProps(
} else if (key === 'on' || key === 'nativeOn') { } else if (key === 'on' || key === 'nativeOn') {
const listeners = legacyProps[key] const listeners = legacyProps[key]
for (const event in listeners) { for (const event in listeners) {
const handlerKey = toHandlerKey(event) const handlerKey = convertLegacyEventKey(event)
const existing = converted[handlerKey] const existing = converted[handlerKey]
const incoming = listeners[event] const incoming = listeners[event]
if (existing !== incoming) { if (existing !== incoming) {
converted[handlerKey] = existing if (existing) {
? [].concat(existing as any, incoming as any) // for the rare case where the same handler is attached
: incoming // twice with/without .native modifier...
if (key === 'nativeOn' && String(existing) === String(incoming)) {
continue
}
converted[handlerKey] = [].concat(existing as any, incoming as any)
} else {
converted[handlerKey] = incoming
}
} }
} }
} else if ( } else if (
@ -185,6 +191,20 @@ function convertLegacyProps(
return converted return converted
} }
function convertLegacyEventKey(event: string): string {
// normalize v2 event prefixes
if (event[0] === '&') {
event = event.slice(1) + 'Passive'
}
if (event[0] === '~') {
event = event.slice(1) + 'Once'
}
if (event[0] === '!') {
event = event.slice(1) + 'Capture'
}
return toHandlerKey(event)
}
function convertLegacyDirectives( function convertLegacyDirectives(
vnode: VNode, vnode: VNode,
props?: LegacyVNodeProps props?: LegacyVNodeProps

View File

@ -0,0 +1,94 @@
import {
camelize,
extend,
hyphenate,
isArray,
isObject,
isReservedProp,
normalizeClass
} from '@vue/shared'
import { ComponentInternalInstance } from '../component'
import { renderSlot } from '../helpers/renderSlot'
import { mergeProps, VNode } from '../vnode'
export function legacyBindObjectProps(
data: any,
_tag: string,
value: any,
_asProp: boolean,
isSync?: boolean
) {
if (value && isObject(value)) {
if (isArray(value)) {
value = toObject(value)
}
for (const key in value) {
if (isReservedProp(key)) {
data[key] = value[key]
} else if (key === 'class') {
data.class = normalizeClass([data.class, value.class])
} else if (key === 'style') {
data.style = normalizeClass([data.style, value.style])
} else {
const attrs = data.attrs || (data.attrs = {})
const camelizedKey = camelize(key)
const hyphenatedKey = hyphenate(key)
if (!(camelizedKey in attrs) && !(hyphenatedKey in attrs)) {
attrs[key] = value[key]
if (isSync) {
const on = data.on || (data.on = {})
on[`update:${key}`] = function($event: any) {
value[key] = $event
}
}
}
}
}
}
return data
}
export function legacyRenderSlot(
instance: ComponentInternalInstance,
name: string,
fallback?: VNode[],
props?: any,
bindObject?: any
) {
if (bindObject) {
props = mergeProps(props, bindObject)
}
return renderSlot(instance.slots, name, props, fallback && (() => fallback))
}
const staticCacheMap = /*#__PURE__*/ new WeakMap<
ComponentInternalInstance,
any[]
>()
export function legacyRenderStatic(
instance: ComponentInternalInstance,
index: number
) {
let cache = staticCacheMap.get(instance)
if (!cache) {
staticCacheMap.set(instance, (cache = []))
}
if (cache[index]) {
return cache[index]
}
const fn = (instance.type as any).staticRenderFns[index]
const ctx = instance.proxy
return (cache[index] = fn.call(ctx, null, ctx))
}
function toObject(arr: Array<any>): Object {
const res = {}
for (let i = 0; i < arr.length; i++) {
if (arr[i]) {
extend(res, arr[i])
}
}
return res
}

View File

@ -9,11 +9,6 @@ import {
isCompatEnabled isCompatEnabled
} from './compatConfig' } from './compatConfig'
const defaultModelMapping = {
prop: 'value',
event: 'input'
}
export const compatModelEventPrefix = `onModelCompat:` export const compatModelEventPrefix = `onModelCompat:`
const warnedTypes = new WeakSet() const warnedTypes = new WeakSet()
@ -40,7 +35,7 @@ export function convertLegacyVModelProps(vnode: VNode) {
warnedTypes.add(type as ComponentOptions) warnedTypes.add(type as ComponentOptions)
} }
const { prop, event } = (type as any).model || defaultModelMapping const { prop = 'value', event = 'input' } = (type as any).model || {}
props[prop] = props.modelValue props[prop] = props.modelValue
delete props.modelValue delete props.modelValue
// important: update dynamic props // important: update dynamic props

View File

@ -20,7 +20,8 @@ import {
isReservedProp, isReservedProp,
EMPTY_ARR, EMPTY_ARR,
def, def,
extend extend,
isOn
} from '@vue/shared' } from '@vue/shared'
import { warn } from './warning' import { warn } from './warning'
import { import {
@ -224,6 +225,13 @@ export function updateProps(
) )
} }
} else { } else {
if (
__COMPAT__ &&
isOn(key) &&
isCompatEnabled(DeprecationTypes.INSTANCE_LISTENERS, instance)
) {
continue
}
attrs[key] = value attrs[key] = value
} }
} }
@ -320,6 +328,13 @@ function setFullProps(
// Any non-declared (either as a prop or an emitted event) props are put // Any non-declared (either as a prop or an emitted event) props are put
// into a separate `attrs` object for spreading. Make sure to preserve // into a separate `attrs` object for spreading. Make sure to preserve
// original key casing // original key casing
if (
__COMPAT__ &&
isOn(key) &&
isCompatEnabled(DeprecationTypes.INSTANCE_LISTENERS, instance)
) {
continue
}
attrs[key] = value attrs[key] = value
} }
} }

View File

@ -80,7 +80,7 @@ const normalizeObjectSlots = (rawSlots: RawSlots, slots: InternalSlots) => {
if (isFunction(value)) { if (isFunction(value)) {
slots[key] = normalizeSlot(key, value, ctx) slots[key] = normalizeSlot(key, value, ctx)
} else if (value != null) { } else if (value != null) {
if (__DEV__) { if (__DEV__ && !__COMPAT__) {
warn( warn(
`Non-function value encountered for slot "${key}". ` + `Non-function value encountered for slot "${key}". ` +
`Prefer function slots for better performance.` `Prefer function slots for better performance.`

View File

@ -0,0 +1,3 @@
import Vue from './index'
export default Vue
export * from '@vue/runtime-dom'

View File

@ -0,0 +1,3 @@
import Vue from './runtime'
export default Vue
export * from '@vue/runtime-dom'

View File

@ -116,7 +116,16 @@ function createConfig(format, output, plugins = []) {
// during a single build. // during a single build.
hasTSChecked = true hasTSChecked = true
const entryFile = /runtime$/.test(format) ? `src/runtime.ts` : `src/index.ts` let entryFile = /runtime$/.test(format) ? `src/runtime.ts` : `src/index.ts`
// the compat build needs both default AND named exports. This will cause
// Rollup to complain for non-ESM targets, so we use separate entries for
// esm vs. non-esm builds.
if (isCompatBuild && (isBrowserESMBuild || isBundlerESMBuild)) {
entryFile = /runtime$/.test(format)
? `src/esm-runtime.ts`
: `src/esm-index.ts`
}
let external = [] let external = []