vue3-yuanma/packages/runtime-core/src/compat/global.ts

654 lines
17 KiB
TypeScript
Raw Normal View History

2021-04-11 20:53:43 +00:00
import {
isReactive,
reactive,
stop,
2021-04-11 20:53:43 +00:00
track,
TrackOpTypes,
trigger,
TriggerOpTypes
} from '@vue/reactivity'
import {
isFunction,
extend,
NOOP,
isArray,
2021-04-28 16:29:51 +00:00
isObject,
isString,
invokeArrayFns
2021-04-11 20:53:43 +00:00
} from '@vue/shared'
2021-04-05 22:13:29 +00:00
import { warn } from '../warning'
import { cloneVNode, createVNode } from '../vnode'
import { RootRenderFunction } from '../renderer'
import {
App,
AppConfig,
AppContext,
CreateAppFunction,
Plugin
} from '../apiCreateApp'
import {
Component,
ComponentOptions,
createComponentInstance,
finishComponentSetup,
isRuntimeOnly,
setupComponent
} from '../component'
2021-04-11 20:53:43 +00:00
import { RenderFunction, mergeOptions } from '../componentOptions'
import { ComponentPublicInstance } from '../componentPublicInstance'
import { devtoolsInitApp, devtoolsUnmountApp } from '../devtools'
import { Directive } from '../directives'
import { nextTick } from '../scheduler'
2021-04-05 22:13:29 +00:00
import { version } from '..'
import {
installLegacyConfigProperties,
LegacyConfig,
legacyOptionMergeStrats
} from './globalConfig'
2021-04-06 15:57:10 +00:00
import { LegacyDirective } from './customDirective'
import {
warnDeprecation,
DeprecationTypes,
assertCompatEnabled,
configureCompat,
isCompatEnabled,
softAssertCompatEnabled
} from './compatConfig'
2021-04-27 21:34:19 +00:00
import { LegacyPublicInstance } from './instance'
/**
* @deprecated the default `Vue` export has been removed in Vue 3. The type for
* the default export is provided only for migration purposes. Please use
* named imports instead - e.g. `import { createApp } from 'vue'`.
*/
export type CompatVue = Pick<App, 'version' | 'component' | 'directive'> & {
2021-04-27 21:34:19 +00:00
configureCompat: typeof configureCompat
// no inference here since these types are not meant for actual use - they
// are merely here to provide type checks for internal implementation and
// information for migration.
2021-04-27 21:34:19 +00:00
new (options?: ComponentOptions): LegacyPublicInstance
version: string
2021-04-05 22:13:29 +00:00
config: AppConfig & LegacyConfig
nextTick: typeof nextTick
use(plugin: Plugin, ...options: any[]): CompatVue
mixin(mixin: ComponentOptions): CompatVue
component(name: string): Component | undefined
component(name: string, component: Component): CompatVue
directive(name: string): Directive | undefined
directive(name: string, directive: Directive): CompatVue
compile(template: string): RenderFunction
/**
* @deprecated
*/
extend: (options?: ComponentOptions) => CompatVue
/**
* @deprecated Vue 3 no longer needs set() for adding new properties.
*/
set(target: any, key: string | number | symbol, value: any): void
/**
* @deprecated Vue 3 no longer needs delete() for property deletions.
*/
delete(target: any, key: string | number | symbol): void
/**
* @deprecated use `reactive` instead.
*/
observable: typeof reactive
/**
* @deprecated filters have been removed from Vue 3.
*/
filter(name: string, arg?: any): null
2021-04-22 18:59:54 +00:00
/**
* @internal
*/
cid: number
/**
* @internal
*/
options: ComponentOptions
/**
* @internal
*/
util: any
2021-04-27 21:34:19 +00:00
/**
* @internal
*/
super: CompatVue
}
export let isCopyingConfig = false
// exported only for test
export let singletonApp: App
let singletonCtor: CompatVue
// Legacy global Vue constructor
2021-04-05 22:13:29 +00:00
export function createCompatVue(
createApp: CreateAppFunction<Element>,
createSingletonApp: CreateAppFunction<Element>
2021-04-05 22:13:29 +00:00
): CompatVue {
singletonApp = createSingletonApp({})
const Vue: CompatVue = (singletonCtor = function Vue(
options: ComponentOptions = {}
) {
return createCompatApp(options, Vue)
} as any)
function createCompatApp(options: ComponentOptions = {}, Ctor: any) {
2021-04-09 22:52:14 +00:00
assertCompatEnabled(DeprecationTypes.GLOBAL_MOUNT, null)
2021-04-06 19:02:07 +00:00
const { data } = options
if (
data &&
!isFunction(data) &&
2021-04-09 22:52:14 +00:00
softAssertCompatEnabled(DeprecationTypes.OPTIONS_DATA_FN, null)
) {
2021-04-06 19:02:07 +00:00
options.data = () => data
}
const app = createApp(options)
if (Ctor !== Vue) {
applySingletonPrototype(app, Ctor)
2021-04-05 22:13:29 +00:00
}
const vm = app._createRoot!(options)
if (options.el) {
return (vm as any).$mount(options.el)
} else {
return vm
}
}
Vue.version = __VERSION__
Vue.config = singletonApp.config
Vue.use = (p, ...options) => {
if (p && isFunction(p.install)) {
p.install(Vue as any, ...options)
} else if (isFunction(p)) {
p(Vue as any, ...options)
}
return Vue
}
Vue.mixin = m => {
singletonApp.mixin(m)
return Vue
}
Vue.component = ((name: string, comp: Component) => {
if (comp) {
singletonApp.component(name, comp)
return Vue
} else {
return singletonApp.component(name)
}
}) as any
Vue.directive = ((name: string, dir: Directive | LegacyDirective) => {
if (dir) {
singletonApp.directive(name, dir as Directive)
return Vue
} else {
return singletonApp.directive(name)
}
}) as any
2021-04-22 18:59:54 +00:00
Vue.options = { _base: Vue }
let cid = 1
Vue.cid = cid
Vue.nextTick = nextTick
2021-04-27 21:34:19 +00:00
const extendCache = new WeakMap()
2021-04-22 18:59:54 +00:00
function extendCtor(this: any, extendOptions: ComponentOptions = {}) {
2021-04-09 22:52:14 +00:00
assertCompatEnabled(DeprecationTypes.GLOBAL_EXTEND, null)
2021-04-22 18:59:54 +00:00
if (isFunction(extendOptions)) {
extendOptions = extendOptions.options
}
2021-04-27 21:34:19 +00:00
if (extendCache.has(extendOptions)) {
return extendCache.get(extendOptions)
}
2021-04-22 18:59:54 +00:00
const Super = this
function SubVue(inlineOptions?: ComponentOptions) {
if (!inlineOptions) {
2021-04-27 21:34:19 +00:00
return createCompatApp(SubVue.options, SubVue)
} else {
return createCompatApp(
2021-04-27 21:34:19 +00:00
mergeOptions(
extend({}, SubVue.options),
inlineOptions,
null,
legacyOptionMergeStrats as any
),
SubVue
)
}
}
2021-04-27 21:34:19 +00:00
SubVue.super = Super
SubVue.prototype = Object.create(Vue.prototype)
SubVue.prototype.constructor = SubVue
2021-04-27 21:34:19 +00:00
// clone non-primitive base option values for edge case of mutating
// extended options
const mergeBase: any = {}
for (const key in Super.options) {
const superValue = Super.options[key]
mergeBase[key] = isArray(superValue)
? superValue.slice()
: isObject(superValue)
? extend(Object.create(null), superValue)
: superValue
}
2021-04-22 18:59:54 +00:00
SubVue.options = mergeOptions(
2021-04-27 21:34:19 +00:00
mergeBase,
extendOptions,
null,
legacyOptionMergeStrats as any
2021-04-22 18:59:54 +00:00
)
SubVue.options._base = SubVue
SubVue.extend = extendCtor.bind(SubVue)
SubVue.mixin = Super.mixin
SubVue.use = Super.use
SubVue.cid = ++cid
2021-04-27 21:34:19 +00:00
extendCache.set(extendOptions, SubVue)
return SubVue
2021-04-22 18:59:54 +00:00
}
Vue.extend = extendCtor.bind(Vue) as any
Vue.set = (target, key, value) => {
2021-04-09 22:52:14 +00:00
assertCompatEnabled(DeprecationTypes.GLOBAL_SET, null)
target[key] = value
}
2021-04-05 22:13:29 +00:00
Vue.delete = (target, key) => {
2021-04-09 22:52:14 +00:00
assertCompatEnabled(DeprecationTypes.GLOBAL_DELETE, null)
delete target[key]
}
2021-04-05 22:13:29 +00:00
Vue.observable = (target: any) => {
2021-04-09 22:52:14 +00:00
assertCompatEnabled(DeprecationTypes.GLOBAL_OBSERVABLE, null)
return reactive(target)
}
Vue.filter = ((name: string, filter?: any) => {
if (filter) {
singletonApp.filter!(name, filter)
return Vue
} else {
return singletonApp.filter!(name)
}
}) as any
2021-04-11 20:53:43 +00:00
// internal utils - these are technically internal but some plugins use it.
const util = {
warn: __DEV__ ? warn : NOOP,
extend,
mergeOptions: (parent: any, child: any, vm?: ComponentPublicInstance) =>
2021-04-27 21:34:19 +00:00
mergeOptions(
parent,
child,
vm && vm.$,
vm ? undefined : (legacyOptionMergeStrats as any)
),
2021-04-11 20:53:43 +00:00
defineReactive
}
Object.defineProperty(Vue, 'util', {
get() {
2021-04-27 21:34:19 +00:00
assertCompatEnabled(DeprecationTypes.GLOBAL_PRIVATE_UTIL, null)
2021-04-11 20:53:43 +00:00
return util
}
})
2021-04-07 15:22:56 +00:00
Vue.configureCompat = configureCompat
return Vue
}
2021-04-05 22:13:29 +00:00
export function installAppCompatProperties(
app: App,
context: AppContext,
render: RootRenderFunction
) {
installFilterMethod(app, context)
if (!singletonApp) {
// this is the call of creating the singleton itself so the rest is
// unnecessary
return
}
installCompatMount(app, context, render)
installLegacyAPIs(app)
applySingletonAppMutations(app)
if (__DEV__) installLegacyConfigProperties(app.config)
}
function installFilterMethod(app: App, context: AppContext) {
context.filters = {}
app.filter = (name: string, filter?: Function): any => {
assertCompatEnabled(DeprecationTypes.FILTERS, null)
if (!filter) {
return context.filters![name]
}
if (__DEV__ && context.filters![name]) {
warn(`Filter "${name}" has already been registered.`)
}
context.filters![name] = filter
return app
}
}
function installLegacyAPIs(app: App) {
// expose global API on app instance for legacy plugins
Object.defineProperties(app, {
// so that app.use() can work with legacy plugins that extend prototypes
prototype: {
get() {
__DEV__ && warnDeprecation(DeprecationTypes.GLOBAL_PROTOTYPE, null)
return app.config.globalProperties
}
},
nextTick: { value: nextTick },
extend: { value: singletonCtor.extend },
set: { value: singletonCtor.set },
delete: { value: singletonCtor.delete },
observable: { value: singletonCtor.observable },
util: {
get() {
return singletonCtor.util
}
}
})
}
function applySingletonAppMutations(app: App) {
// copy over asset registries and deopt flag
;['mixins', 'components', 'directives', 'filters', 'deopt'].forEach(key => {
// @ts-ignore
app._context[key] = singletonApp._context[key]
})
// copy over global config mutations
isCopyingConfig = true
for (const key in singletonApp.config) {
if (key === 'isNativeTag') continue
if (
isRuntimeOnly() &&
(key === 'isCustomElement' || key === 'compilerOptions')
) {
continue
}
const val = singletonApp.config[key as keyof AppConfig]
// @ts-ignore
app.config[key] = val
// compat for runtime ignoredElements -> isCustomElement
if (
key === 'ignoredElements' &&
isCompatEnabled(DeprecationTypes.CONFIG_IGNORED_ELEMENTS, null) &&
!isRuntimeOnly() &&
isArray(val)
) {
app.config.compilerOptions.isCustomElement = tag => {
return val.some(v => (isString(v) ? v === tag : v.test(tag)))
}
}
}
isCopyingConfig = false
applySingletonPrototype(app, singletonCtor)
}
function applySingletonPrototype(app: App, Ctor: Function) {
// copy prototype augmentations as config.globalProperties
const enabled = isCompatEnabled(DeprecationTypes.GLOBAL_PROTOTYPE, null)
if (enabled) {
app.config.globalProperties = Object.create(Ctor.prototype)
}
let hasPrototypeAugmentations = false
const descriptors = Object.getOwnPropertyDescriptors(Ctor.prototype)
for (const key in descriptors) {
if (key !== 'constructor') {
hasPrototypeAugmentations = true
if (enabled) {
Object.defineProperty(
app.config.globalProperties,
key,
descriptors[key]
)
}
}
}
if (__DEV__ && hasPrototypeAugmentations) {
warnDeprecation(DeprecationTypes.GLOBAL_PROTOTYPE, null)
}
}
function installCompatMount(
2021-04-05 22:13:29 +00:00
app: App,
context: AppContext,
render: RootRenderFunction
2021-04-05 22:13:29 +00:00
) {
let isMounted = false
/**
* 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 component = app._component
const vnode = createVNode(component, options.propsData || null)
vnode.appContext = context
const hasNoRender =
!isFunction(component) && !component.render && !component.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
}
2021-04-06 16:17:17 +00:00
setupComponent(instance)
2021-04-05 22:13:29 +00:00
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.
2021-04-27 21:34:19 +00:00
instance.ctx._compat_mount = (selectorOrEl?: string | Element) => {
2021-04-05 22:13:29 +00:00
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 {
2021-04-27 21:34:19 +00:00
// eslint-disable-next-line
container = selectorOrEl || document.createElement('div')
2021-04-05 22:13:29 +00:00
}
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)) {
2021-04-09 22:52:14 +00:00
warnDeprecation(DeprecationTypes.GLOBAL_MOUNT_CONTAINER, null)
2021-04-05 22:13:29 +00:00
break
}
}
}
instance.render = null
;(component as ComponentOptions).template = container.innerHTML
2021-04-06 16:17:17 +00:00
finishComponentSetup(instance, false, true /* skip options */)
2021-04-05 22:13:29 +00:00
}
// 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 = () => {
if (isMounted) {
render(null, app._container)
if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
devtoolsUnmountApp(app)
}
delete app._container.__vue_app__
} else {
const { bum, effects, um } = instance
// beforeDestroy hooks
if (bum) {
invokeArrayFns(bum)
}
if (isCompatEnabled(DeprecationTypes.INSTANCE_EVENT_HOOKS, instance)) {
instance.emit('hook:beforeDestroy')
}
// stop effects
if (effects) {
for (let i = 0; i < effects.length; i++) {
stop(effects[i])
}
}
// unmounted hook
if (um) {
invokeArrayFns(um)
}
if (isCompatEnabled(DeprecationTypes.INSTANCE_EVENT_HOOKS, instance)) {
instance.emit('hook:destroyed')
}
}
}
2021-04-05 22:13:29 +00:00
return instance.proxy!
}
}
2021-04-11 20:53:43 +00:00
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
const patched = new WeakSet<object>()
function defineReactive(obj: any, key: string, val: any) {
// it's possible for the orignial object to be mutated after being defined
// and expecting reactivity... we are covering it here because this seems to
// be a bit more common.
if (isObject(val) && !isReactive(val) && !patched.has(val)) {
const reactiveVal = reactive(val)
if (isArray(val)) {
methodsToPatch.forEach(m => {
// @ts-ignore
val[m] = (...args: any[]) => {
// @ts-ignore
Array.prototype[m].call(reactiveVal, ...args)
}
})
} else {
Object.keys(val).forEach(key => {
2021-04-22 21:30:54 +00:00
try {
defineReactiveSimple(val, key, val[key])
} catch (e) {}
2021-04-11 20:53:43 +00:00
})
}
}
const i = obj.$
if (i && obj === i.proxy) {
// target is a Vue instance - define on instance.ctx
defineReactiveSimple(i.ctx, key, val)
2021-04-11 20:53:43 +00:00
i.accessCache = Object.create(null)
} else if (isReactive(obj)) {
obj[key] = val
} else {
defineReactiveSimple(obj, key, val)
}
}
function defineReactiveSimple(obj: any, key: string, val: any) {
val = isObject(val) ? reactive(val) : val
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
track(obj, TrackOpTypes.GET, key)
return val
},
set(newVal) {
val = isObject(newVal) ? reactive(newVal) : newVal
trigger(obj, TriggerOpTypes.SET, key, newVal)
}
})
}