fix(custom-elements): use strict number casting

close #4946
close #2598
close #2604

This commit also refactors internal usage of previous loose
implementation of `toNumber` to the stricter version where applicable.
Use of `looseToNumber` is preserved for `v-model.number` modifier to
ensure backwards compatibility and consistency with Vue 2 behavior.
This commit is contained in:
Evan You 2022-11-14 16:20:12 +08:00
parent efa2ac54d9
commit 7d0c63ff43
8 changed files with 51 additions and 29 deletions

View File

@ -2,9 +2,9 @@ import {
extend, extend,
looseEqual, looseEqual,
looseIndexOf, looseIndexOf,
looseToNumber,
NOOP, NOOP,
toDisplayString, toDisplayString
toNumber
} from '@vue/shared' } from '@vue/shared'
import { import {
ComponentPublicInstance, ComponentPublicInstance,
@ -148,7 +148,7 @@ export function installCompatInstanceProperties(map: PublicPropertiesMap) {
$createElement: () => compatH, $createElement: () => compatH,
_c: () => compatH, _c: () => compatH,
_o: () => legacyMarkOnce, _o: () => legacyMarkOnce,
_n: () => toNumber, _n: () => looseToNumber,
_s: () => toDisplayString, _s: () => toDisplayString,
_l: () => renderList, _l: () => renderList,
_t: i => legacyRenderSlot.bind(null, i), _t: i => legacyRenderSlot.bind(null, i),

View File

@ -10,8 +10,8 @@ import {
isObject, isObject,
isString, isString,
isOn, isOn,
toNumber, UnionToIntersection,
UnionToIntersection looseToNumber
} from '@vue/shared' } from '@vue/shared'
import { import {
ComponentInternalInstance, ComponentInternalInstance,
@ -126,7 +126,7 @@ export function emit(
args = rawArgs.map(a => (isString(a) ? a.trim() : a)) args = rawArgs.map(a => (isString(a) ? a.trim() : a))
} }
if (number) { if (number) {
args = rawArgs.map(toNumber) args = rawArgs.map(looseToNumber)
} }
} }

View File

@ -22,7 +22,12 @@ import {
} from '../renderer' } from '../renderer'
import { queuePostFlushCb } from '../scheduler' import { queuePostFlushCb } from '../scheduler'
import { filterSingleRoot, updateHOCHostEl } from '../componentRenderUtils' import { filterSingleRoot, updateHOCHostEl } from '../componentRenderUtils'
import { pushWarningContext, popWarningContext, warn } from '../warning' import {
pushWarningContext,
popWarningContext,
warn,
assertNumber
} from '../warning'
import { handleError, ErrorCodes } from '../errorHandling' import { handleError, ErrorCodes } from '../errorHandling'
export interface SuspenseProps { export interface SuspenseProps {
@ -419,6 +424,10 @@ function createSuspenseBoundary(
} = rendererInternals } = rendererInternals
const timeout = toNumber(vnode.props && vnode.props.timeout) const timeout = toNumber(vnode.props && vnode.props.timeout)
if (__DEV__) {
assertNumber(timeout, `Suspense timeout`)
}
const suspense: SuspenseBoundary = { const suspense: SuspenseBoundary = {
vnode, vnode,
parent, parent,

View File

@ -104,7 +104,7 @@ export { useSSRContext, ssrContextKey } from './helpers/useSsrContext'
export { createRenderer, createHydrationRenderer } from './renderer' export { createRenderer, createHydrationRenderer } from './renderer'
export { queuePostFlushCb } from './scheduler' export { queuePostFlushCb } from './scheduler'
export { warn } from './warning' export { warn, assertNumber } from './warning'
export { export {
handleError, handleError,
callWithErrorHandling, callWithErrorHandling,

View File

@ -162,3 +162,15 @@ function formatProp(key: string, value: unknown, raw?: boolean): any {
return raw ? value : [`${key}=`, value] return raw ? value : [`${key}=`, value]
} }
} }
/**
* @internal
*/
export function assertNumber(val: unknown, type: string) {
if (!__DEV__) return
if (typeof val !== 'number') {
warn(`${type} is not a valid number - ` + `got ${JSON.stringify(val)}.`)
} else if (isNaN(val)) {
warn(`${type} is NaN - ` + 'the duration expression might be incorrect.')
}
}

View File

@ -2,7 +2,7 @@ import {
BaseTransition, BaseTransition,
BaseTransitionProps, BaseTransitionProps,
h, h,
warn, assertNumber,
FunctionalComponent, FunctionalComponent,
compatUtils, compatUtils,
DeprecationTypes DeprecationTypes
@ -283,24 +283,10 @@ function normalizeDuration(
function NumberOf(val: unknown): number { function NumberOf(val: unknown): number {
const res = toNumber(val) const res = toNumber(val)
if (__DEV__) validateDuration(res) if (__DEV__) assertNumber(res, '<transition> explicit duration')
return res return res
} }
function validateDuration(val: unknown) {
if (typeof val !== 'number') {
warn(
`<transition> explicit duration is not a valid number - ` +
`got ${JSON.stringify(val)}.`
)
} else if (isNaN(val)) {
warn(
`<transition> explicit duration is NaN - ` +
'the duration expression might be incorrect.'
)
}
}
export function addTransitionClass(el: Element, cls: string) { export function addTransitionClass(el: Element, cls: string) {
cls.split(/\s+/).forEach(c => c && el.classList.add(c)) cls.split(/\s+/).forEach(c => c && el.classList.add(c))
;( ;(

View File

@ -11,7 +11,7 @@ import {
looseEqual, looseEqual,
looseIndexOf, looseIndexOf,
invokeArrayFns, invokeArrayFns,
toNumber, looseToNumber,
isSet isSet
} from '@vue/shared' } from '@vue/shared'
@ -54,7 +54,7 @@ export const vModelText: ModelDirective<
domValue = domValue.trim() domValue = domValue.trim()
} }
if (castToNumber) { if (castToNumber) {
domValue = toNumber(domValue) domValue = looseToNumber(domValue)
} }
el._assign(domValue) el._assign(domValue)
}) })
@ -88,7 +88,10 @@ export const vModelText: ModelDirective<
if (trim && el.value.trim() === value) { if (trim && el.value.trim() === value) {
return return
} }
if ((number || el.type === 'number') && toNumber(el.value) === value) { if (
(number || el.type === 'number') &&
looseToNumber(el.value) === value
) {
return return
} }
} }
@ -182,7 +185,7 @@ export const vModelSelect: ModelDirective<HTMLSelectElement> = {
const selectedVal = Array.prototype.filter const selectedVal = Array.prototype.filter
.call(el.options, (o: HTMLOptionElement) => o.selected) .call(el.options, (o: HTMLOptionElement) => o.selected)
.map((o: HTMLOptionElement) => .map((o: HTMLOptionElement) =>
number ? toNumber(getValue(o)) : getValue(o) number ? looseToNumber(getValue(o)) : getValue(o)
) )
el._assign( el._assign(
el.multiple el.multiple

View File

@ -153,11 +153,23 @@ export const def = (obj: object, key: string | symbol, value: any) => {
}) })
} }
export const toNumber = (val: any): any => { /**
* "123-foo" will be parsed to 123
* This is used for the .number modifier in v-model
*/
export const looseToNumber = (val: any): any => {
const n = parseFloat(val) const n = parseFloat(val)
return isNaN(n) ? val : n return isNaN(n) ? val : n
} }
/**
* "123-foo" will be returned as-is
*/
export const toNumber = (val: any): any => {
const n = Number(val)
return isNaN(n) ? val : n
}
let _globalThis: any let _globalThis: any
export const getGlobalThis = (): any => { export const getGlobalThis = (): any => {
return ( return (