wip: switch to new implementation
This commit is contained in:
parent
35effdee5a
commit
3cded86b98
@ -1,214 +0,0 @@
|
||||
import { EMPTY_OBJ, NOOP } from '@vue/shared'
|
||||
import { VNode, Slots, RenderNode, MountedVNode } from './vdom'
|
||||
import {
|
||||
Data,
|
||||
ComponentOptions,
|
||||
ComponentClassOptions,
|
||||
ComponentPropsOptions,
|
||||
WatchOptions
|
||||
} from './componentOptions'
|
||||
import { setupWatcher } from './componentWatch'
|
||||
import { ReactiveEffect, DebuggerEvent, ComputedGetter } from '@vue/observer'
|
||||
import { nextTick } from '@vue/scheduler'
|
||||
import { ErrorTypes } from './errorHandling'
|
||||
import { initializeComponentInstance } from './componentInstance'
|
||||
import { EventEmitter, invokeListeners } from './optional/eventEmitter'
|
||||
import { warn } from './warning'
|
||||
import { ComponentProxy } from './componentProxy'
|
||||
|
||||
// public component instance type
|
||||
export interface Component<P = {}, D = {}> extends PublicInstanceMethods {
|
||||
readonly $el: any
|
||||
readonly $vnode: MountedVNode
|
||||
readonly $parentVNode: MountedVNode
|
||||
readonly $data: D
|
||||
readonly $props: Readonly<P>
|
||||
readonly $attrs: Readonly<Data>
|
||||
readonly $slots: Slots
|
||||
readonly $root: Component
|
||||
readonly $parent: Component
|
||||
readonly $children: Component[]
|
||||
readonly $options: ComponentOptions<P, D, this>
|
||||
readonly $refs: Record<string | symbol, any>
|
||||
readonly $proxy: this
|
||||
}
|
||||
|
||||
interface PublicInstanceMethods {
|
||||
$forceUpdate(): void
|
||||
$nextTick(fn: () => any): Promise<void>
|
||||
$watch(
|
||||
keyOrFn: string | ((this: this) => any),
|
||||
cb: (this: this, newValue: any, oldValue: any) => void,
|
||||
options?: WatchOptions
|
||||
): () => void
|
||||
$emit(name: string, ...payload: any[]): void
|
||||
}
|
||||
|
||||
export interface APIMethods<P = {}, D = {}> {
|
||||
data(): Partial<D>
|
||||
render(props: Readonly<P>, slots: Slots, attrs: Data, parentVNode: VNode): any
|
||||
}
|
||||
|
||||
export interface LifecycleMethods {
|
||||
beforeCreate(): void
|
||||
created(): void
|
||||
beforeMount(): void
|
||||
mounted(): void
|
||||
beforeUpdate(vnode: VNode): void
|
||||
updated(vnode: VNode): void
|
||||
beforeUnmount(): void
|
||||
unmounted(): void
|
||||
errorCaptured(): (
|
||||
err: Error,
|
||||
type: ErrorTypes,
|
||||
instance: ComponentInstance | null,
|
||||
vnode: VNode
|
||||
) => boolean | void
|
||||
activated(): void
|
||||
deactivated(): void
|
||||
renderTracked(e: DebuggerEvent): void
|
||||
renderTriggered(e: DebuggerEvent): void
|
||||
}
|
||||
|
||||
export interface ComponentClass extends ComponentClassOptions {
|
||||
options?: ComponentOptions
|
||||
new <P = {}, D = {}>(): Component<P, D>
|
||||
}
|
||||
|
||||
export interface FunctionalComponent<P = {}> {
|
||||
(props: P, slots: Slots, attrs: Data, parentVNode: VNode): any
|
||||
props?: ComponentPropsOptions<P>
|
||||
displayName?: string
|
||||
}
|
||||
|
||||
export type ComponentType = ComponentClass | FunctionalComponent
|
||||
|
||||
// Internal type that represents a mounted instance.
|
||||
// It extends ComponentImplementation with mounted instance properties.
|
||||
export interface ComponentInstance<P = {}, D = {}>
|
||||
extends ComponentImplementation,
|
||||
Partial<APIMethods<P, D>>,
|
||||
Partial<LifecycleMethods> {
|
||||
constructor: ComponentClass
|
||||
render: APIMethods<P, D>['render']
|
||||
|
||||
$vnode: MountedVNode
|
||||
$data: D
|
||||
$props: P
|
||||
$attrs: Data
|
||||
$slots: Slots
|
||||
$root: ComponentProxy
|
||||
$children: ComponentProxy[]
|
||||
$options: ComponentOptions<P, D>
|
||||
$proxy: ComponentProxy<this>
|
||||
|
||||
_update: ReactiveEffect
|
||||
_queueJob: ((fn: () => void) => void)
|
||||
}
|
||||
|
||||
// actual implementation of the component
|
||||
class ComponentImplementation implements PublicInstanceMethods {
|
||||
get $el(): any {
|
||||
const el = this.$vnode && this.$vnode.el
|
||||
return typeof el === 'function' ? (el as any)() : el
|
||||
}
|
||||
|
||||
$vnode: VNode | null = null
|
||||
$parentVNode: VNode | null = null
|
||||
$data: Data | null = null
|
||||
$props: Data | null = null
|
||||
$attrs: Data | null = null
|
||||
$slots: Slots | null = null
|
||||
$root: ComponentProxy | null = null
|
||||
$parent: ComponentProxy | null = null
|
||||
$children: ComponentProxy[] = []
|
||||
$options: ComponentOptions | null = null
|
||||
$refs: Record<string, ComponentInstance | RenderNode> = {}
|
||||
$proxy: ComponentProxy<this> | null = null
|
||||
|
||||
_rawData: Data | null = null
|
||||
_computedGetters: Record<string, ComputedGetter> | null = null
|
||||
_watchHandles: Set<ReactiveEffect> | null = null
|
||||
_mounted: boolean = false
|
||||
_unmounted: boolean = false
|
||||
_events: { [event: string]: Function[] | null } | null = null
|
||||
_update: ReactiveEffect | null = null
|
||||
_queueJob: ((fn: () => void) => void) | null = null
|
||||
_isVue: boolean = true
|
||||
_inactiveRoot: boolean = false
|
||||
|
||||
constructor(props?: object) {
|
||||
if (props === void 0) {
|
||||
// When invoked without any arguments, this is the default path where
|
||||
// we initiailize a proper component instance. Note the returned value
|
||||
// here is actually a proxy of the raw instance (and will be the `this`
|
||||
// context) in all sub-class methods, including the constructor!
|
||||
return initializeComponentInstance(this as any) as any
|
||||
} else {
|
||||
// the presence of the props argument indicates that this class is being
|
||||
// instantiated as a mixin, and should expose the props on itself
|
||||
// so that the extended class constructor (and property initializers) can
|
||||
// access $props.
|
||||
this.$props = props
|
||||
Object.assign(this, props)
|
||||
}
|
||||
if (__COMPAT__) {
|
||||
;(this as any)._eventEmitter = new EventEmitter(this)
|
||||
}
|
||||
}
|
||||
|
||||
// necessary to tell this apart from a functional
|
||||
render(...args: any[]): any {
|
||||
if (__DEV__) {
|
||||
const name =
|
||||
(this.$options && this.$options.displayName) || this.constructor.name
|
||||
warn(`Class component \`${name}\` is missing render() method.`)
|
||||
}
|
||||
}
|
||||
|
||||
// to be set by renderer during mount
|
||||
$forceUpdate: () => void = NOOP
|
||||
|
||||
$nextTick(fn: () => any): Promise<void> {
|
||||
return nextTick(fn)
|
||||
}
|
||||
|
||||
$watch(
|
||||
keyOrFn: string | ((this: this) => any),
|
||||
cb: (this: this, newValue: any, oldValue: any) => void,
|
||||
options?: WatchOptions
|
||||
): () => void {
|
||||
return setupWatcher(this as any, keyOrFn, cb, options)
|
||||
}
|
||||
|
||||
$emit(name: string, ...payload: any[]) {
|
||||
const parentData =
|
||||
(this.$parentVNode && this.$parentVNode.data) || EMPTY_OBJ
|
||||
const parentListener =
|
||||
parentData['on' + name] || parentData['on' + name.toLowerCase()]
|
||||
if (parentListener) {
|
||||
invokeListeners(parentListener, payload)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// legacy event emitter interface exposed on component instances
|
||||
if (__COMPAT__) {
|
||||
const p = ComponentImplementation.prototype as any
|
||||
;['on', 'off', 'once'].forEach(key => {
|
||||
p['$' + key] = function(...args: any[]) {
|
||||
this._eventEmitter[key](...args)
|
||||
return this
|
||||
}
|
||||
})
|
||||
const emit = p.$emit
|
||||
p.$emit = function(...args: any[]) {
|
||||
emit.call(this, ...args)
|
||||
this._eventEmitter.emit(...args)
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
// the exported Component has the implementation details of the actual
|
||||
// ComponentImplementation class but with proper type inference of ComponentClass.
|
||||
export const Component = ComponentImplementation as ComponentClass
|
@ -1,31 +0,0 @@
|
||||
import { NOOP, isFunction } from '@vue/shared'
|
||||
import { computed, stop, ComputedGetter } from '@vue/observer'
|
||||
import { ComponentInstance } from './component'
|
||||
import { ComponentComputedOptions } from './componentOptions'
|
||||
|
||||
export type ComputedHandles = Record<string, ComputedGetter>
|
||||
|
||||
export function initializeComputed(
|
||||
instance: ComponentInstance,
|
||||
computedOptions: ComponentComputedOptions | undefined
|
||||
) {
|
||||
if (!computedOptions) {
|
||||
return
|
||||
}
|
||||
const handles: ComputedHandles = (instance._computedGetters = {})
|
||||
const proxy = instance.$proxy
|
||||
for (const key in computedOptions) {
|
||||
const option = computedOptions[key]
|
||||
const getter = isFunction(option) ? option : option.get || NOOP
|
||||
handles[key] = computed(getter, proxy)
|
||||
}
|
||||
}
|
||||
|
||||
export function teardownComputed(instance: ComponentInstance) {
|
||||
const handles = instance._computedGetters
|
||||
if (handles !== null) {
|
||||
for (const key in handles) {
|
||||
stop(handles[key].effect)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,106 +0,0 @@
|
||||
import { VNode, MountedVNode } from './vdom'
|
||||
import { ComponentInstance, ComponentClass } from './component'
|
||||
import { initializeState } from './componentState'
|
||||
import { initializeProps } from './componentProps'
|
||||
import { initializeWatch, teardownWatch } from './componentWatch'
|
||||
import { initializeComputed, teardownComputed } from './componentComputed'
|
||||
import { ComponentProxy, createRenderProxy } from './componentProxy'
|
||||
import { resolveComponentOptionsFromClass } from './componentOptions'
|
||||
import { VNodeFlags } from './flags'
|
||||
import { ErrorTypes, callLifecycleHookWithHandler } from './errorHandling'
|
||||
import { stop } from '@vue/observer'
|
||||
import { EMPTY_OBJ } from '@vue/shared'
|
||||
|
||||
let currentVNode: VNode | null = null
|
||||
let currentContextVNode: VNode | null = null
|
||||
|
||||
export function createComponentInstance(vnode: VNode): ComponentInstance {
|
||||
// component instance creation is done in two steps.
|
||||
// first, `initializeComponentInstance` is called inside base component
|
||||
// constructor as the instance is created so that the extended component's
|
||||
// constructor has access to public properties and most importantly props.
|
||||
// we are storing the vnodes in variables here so that there's no need to
|
||||
// always pass args in super()
|
||||
currentVNode = vnode
|
||||
currentContextVNode = vnode.contextVNode
|
||||
const Component = vnode.tag as ComponentClass
|
||||
const instanceProxy = new Component() as ComponentProxy
|
||||
const instance = instanceProxy._self
|
||||
|
||||
// then we finish the initialization by collecting properties set on the
|
||||
// instance
|
||||
const {
|
||||
$options: { created, computed, watch }
|
||||
} = instance
|
||||
initializeState(instance, !Component.fromOptions)
|
||||
initializeComputed(instance, computed)
|
||||
initializeWatch(instance, watch)
|
||||
instance.$slots = currentVNode.slots || EMPTY_OBJ
|
||||
|
||||
if (created) {
|
||||
callLifecycleHookWithHandler(created, instanceProxy, ErrorTypes.CREATED)
|
||||
}
|
||||
|
||||
currentVNode = currentContextVNode = null
|
||||
return instance
|
||||
}
|
||||
|
||||
// this is called inside the base component's constructor
|
||||
// it initializes all the way up to props so that they are available
|
||||
// inside the extended component's constructor, and returns the proxy of the
|
||||
// raw instance.
|
||||
export function initializeComponentInstance<T extends ComponentInstance>(
|
||||
instance: T
|
||||
): ComponentProxy<T> {
|
||||
if (__DEV__ && currentVNode === null) {
|
||||
throw new Error(
|
||||
`Component classes are not meant to be manually instantiated.`
|
||||
)
|
||||
}
|
||||
|
||||
instance.$options = resolveComponentOptionsFromClass(instance.constructor)
|
||||
instance.$parentVNode = currentVNode as MountedVNode
|
||||
|
||||
// renderProxy
|
||||
const proxy = (instance.$proxy = createRenderProxy(instance))
|
||||
|
||||
// parent chain management
|
||||
if (currentContextVNode !== null) {
|
||||
// locate first non-functional parent
|
||||
while (currentContextVNode !== null) {
|
||||
if ((currentContextVNode.flags & VNodeFlags.COMPONENT_STATEFUL) > 0) {
|
||||
const parentComponent = (currentContextVNode as VNode)
|
||||
.children as ComponentInstance
|
||||
instance.$parent = parentComponent.$proxy
|
||||
instance.$root = parentComponent.$root
|
||||
parentComponent.$children.push(proxy)
|
||||
break
|
||||
}
|
||||
currentContextVNode = currentContextVNode.contextVNode
|
||||
}
|
||||
} else {
|
||||
instance.$root = proxy
|
||||
}
|
||||
|
||||
// beforeCreate hook is called right in the constructor
|
||||
const { beforeCreate, props } = instance.$options
|
||||
if (beforeCreate) {
|
||||
callLifecycleHookWithHandler(beforeCreate, proxy, ErrorTypes.BEFORE_CREATE)
|
||||
}
|
||||
initializeProps(instance, props, (currentVNode as VNode).data)
|
||||
|
||||
return proxy
|
||||
}
|
||||
|
||||
export function teardownComponentInstance(instance: ComponentInstance) {
|
||||
const parentComponent = instance.$parent && instance.$parent._self
|
||||
if (parentComponent && !parentComponent._unmounted) {
|
||||
parentComponent.$children.splice(
|
||||
parentComponent.$children.indexOf(instance.$proxy),
|
||||
1
|
||||
)
|
||||
}
|
||||
stop(instance._update)
|
||||
teardownComputed(instance)
|
||||
teardownWatch(instance)
|
||||
}
|
@ -1,283 +0,0 @@
|
||||
import {
|
||||
Component,
|
||||
ComponentInstance,
|
||||
ComponentClass,
|
||||
APIMethods,
|
||||
LifecycleMethods
|
||||
} from './component'
|
||||
import { isArray, isObject, isFunction } from '@vue/shared'
|
||||
import { normalizePropsOptions } from './componentProps'
|
||||
import { warn } from './warning'
|
||||
import { h } from './h'
|
||||
|
||||
export type Data = Record<string, any>
|
||||
|
||||
export interface ComponentClassOptions<P = {}, This = ComponentInstance> {
|
||||
props?: ComponentPropsOptions<P>
|
||||
computed?: ComponentComputedOptions<This>
|
||||
watch?: ComponentWatchOptions<This>
|
||||
displayName?: string
|
||||
fromOptions?: boolean
|
||||
}
|
||||
|
||||
export interface ComponentOptions<
|
||||
P = {},
|
||||
D = {},
|
||||
This = ComponentInstance<P, D>
|
||||
>
|
||||
extends ComponentClassOptions<P, This>,
|
||||
Partial<APIMethods<P, D>>,
|
||||
Partial<LifecycleMethods> {
|
||||
// TODO other options
|
||||
readonly [key: string]: any
|
||||
}
|
||||
|
||||
export type ComponentPropsOptions<P = Data> = {
|
||||
[K in keyof P]: PropValidator<P[K]>
|
||||
}
|
||||
|
||||
export type Prop<T> = { (): T } | { new (...args: any[]): T & object }
|
||||
|
||||
export type PropType<T> = Prop<T> | Prop<T>[]
|
||||
|
||||
export type PropValidator<T> = PropOptions<T> | PropType<T>
|
||||
|
||||
export interface PropOptions<T = any> {
|
||||
type?: PropType<T> | true | null
|
||||
required?: boolean
|
||||
default?: T | null | undefined | (() => T | null | undefined)
|
||||
validator?(value: T): boolean
|
||||
}
|
||||
|
||||
export interface ComponentComputedOptions<This = ComponentInstance> {
|
||||
[key: string]: ((this: This, c: This) => any) | SingleComputedOptions<This>
|
||||
}
|
||||
|
||||
type SingleComputedOptions<This> = {
|
||||
get: (this: This, c: This) => any
|
||||
set?: (value: any) => void
|
||||
cache?: boolean
|
||||
}
|
||||
|
||||
export interface ComponentWatchOptions<This = ComponentInstance> {
|
||||
[key: string]: ComponentWatchOption<This>
|
||||
}
|
||||
|
||||
export type ComponentWatchOption<This = ComponentInstance> =
|
||||
| WatchHandler<This>
|
||||
| WatchHandler<This>[]
|
||||
| WatchOptionsWithHandler<This>
|
||||
| string
|
||||
|
||||
export type WatchHandler<This = any> = (
|
||||
this: This,
|
||||
val: any,
|
||||
oldVal: any
|
||||
) => void
|
||||
|
||||
export interface WatchOptionsWithHandler<This = any> extends WatchOptions {
|
||||
handler: WatchHandler<This>
|
||||
}
|
||||
|
||||
export interface WatchOptions {
|
||||
sync?: boolean
|
||||
deep?: boolean
|
||||
immediate?: boolean
|
||||
}
|
||||
|
||||
type ReservedKeys = { [K in keyof (APIMethods & LifecycleMethods)]: 1 }
|
||||
|
||||
export const reservedMethods: ReservedKeys = {
|
||||
data: 1,
|
||||
render: 1,
|
||||
beforeCreate: 1,
|
||||
created: 1,
|
||||
beforeMount: 1,
|
||||
mounted: 1,
|
||||
beforeUpdate: 1,
|
||||
updated: 1,
|
||||
beforeUnmount: 1,
|
||||
unmounted: 1,
|
||||
errorCaptured: 1,
|
||||
activated: 1,
|
||||
deactivated: 1,
|
||||
renderTracked: 1,
|
||||
renderTriggered: 1
|
||||
}
|
||||
|
||||
export function isReservedKey(key: string): boolean {
|
||||
return key[0] === '_' || key[0] === '$' || reservedMethods.hasOwnProperty(key)
|
||||
}
|
||||
|
||||
// This is a special marker from the @prop decorator.
|
||||
// The decorator stores prop options on the Class' prototype as __prop_xxx
|
||||
const propPrefixRE = /^__prop_/
|
||||
|
||||
// This is called in the base component constructor and the return value is
|
||||
// set on the instance as $options.
|
||||
export function resolveComponentOptionsFromClass(
|
||||
Class: ComponentClass
|
||||
): ComponentOptions {
|
||||
if (Class.hasOwnProperty('options')) {
|
||||
return Class.options as ComponentOptions
|
||||
}
|
||||
let options = {} as any
|
||||
|
||||
const staticDescriptors = Object.getOwnPropertyDescriptors(Class)
|
||||
for (const key in staticDescriptors) {
|
||||
const { enumerable, get, value } = staticDescriptors[key]
|
||||
if (enumerable || get) {
|
||||
options[key] = get ? get() : value
|
||||
}
|
||||
}
|
||||
|
||||
// pre-normalize array props options into object.
|
||||
// we may need to attach more props to it (declared by decorators)
|
||||
if (Array.isArray(options.props)) {
|
||||
options.props = normalizePropsOptions(options.props)
|
||||
}
|
||||
|
||||
const instanceDescriptors = Object.getOwnPropertyDescriptors(Class.prototype)
|
||||
for (const key in instanceDescriptors) {
|
||||
const { get, value } = instanceDescriptors[key]
|
||||
if (get) {
|
||||
// computed properties
|
||||
;(options.computed || (options.computed = {}))[key] = get
|
||||
// there's no need to do anything for the setter
|
||||
// as it's already defined on the prototype
|
||||
} else if (isFunction(value) && key !== 'constructor') {
|
||||
if (key in reservedMethods) {
|
||||
// lifecycle hooks / reserved methods
|
||||
options[key] = value
|
||||
} else {
|
||||
// normal methods
|
||||
;(options.methods || (options.methods = {}))[key] = value
|
||||
}
|
||||
} else if (propPrefixRE.test(key)) {
|
||||
// decorator-declared props
|
||||
const propName = key.replace(propPrefixRE, '')
|
||||
;(options.props || (options.props = {}))[propName] = value
|
||||
}
|
||||
}
|
||||
|
||||
// post-normalize all prop options into same object format
|
||||
if (options.props) {
|
||||
options.props = normalizePropsOptions(options.props)
|
||||
}
|
||||
|
||||
const ParentClass = Object.getPrototypeOf(Class)
|
||||
if (ParentClass !== Component) {
|
||||
const parentOptions = resolveComponentOptionsFromClass(ParentClass)
|
||||
options = mergeComponentOptions(parentOptions, options)
|
||||
}
|
||||
|
||||
Class.options = options
|
||||
return options
|
||||
}
|
||||
|
||||
export function createComponentClassFromOptions(
|
||||
options: ComponentOptions
|
||||
): ComponentClass {
|
||||
class AnonymousComponent extends Component {
|
||||
static options = options
|
||||
// indicate this component was created from options
|
||||
static fromOptions = true
|
||||
}
|
||||
const proto = AnonymousComponent.prototype as any
|
||||
for (const key in options) {
|
||||
const value = options[key]
|
||||
if (key === 'render') {
|
||||
if (__COMPAT__) {
|
||||
options.render = function() {
|
||||
return value.call(this, h)
|
||||
}
|
||||
}
|
||||
// so that we can call instance.render directly
|
||||
proto.render = options.render
|
||||
} else if (key === 'computed') {
|
||||
// create computed setters on prototype
|
||||
// (getters are handled by the render proxy)
|
||||
for (const computedKey in value) {
|
||||
const computed = value[computedKey]
|
||||
const set = isObject(computed) && computed.set
|
||||
if (set) {
|
||||
Object.defineProperty(proto, computedKey, {
|
||||
configurable: true,
|
||||
set
|
||||
})
|
||||
}
|
||||
}
|
||||
} else if (key === 'methods') {
|
||||
for (const method in value) {
|
||||
if (__DEV__ && proto.hasOwnProperty(method)) {
|
||||
warn(
|
||||
`Object syntax contains method name that conflicts with ` +
|
||||
`lifecycle hook: "${method}"`
|
||||
)
|
||||
}
|
||||
proto[method] = value[method]
|
||||
}
|
||||
} else if (__COMPAT__) {
|
||||
if (key === 'name') {
|
||||
options.displayName = value
|
||||
} else if (key === 'render') {
|
||||
options.render = function() {
|
||||
return value.call(this, h)
|
||||
}
|
||||
} else if (key === 'beforeDestroy') {
|
||||
options.beforeUnmount = value
|
||||
} else if (key === 'destroyed') {
|
||||
options.unmounted = value
|
||||
}
|
||||
}
|
||||
}
|
||||
return AnonymousComponent as ComponentClass
|
||||
}
|
||||
|
||||
export function mergeComponentOptions(to: any, from: any): ComponentOptions {
|
||||
const res: any = Object.assign({}, to)
|
||||
if (from.mixins) {
|
||||
from.mixins.forEach((mixin: any) => {
|
||||
from = mergeComponentOptions(from, mixin)
|
||||
})
|
||||
}
|
||||
for (const key in from) {
|
||||
const value = from[key]
|
||||
const existing = res[key]
|
||||
if (isFunction(value) && isFunction(existing)) {
|
||||
if (key === 'data') {
|
||||
// for data we need to merge the returned value
|
||||
res[key] = mergeDataFn(existing, value)
|
||||
} else if (/^render|^errorCaptured/.test(key)) {
|
||||
// render, renderTracked, renderTriggered & errorCaptured
|
||||
// are never merged
|
||||
res[key] = value
|
||||
} else {
|
||||
// merge lifecycle hooks
|
||||
res[key] = mergeLifecycleHooks(existing, value)
|
||||
}
|
||||
} else if (isArray(value) && isArray(existing)) {
|
||||
res[key] = existing.concat(value)
|
||||
} else if (isObject(value) && isObject(existing)) {
|
||||
res[key] = Object.assign({}, existing, value)
|
||||
} else {
|
||||
res[key] = value
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
export function mergeLifecycleHooks(a: Function, b: Function): Function {
|
||||
return function(...args: any[]) {
|
||||
a.call(this, ...args)
|
||||
b.call(this, ...args)
|
||||
}
|
||||
}
|
||||
|
||||
export function mergeDataFn(a: Function, b: Function): Function {
|
||||
// TODO: backwards compat requires recursive merge,
|
||||
// but maybe we should just warn if we detect clashing keys
|
||||
return function() {
|
||||
return Object.assign(a.call(this), b.call(this))
|
||||
}
|
||||
}
|
@ -1,309 +0,0 @@
|
||||
import { immutable, unwrap } from '@vue/observer'
|
||||
import { ComponentInstance } from './component'
|
||||
import {
|
||||
Data,
|
||||
PropOptions,
|
||||
Prop,
|
||||
PropType,
|
||||
ComponentPropsOptions,
|
||||
isReservedKey
|
||||
} from './componentOptions'
|
||||
import {
|
||||
EMPTY_OBJ,
|
||||
camelize,
|
||||
hyphenate,
|
||||
capitalize,
|
||||
isString,
|
||||
isFunction,
|
||||
isArray,
|
||||
isObject
|
||||
} from '@vue/shared'
|
||||
import { warn } from './warning'
|
||||
|
||||
const enum BooleanFlags {
|
||||
shouldCast = '1',
|
||||
shouldCastTrue = '2'
|
||||
}
|
||||
|
||||
type NormalizedProp = PropOptions & {
|
||||
[BooleanFlags.shouldCast]?: boolean
|
||||
[BooleanFlags.shouldCastTrue]?: boolean
|
||||
}
|
||||
|
||||
type NormalizedPropsOptions = Record<string, NormalizedProp>
|
||||
|
||||
export function initializeProps(
|
||||
instance: ComponentInstance,
|
||||
options: NormalizedPropsOptions | undefined,
|
||||
data: Data | null
|
||||
) {
|
||||
const { 0: props, 1: attrs } = resolveProps(data, options)
|
||||
instance.$props = __DEV__ ? immutable(props) : props
|
||||
instance.$attrs = options
|
||||
? __DEV__
|
||||
? immutable(attrs)
|
||||
: attrs
|
||||
: instance.$props
|
||||
}
|
||||
|
||||
// resolve raw VNode data.
|
||||
// - filter out reserved keys (key, ref, slots)
|
||||
// - extract class and style into $attrs (to be merged onto child
|
||||
// component root)
|
||||
// - for the rest:
|
||||
// - if has declared props: put declared ones in `props`, the rest in `attrs`
|
||||
// - else: everything goes in `props`.
|
||||
|
||||
const EMPTY_PROPS = [EMPTY_OBJ, EMPTY_OBJ] as [Data, Data]
|
||||
|
||||
export function resolveProps(
|
||||
rawData: any,
|
||||
_options: NormalizedPropsOptions | void
|
||||
): [Data, Data] {
|
||||
const hasDeclaredProps = _options != null
|
||||
const options = _options as NormalizedPropsOptions
|
||||
if (!rawData && !hasDeclaredProps) {
|
||||
return EMPTY_PROPS
|
||||
}
|
||||
const props: any = {}
|
||||
let attrs: any = void 0
|
||||
if (rawData != null) {
|
||||
for (const key in rawData) {
|
||||
// key, ref, slots are reserved
|
||||
if (key === 'key' || key === 'ref' || key === 'slots') {
|
||||
continue
|
||||
}
|
||||
// any non-declared data are put into a separate `attrs` object
|
||||
// for spreading
|
||||
if (hasDeclaredProps && !options.hasOwnProperty(key)) {
|
||||
;(attrs || (attrs = {}))[key] = rawData[key]
|
||||
} else {
|
||||
props[key] = rawData[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
// set default values, cast booleans & run validators
|
||||
if (hasDeclaredProps) {
|
||||
for (const key in options) {
|
||||
let opt = options[key]
|
||||
if (opt == null) continue
|
||||
const isAbsent = !props.hasOwnProperty(key)
|
||||
const hasDefault = opt.hasOwnProperty('default')
|
||||
const currentValue = props[key]
|
||||
// default values
|
||||
if (hasDefault && currentValue === undefined) {
|
||||
const defaultValue = opt.default
|
||||
props[key] = isFunction(defaultValue) ? defaultValue() : defaultValue
|
||||
}
|
||||
// boolean casting
|
||||
if (opt[BooleanFlags.shouldCast]) {
|
||||
if (isAbsent && !hasDefault) {
|
||||
props[key] = false
|
||||
} else if (
|
||||
opt[BooleanFlags.shouldCastTrue] &&
|
||||
(currentValue === '' || currentValue === hyphenate(key))
|
||||
) {
|
||||
props[key] = true
|
||||
}
|
||||
}
|
||||
// runtime validation
|
||||
if (__DEV__ && rawData) {
|
||||
validateProp(key, unwrap(rawData[key]), opt, isAbsent)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// if component has no declared props, $attrs === $props
|
||||
attrs = props
|
||||
}
|
||||
return [props, attrs]
|
||||
}
|
||||
|
||||
export function normalizePropsOptions(
|
||||
raw: ComponentPropsOptions | void
|
||||
): NormalizedPropsOptions | void {
|
||||
if (!raw) {
|
||||
return
|
||||
}
|
||||
const normalized: NormalizedPropsOptions = {}
|
||||
if (isArray(raw)) {
|
||||
for (let i = 0; i < raw.length; i++) {
|
||||
if (__DEV__ && !isString(raw[i])) {
|
||||
warn(`props must be strings when using array syntax.`, raw[i])
|
||||
}
|
||||
const normalizedKey = camelize(raw[i])
|
||||
if (!isReservedKey(normalizedKey)) {
|
||||
normalized[normalizedKey] = EMPTY_OBJ
|
||||
} else if (__DEV__) {
|
||||
warn(`Invalid prop name: "${normalizedKey}" is a reserved property.`)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (__DEV__ && !isObject(raw)) {
|
||||
warn(`invalid props options`, raw)
|
||||
}
|
||||
for (const key in raw) {
|
||||
const normalizedKey = camelize(key)
|
||||
if (!isReservedKey(normalizedKey)) {
|
||||
const opt = raw[key]
|
||||
const prop = (normalized[normalizedKey] =
|
||||
isArray(opt) || isFunction(opt) ? { type: opt } : opt)
|
||||
if (prop) {
|
||||
const booleanIndex = getTypeIndex(Boolean, prop.type)
|
||||
const stringIndex = getTypeIndex(String, prop.type)
|
||||
;(prop as NormalizedProp)[BooleanFlags.shouldCast] = booleanIndex > -1
|
||||
;(prop as NormalizedProp)[BooleanFlags.shouldCastTrue] =
|
||||
booleanIndex < stringIndex
|
||||
}
|
||||
} else if (__DEV__) {
|
||||
warn(`Invalid prop name: "${normalizedKey}" is a reserved property.`)
|
||||
}
|
||||
}
|
||||
}
|
||||
return normalized
|
||||
}
|
||||
|
||||
// use function string name to check type constructors
|
||||
// so that it works across vms / iframes.
|
||||
function getType(ctor: Prop<any>): string {
|
||||
const match = ctor && ctor.toString().match(/^\s*function (\w+)/)
|
||||
return match ? match[1] : ''
|
||||
}
|
||||
|
||||
function isSameType(a: Prop<any>, b: Prop<any>): boolean {
|
||||
return getType(a) === getType(b)
|
||||
}
|
||||
|
||||
function getTypeIndex(
|
||||
type: Prop<any>,
|
||||
expectedTypes: PropType<any> | void | null | true
|
||||
): number {
|
||||
if (isArray(expectedTypes)) {
|
||||
for (let i = 0, len = expectedTypes.length; i < len; i++) {
|
||||
if (isSameType(expectedTypes[i], type)) {
|
||||
return i
|
||||
}
|
||||
}
|
||||
} else if (isObject(expectedTypes)) {
|
||||
return isSameType(expectedTypes, type) ? 0 : -1
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
type AssertionResult = {
|
||||
valid: boolean
|
||||
expectedType: string
|
||||
}
|
||||
|
||||
function validateProp(
|
||||
name: string,
|
||||
value: any,
|
||||
prop: PropOptions<any>,
|
||||
isAbsent: boolean
|
||||
) {
|
||||
const { type, required, validator } = prop
|
||||
// required!
|
||||
if (required && isAbsent) {
|
||||
warn('Missing required prop: "' + name + '"')
|
||||
return
|
||||
}
|
||||
// missing but optional
|
||||
if (value == null && !prop.required) {
|
||||
return
|
||||
}
|
||||
// type check
|
||||
if (type != null && type !== true) {
|
||||
let isValid = false
|
||||
const types = isArray(type) ? type : [type]
|
||||
const expectedTypes = []
|
||||
// value is valid as long as one of the specified types match
|
||||
for (let i = 0; i < types.length && !isValid; i++) {
|
||||
const { valid, expectedType } = assertType(value, types[i])
|
||||
expectedTypes.push(expectedType || '')
|
||||
isValid = valid
|
||||
}
|
||||
if (!isValid) {
|
||||
warn(getInvalidTypeMessage(name, value, expectedTypes))
|
||||
return
|
||||
}
|
||||
}
|
||||
// custom validator
|
||||
if (validator && !validator(value)) {
|
||||
warn('Invalid prop: custom validator check failed for prop "' + name + '".')
|
||||
}
|
||||
}
|
||||
|
||||
const simpleCheckRE = /^(String|Number|Boolean|Function|Symbol)$/
|
||||
|
||||
function assertType(value: any, type: Prop<any>): AssertionResult {
|
||||
let valid
|
||||
const expectedType = getType(type)
|
||||
if (simpleCheckRE.test(expectedType)) {
|
||||
const t = typeof value
|
||||
valid = t === expectedType.toLowerCase()
|
||||
// for primitive wrapper objects
|
||||
if (!valid && t === 'object') {
|
||||
valid = value instanceof type
|
||||
}
|
||||
} else if (expectedType === 'Object') {
|
||||
valid = toRawType(value) === 'Object'
|
||||
} else if (expectedType === 'Array') {
|
||||
valid = isArray(value)
|
||||
} else {
|
||||
valid = value instanceof type
|
||||
}
|
||||
return {
|
||||
valid,
|
||||
expectedType
|
||||
}
|
||||
}
|
||||
|
||||
function getInvalidTypeMessage(
|
||||
name: string,
|
||||
value: any,
|
||||
expectedTypes: string[]
|
||||
): string {
|
||||
let message =
|
||||
`Invalid prop: type check failed for prop "${name}".` +
|
||||
` Expected ${expectedTypes.map(capitalize).join(', ')}`
|
||||
const expectedType = expectedTypes[0]
|
||||
const receivedType = toRawType(value)
|
||||
const expectedValue = styleValue(value, expectedType)
|
||||
const receivedValue = styleValue(value, receivedType)
|
||||
// check if we need to specify expected value
|
||||
if (
|
||||
expectedTypes.length === 1 &&
|
||||
isExplicable(expectedType) &&
|
||||
!isBoolean(expectedType, receivedType)
|
||||
) {
|
||||
message += ` with value ${expectedValue}`
|
||||
}
|
||||
message += `, got ${receivedType} `
|
||||
// check if we need to specify received value
|
||||
if (isExplicable(receivedType)) {
|
||||
message += `with value ${receivedValue}.`
|
||||
}
|
||||
return message
|
||||
}
|
||||
|
||||
function styleValue(value: any, type: string): string {
|
||||
if (type === 'String') {
|
||||
return `"${value}"`
|
||||
} else if (type === 'Number') {
|
||||
return `${Number(value)}`
|
||||
} else {
|
||||
return `${value}`
|
||||
}
|
||||
}
|
||||
|
||||
function toRawType(value: any): string {
|
||||
return Object.prototype.toString.call(value).slice(8, -1)
|
||||
}
|
||||
|
||||
function isExplicable(type: string): boolean {
|
||||
const explicitTypes = ['string', 'number', 'boolean']
|
||||
return explicitTypes.some(elem => type.toLowerCase() === elem)
|
||||
}
|
||||
|
||||
function isBoolean(...args: string[]): boolean {
|
||||
return args.some(elem => elem.toLowerCase() === 'boolean')
|
||||
}
|
@ -1,96 +0,0 @@
|
||||
import { ComponentInstance } from './component'
|
||||
import { isFunction } from '@vue/shared'
|
||||
import { isRendering } from './componentRenderUtils'
|
||||
import { isReservedKey, reservedMethods } from './componentOptions'
|
||||
import { warn } from './warning'
|
||||
|
||||
const bindCache = new WeakMap()
|
||||
|
||||
// TODO: bound methods should also capture/handle errors
|
||||
function getBoundMethod(fn: Function, target: any, receiver: any): Function {
|
||||
let boundMethodsForTarget = bindCache.get(target)
|
||||
if (boundMethodsForTarget === void 0) {
|
||||
bindCache.set(target, (boundMethodsForTarget = new Map()))
|
||||
}
|
||||
let boundFn = boundMethodsForTarget.get(fn)
|
||||
if (boundFn === void 0) {
|
||||
boundMethodsForTarget.set(fn, (boundFn = fn.bind(receiver)))
|
||||
}
|
||||
return boundFn
|
||||
}
|
||||
|
||||
const renderProxyHandlers = {
|
||||
get(target: ComponentInstance<any, any>, key: string, receiver: any) {
|
||||
let i: any
|
||||
if (key === '_self') {
|
||||
return target
|
||||
} else if ((i = target._rawData) !== null && i.hasOwnProperty(key)) {
|
||||
// data
|
||||
// make sure to return from $data to register dependency
|
||||
return target.$data[key]
|
||||
} else if ((i = target.$options.props) != null && i.hasOwnProperty(key)) {
|
||||
// props are only proxied if declared
|
||||
return target.$props[key]
|
||||
} else if (
|
||||
(i = target._computedGetters) !== null &&
|
||||
i.hasOwnProperty(key)
|
||||
) {
|
||||
// computed
|
||||
return i[key]()
|
||||
} else if (key[0] !== '_') {
|
||||
if (__DEV__ && isRendering) {
|
||||
if (key in reservedMethods) {
|
||||
warn(
|
||||
`"${key}" is a reserved method / lifecycle hook and should not be ` +
|
||||
`used as a normal method during render.`
|
||||
)
|
||||
} else if (!(key in target)) {
|
||||
warn(
|
||||
`property "${key}" was accessed during render but does not exist ` +
|
||||
`on instance.`
|
||||
)
|
||||
}
|
||||
}
|
||||
const value = Reflect.get(target, key, receiver)
|
||||
if (key !== 'constructor' && isFunction(value)) {
|
||||
// auto bind
|
||||
return getBoundMethod(value, target, receiver)
|
||||
} else {
|
||||
return value
|
||||
}
|
||||
}
|
||||
},
|
||||
set(
|
||||
target: ComponentInstance<any, any>,
|
||||
key: string,
|
||||
value: any,
|
||||
receiver: any
|
||||
): boolean {
|
||||
let i: any
|
||||
if (__DEV__) {
|
||||
if (isReservedKey(key) && key in target) {
|
||||
warn(`failed setting property "${key}": reserved fields are immutable.`)
|
||||
return false
|
||||
}
|
||||
if ((i = target.$options.props) != null && i.hasOwnProperty(key)) {
|
||||
warn(`failed setting property "${key}": props are immutable.`)
|
||||
return false
|
||||
}
|
||||
}
|
||||
if ((i = target._rawData) !== null && i.hasOwnProperty(key)) {
|
||||
target.$data[key] = value
|
||||
return true
|
||||
} else {
|
||||
return Reflect.set(target, key, value, receiver)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export type ComponentProxy<T = ComponentInstance> = T & { _self: T }
|
||||
|
||||
export function createRenderProxy<T extends ComponentInstance>(
|
||||
instance: T
|
||||
): ComponentProxy<T> {
|
||||
debugger
|
||||
return new Proxy(instance, renderProxyHandlers) as any
|
||||
}
|
@ -1,154 +0,0 @@
|
||||
import { VNode, createFragment, createTextVNode, cloneVNode } from './vdom'
|
||||
import { ComponentInstance, FunctionalComponent } from './component'
|
||||
import { resolveProps } from './componentProps'
|
||||
import { handleError, ErrorTypes } from './errorHandling'
|
||||
import { VNodeFlags, ChildrenFlags } from './flags'
|
||||
import { EMPTY_OBJ, isArray, isObject } from '@vue/shared'
|
||||
|
||||
export let isRendering = false
|
||||
|
||||
export function renderInstanceRoot(instance: ComponentInstance): VNode {
|
||||
let vnode
|
||||
const { render, $proxy, $props, $slots, $attrs, $parentVNode } = instance
|
||||
if (__DEV__) {
|
||||
isRendering = true
|
||||
}
|
||||
try {
|
||||
vnode = render.call($proxy, $props, $slots, $attrs, $parentVNode)
|
||||
} catch (err) {
|
||||
if (__DEV__) {
|
||||
isRendering = false
|
||||
}
|
||||
handleError(err, instance, ErrorTypes.RENDER)
|
||||
}
|
||||
if (__DEV__) {
|
||||
isRendering = false
|
||||
}
|
||||
return normalizeComponentRoot(vnode, $parentVNode)
|
||||
}
|
||||
|
||||
export function renderFunctionalRoot(vnode: VNode): VNode {
|
||||
const render = vnode.tag as FunctionalComponent
|
||||
const { 0: props, 1: attrs } = resolveProps(vnode.data, render.props)
|
||||
let subTree
|
||||
try {
|
||||
subTree = render(props, vnode.slots || EMPTY_OBJ, attrs, vnode)
|
||||
} catch (err) {
|
||||
handleError(err, vnode, ErrorTypes.RENDER)
|
||||
}
|
||||
return normalizeComponentRoot(subTree, vnode)
|
||||
}
|
||||
|
||||
function normalizeComponentRoot(
|
||||
vnode: any,
|
||||
componentVNode: VNode | null
|
||||
): VNode {
|
||||
if (vnode == null) {
|
||||
vnode = createTextVNode('')
|
||||
} else if (!isObject(vnode)) {
|
||||
vnode = createTextVNode(vnode + '')
|
||||
} else if (isArray(vnode)) {
|
||||
if (vnode.length === 1) {
|
||||
vnode = normalizeComponentRoot(vnode[0], componentVNode)
|
||||
} else {
|
||||
vnode = createFragment(vnode)
|
||||
}
|
||||
} else {
|
||||
const { el, flags } = vnode
|
||||
if (
|
||||
componentVNode &&
|
||||
(flags & VNodeFlags.COMPONENT || flags & VNodeFlags.ELEMENT)
|
||||
) {
|
||||
if (el) {
|
||||
vnode = cloneVNode(vnode as VNode)
|
||||
}
|
||||
if (flags & VNodeFlags.COMPONENT) {
|
||||
vnode.parentVNode = componentVNode
|
||||
}
|
||||
} else if (el) {
|
||||
vnode = cloneVNode(vnode as VNode)
|
||||
}
|
||||
}
|
||||
return vnode
|
||||
}
|
||||
|
||||
export function shouldUpdateComponent(
|
||||
prevVNode: VNode,
|
||||
nextVNode: VNode
|
||||
): boolean {
|
||||
const { data: prevProps, childFlags: prevChildFlags } = prevVNode
|
||||
const { data: nextProps, childFlags: nextChildFlags } = nextVNode
|
||||
// If has different slots content, or has non-compiled slots,
|
||||
// the child needs to be force updated.
|
||||
if (
|
||||
prevChildFlags !== nextChildFlags ||
|
||||
(nextChildFlags & ChildrenFlags.DYNAMIC_SLOTS) > 0
|
||||
) {
|
||||
return true
|
||||
}
|
||||
if (prevProps === nextProps) {
|
||||
return false
|
||||
}
|
||||
if (prevProps === null) {
|
||||
return nextProps !== null
|
||||
}
|
||||
if (nextProps === null) {
|
||||
return prevProps !== null
|
||||
}
|
||||
const nextKeys = Object.keys(nextProps)
|
||||
if (nextKeys.length !== Object.keys(prevProps).length) {
|
||||
return true
|
||||
}
|
||||
for (let i = 0; i < nextKeys.length; i++) {
|
||||
const key = nextKeys[i]
|
||||
if (nextProps[key] !== prevProps[key]) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// DEV only
|
||||
export function getReasonForComponentUpdate(
|
||||
prevVNode: VNode,
|
||||
nextVNode: VNode
|
||||
): any {
|
||||
const reasons = []
|
||||
const { childFlags: prevChildFlags } = prevVNode
|
||||
const { childFlags: nextChildFlags } = nextVNode
|
||||
if (
|
||||
prevChildFlags !== nextChildFlags ||
|
||||
(nextChildFlags & ChildrenFlags.DYNAMIC_SLOTS) > 0
|
||||
) {
|
||||
reasons.push({
|
||||
type: `slots may have changed`,
|
||||
tip: `use function slots + $stable: true to avoid slot-triggered child updates.`
|
||||
})
|
||||
}
|
||||
const prevProps = prevVNode.data || EMPTY_OBJ
|
||||
const nextProps = nextVNode.data || EMPTY_OBJ
|
||||
for (const key in nextProps) {
|
||||
if (nextProps[key] !== prevProps[key]) {
|
||||
reasons.push({
|
||||
type: 'prop changed',
|
||||
key,
|
||||
value: nextProps[key],
|
||||
oldValue: prevProps[key]
|
||||
})
|
||||
}
|
||||
}
|
||||
for (const key in prevProps) {
|
||||
if (!(key in nextProps)) {
|
||||
reasons.push({
|
||||
type: 'prop changed',
|
||||
key,
|
||||
value: undefined,
|
||||
oldValue: prevProps[key]
|
||||
})
|
||||
}
|
||||
}
|
||||
return {
|
||||
type: 'triggered by parent',
|
||||
reasons
|
||||
}
|
||||
}
|
@ -1,31 +0,0 @@
|
||||
import { ComponentInstance } from './component'
|
||||
import { observable } from '@vue/observer'
|
||||
import { isReservedKey } from './componentOptions'
|
||||
|
||||
export function initializeState(
|
||||
instance: ComponentInstance,
|
||||
shouldExtractInitializers: boolean
|
||||
) {
|
||||
const { data } = instance.$options
|
||||
const rawData = (instance._rawData = (data ? data.call(instance) : {}) as any)
|
||||
if (shouldExtractInitializers) {
|
||||
extractInitializers(instance, rawData)
|
||||
}
|
||||
instance.$data = observable(rawData || {})
|
||||
}
|
||||
|
||||
// extract properties initialized in a component's constructor
|
||||
export function extractInitializers(
|
||||
instance: ComponentInstance,
|
||||
data: any = {}
|
||||
): any {
|
||||
const keys = Object.keys(instance)
|
||||
const props = instance.$props
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const key = keys[i]
|
||||
if (!isReservedKey(key) && !props.hasOwnProperty(key)) {
|
||||
data[key] = (instance as any)[key]
|
||||
}
|
||||
}
|
||||
return data
|
||||
}
|
@ -1,143 +0,0 @@
|
||||
import {
|
||||
EMPTY_OBJ,
|
||||
NOOP,
|
||||
isFunction,
|
||||
isArray,
|
||||
isString,
|
||||
isObject
|
||||
} from '@vue/shared'
|
||||
import { ComponentInstance } from './component'
|
||||
import { ComponentWatchOptions, WatchOptions } from './componentOptions'
|
||||
import { effect, stop } from '@vue/observer'
|
||||
import { queueJob } from '@vue/scheduler'
|
||||
import { handleError, ErrorTypes } from './errorHandling'
|
||||
import { warn } from './warning'
|
||||
|
||||
export function initializeWatch(
|
||||
instance: ComponentInstance,
|
||||
options: ComponentWatchOptions | undefined
|
||||
) {
|
||||
if (options !== void 0) {
|
||||
for (const key in options) {
|
||||
const opt = options[key]
|
||||
if (isArray(opt)) {
|
||||
opt.forEach(o => setupWatcher(instance, key, o))
|
||||
} else if (isFunction(opt)) {
|
||||
setupWatcher(instance, key, opt)
|
||||
} else if (isString(opt)) {
|
||||
setupWatcher(instance, key, (instance as any)[opt])
|
||||
} else if (opt.handler) {
|
||||
setupWatcher(instance, key, opt.handler, opt)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function setupWatcher(
|
||||
instance: ComponentInstance,
|
||||
keyOrFn: string | Function,
|
||||
cb: (newValue: any, oldValue: any) => void,
|
||||
options: WatchOptions = EMPTY_OBJ as WatchOptions
|
||||
): () => void {
|
||||
const handles = instance._watchHandles || (instance._watchHandles = new Set())
|
||||
const proxy = instance.$proxy
|
||||
|
||||
const rawGetter = isString(keyOrFn)
|
||||
? parseDotPath(keyOrFn, proxy)
|
||||
: () => keyOrFn.call(proxy)
|
||||
|
||||
if (__DEV__ && rawGetter === NOOP) {
|
||||
warn(
|
||||
`Failed watching expression: "${keyOrFn}". ` +
|
||||
`Watch expressions can only be dot-delimited paths. ` +
|
||||
`For more complex expressions, use $watch with a function instead.`
|
||||
)
|
||||
}
|
||||
|
||||
const getter = options.deep ? () => traverse(rawGetter()) : rawGetter
|
||||
|
||||
let oldValue: any
|
||||
|
||||
const applyCb = () => {
|
||||
const newValue = runner()
|
||||
if (options.deep || newValue !== oldValue) {
|
||||
try {
|
||||
cb.call(instance.$proxy, newValue, oldValue)
|
||||
} catch (e) {
|
||||
handleError(e, instance, ErrorTypes.WATCH_CALLBACK)
|
||||
}
|
||||
oldValue = newValue
|
||||
}
|
||||
}
|
||||
|
||||
const runner = effect(getter, {
|
||||
lazy: true,
|
||||
scheduler: options.sync
|
||||
? applyCb
|
||||
: () => {
|
||||
// defer watch callback using the scheduler so that multiple mutations
|
||||
// result in one call only.
|
||||
queueJob(applyCb)
|
||||
}
|
||||
})
|
||||
|
||||
oldValue = runner()
|
||||
handles.add(runner)
|
||||
|
||||
if (options.immediate) {
|
||||
cb.call(instance.$proxy, oldValue, undefined)
|
||||
}
|
||||
|
||||
return () => {
|
||||
stop(runner)
|
||||
handles.delete(runner)
|
||||
}
|
||||
}
|
||||
|
||||
export function teardownWatch(instance: ComponentInstance) {
|
||||
if (instance._watchHandles !== null) {
|
||||
instance._watchHandles.forEach(stop)
|
||||
}
|
||||
}
|
||||
|
||||
const bailRE = /[^\w.$]/
|
||||
|
||||
function parseDotPath(path: string, ctx: any): Function {
|
||||
if (bailRE.test(path)) {
|
||||
return NOOP
|
||||
}
|
||||
const segments = path.split('.')
|
||||
if (segments.length === 1) {
|
||||
return () => ctx[path]
|
||||
} else {
|
||||
return () => {
|
||||
let obj = ctx
|
||||
for (let i = 0; i < segments.length; i++) {
|
||||
if (!obj) return
|
||||
obj = obj[segments[i]]
|
||||
}
|
||||
return obj
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function traverse(value: any, seen: Set<any> = new Set()) {
|
||||
if (!isObject(value) || seen.has(value)) {
|
||||
return
|
||||
}
|
||||
seen.add(value)
|
||||
if (isArray(value)) {
|
||||
for (let i = 0; i < value.length; i++) {
|
||||
traverse(value[i], seen)
|
||||
}
|
||||
} else if (value instanceof Map || value instanceof Set) {
|
||||
;(value as any).forEach((v: any) => {
|
||||
traverse(v, seen)
|
||||
})
|
||||
} else {
|
||||
for (const key in value) {
|
||||
traverse(value[key], seen)
|
||||
}
|
||||
}
|
||||
return value
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -1,134 +0,0 @@
|
||||
import { ComponentInstance } from './component'
|
||||
import { warn, pushWarningContext, popWarningContext } from './warning'
|
||||
import { VNode } from './vdom'
|
||||
import { VNodeFlags } from './flags'
|
||||
import { ComponentProxy } from './componentProxy'
|
||||
|
||||
export const enum ErrorTypes {
|
||||
BEFORE_CREATE = 1,
|
||||
CREATED,
|
||||
BEFORE_MOUNT,
|
||||
MOUNTED,
|
||||
BEFORE_UPDATE,
|
||||
UPDATED,
|
||||
BEFORE_UNMOUNT,
|
||||
UNMOUNTED,
|
||||
ACTIVATED,
|
||||
DEACTIVATED,
|
||||
ERROR_CAPTURED,
|
||||
RENDER,
|
||||
RENDER_TRACKED,
|
||||
RENDER_TRIGGERED,
|
||||
WATCH_CALLBACK,
|
||||
NATIVE_EVENT_HANDLER,
|
||||
COMPONENT_EVENT_HANDLER,
|
||||
SCHEDULER
|
||||
}
|
||||
|
||||
const ErrorTypeStrings: Record<number, string> = {
|
||||
[ErrorTypes.BEFORE_CREATE]: 'in beforeCreate lifecycle hook',
|
||||
[ErrorTypes.CREATED]: 'in created lifecycle hook',
|
||||
[ErrorTypes.BEFORE_MOUNT]: 'in beforeMount lifecycle hook',
|
||||
[ErrorTypes.MOUNTED]: 'in mounted lifecycle hook',
|
||||
[ErrorTypes.BEFORE_UPDATE]: 'in beforeUpdate lifecycle hook',
|
||||
[ErrorTypes.UPDATED]: 'in updated lifecycle hook',
|
||||
[ErrorTypes.BEFORE_UNMOUNT]: 'in beforeUnmount lifecycle hook',
|
||||
[ErrorTypes.UNMOUNTED]: 'in unmounted lifecycle hook',
|
||||
[ErrorTypes.ACTIVATED]: 'in activated lifecycle hook',
|
||||
[ErrorTypes.DEACTIVATED]: 'in deactivated lifecycle hook',
|
||||
[ErrorTypes.ERROR_CAPTURED]: 'in errorCaptured lifecycle hook',
|
||||
[ErrorTypes.RENDER]: 'in render function',
|
||||
[ErrorTypes.RENDER_TRACKED]: 'in renderTracked debug hook',
|
||||
[ErrorTypes.RENDER_TRIGGERED]: 'in renderTriggered debug hook',
|
||||
[ErrorTypes.WATCH_CALLBACK]: 'in watcher callback',
|
||||
[ErrorTypes.NATIVE_EVENT_HANDLER]: 'in native event handler',
|
||||
[ErrorTypes.COMPONENT_EVENT_HANDLER]: 'in component event handler',
|
||||
[ErrorTypes.SCHEDULER]:
|
||||
'when flushing updates. This may be a Vue internals bug.'
|
||||
}
|
||||
|
||||
export function callLifecycleHookWithHandler(
|
||||
hook: Function,
|
||||
instanceProxy: ComponentProxy,
|
||||
type: ErrorTypes,
|
||||
arg?: any
|
||||
) {
|
||||
try {
|
||||
const res = hook.call(instanceProxy, arg)
|
||||
if (res && !res._isVue && typeof res.then === 'function') {
|
||||
;(res as Promise<any>).catch(err => {
|
||||
handleError(err, instanceProxy._self, type)
|
||||
})
|
||||
}
|
||||
} catch (err) {
|
||||
handleError(err, instanceProxy._self, type)
|
||||
}
|
||||
}
|
||||
|
||||
export function handleError(
|
||||
err: Error,
|
||||
instance: ComponentInstance | VNode | null,
|
||||
type: ErrorTypes
|
||||
) {
|
||||
const isFunctional = instance && (instance as VNode)._isVNode
|
||||
const contextVNode =
|
||||
instance &&
|
||||
((isFunctional
|
||||
? instance
|
||||
: (instance as ComponentInstance).$parentVNode) as VNode | null)
|
||||
let cur: ComponentInstance | null = null
|
||||
if (isFunctional) {
|
||||
let vnode = instance as VNode | null
|
||||
while (vnode && !(vnode.flags & VNodeFlags.COMPONENT_STATEFUL)) {
|
||||
vnode = vnode.contextVNode
|
||||
}
|
||||
if (vnode) {
|
||||
cur = vnode.children as ComponentInstance
|
||||
}
|
||||
} else if (instance) {
|
||||
const parent = (instance as ComponentInstance).$parent
|
||||
cur = parent && parent._self
|
||||
}
|
||||
while (cur) {
|
||||
const handler = cur.errorCaptured
|
||||
if (handler) {
|
||||
try {
|
||||
const captured = handler.call(
|
||||
cur,
|
||||
err,
|
||||
type,
|
||||
isFunctional ? null : instance
|
||||
)
|
||||
if (captured) return
|
||||
} catch (err2) {
|
||||
logError(err2, ErrorTypes.ERROR_CAPTURED, contextVNode)
|
||||
}
|
||||
}
|
||||
cur = cur.$parent && cur.$parent._self
|
||||
}
|
||||
logError(err, type, contextVNode)
|
||||
}
|
||||
|
||||
function logError(err: Error, type: ErrorTypes, contextVNode: VNode | null) {
|
||||
if (__DEV__) {
|
||||
const info = ErrorTypeStrings[type]
|
||||
if (contextVNode) {
|
||||
pushWarningContext(contextVNode)
|
||||
}
|
||||
if (/private field/.test(err.message)) {
|
||||
warn(
|
||||
`Private fields cannot be accessed directly on \`this\` in a component ` +
|
||||
`class because they cannot be tunneled through Proxies. ` +
|
||||
`Use \`this._self.#field\` instead.`
|
||||
)
|
||||
} else {
|
||||
warn(`Unhandled error${info ? ` ${info}` : ``}`)
|
||||
}
|
||||
console.error(err)
|
||||
if (contextVNode) {
|
||||
popWarningContext()
|
||||
}
|
||||
} else {
|
||||
throw err
|
||||
}
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
// vnode flags
|
||||
export const enum VNodeFlags {
|
||||
ELEMENT_HTML = 1,
|
||||
ELEMENT_SVG = 1 << 1,
|
||||
|
||||
COMPONENT_STATEFUL_NORMAL = 1 << 2,
|
||||
COMPONENT_STATEFUL_SHOULD_KEEP_ALIVE = 1 << 3,
|
||||
COMPONENT_STATEFUL_KEPT_ALIVE = 1 << 4,
|
||||
COMPONENT_FUNCTIONAL = 1 << 5,
|
||||
|
||||
TEXT = 1 << 6,
|
||||
FRAGMENT = 1 << 7,
|
||||
PORTAL = 1 << 8,
|
||||
|
||||
// masks (only use for bitwise checks, do not use equal checks or assign)
|
||||
ELEMENT = ELEMENT_HTML | ELEMENT_SVG,
|
||||
COMPONENT_STATEFUL = COMPONENT_STATEFUL_NORMAL |
|
||||
COMPONENT_STATEFUL_SHOULD_KEEP_ALIVE |
|
||||
COMPONENT_STATEFUL_KEPT_ALIVE,
|
||||
COMPONENT = COMPONENT_STATEFUL | COMPONENT_FUNCTIONAL
|
||||
}
|
||||
|
||||
export const enum ChildrenFlags {
|
||||
UNKNOWN_CHILDREN = 0,
|
||||
NO_CHILDREN = 1,
|
||||
SINGLE_VNODE = 1 << 1,
|
||||
KEYED_VNODES = 1 << 2,
|
||||
NONE_KEYED_VNODES = 1 << 3,
|
||||
STABLE_SLOTS = 1 << 4,
|
||||
DYNAMIC_SLOTS = 1 << 5,
|
||||
|
||||
// masks
|
||||
HAS_SLOTS = STABLE_SLOTS | DYNAMIC_SLOTS,
|
||||
MULTIPLE_VNODES = KEYED_VNODES | NONE_KEYED_VNODES
|
||||
}
|
@ -1,168 +1,84 @@
|
||||
import { ChildrenFlags } from './flags'
|
||||
import { ComponentClass, FunctionalComponent, Component } from './component'
|
||||
import { ComponentOptions } from './componentOptions'
|
||||
import {
|
||||
VNode,
|
||||
createElementVNode,
|
||||
createComponentVNode,
|
||||
createFragment,
|
||||
createPortal,
|
||||
VNodeData,
|
||||
BuiltInProps,
|
||||
Key
|
||||
} from './vdom'
|
||||
import { isObservable } from '@vue/observer'
|
||||
import { warn } from './warning'
|
||||
import { isString, isArray, isFunction, isObject } from '@vue/shared'
|
||||
export const Fragment = Symbol('Fragment')
|
||||
export const Text = Symbol('Text')
|
||||
export const Empty = Symbol('Empty')
|
||||
|
||||
export const Fragment = Symbol()
|
||||
export const Portal = Symbol()
|
||||
|
||||
type RawChildType = VNode | string | number | boolean | null | undefined
|
||||
|
||||
export type RawSlots = {
|
||||
$stable?: boolean
|
||||
[name: string]: RawChildType | (() => RawChildrenType)
|
||||
}
|
||||
|
||||
export type RawChildrenType = RawChildType | RawChildType[]
|
||||
|
||||
export type ElementType =
|
||||
type VNodeTypes =
|
||||
| string
|
||||
| FunctionalComponent
|
||||
| ComponentClass
|
||||
| ComponentOptions
|
||||
| Function
|
||||
| typeof Fragment
|
||||
| typeof Portal
|
||||
| typeof Text
|
||||
| typeof Empty
|
||||
|
||||
// This is used to differentiate the data object from
|
||||
// vnodes and arrays
|
||||
type Differ = { _isVNode?: never; [Symbol.iterator]?: never }
|
||||
export type VNodeChild = VNode | string | number | null
|
||||
export interface VNodeChildren extends Array<VNodeChildren | VNodeChild> {}
|
||||
|
||||
type OptionsComponent<P> =
|
||||
| (ComponentOptions<P> & { template: string })
|
||||
| (ComponentOptions<P> & { render: Function })
|
||||
|
||||
// TODO improve return type with props information
|
||||
interface createElement {
|
||||
// element
|
||||
(tag: string, children?: RawChildrenType): VNode
|
||||
(
|
||||
tag: string,
|
||||
// TODO support native element properties
|
||||
data?: VNodeData & Differ | null,
|
||||
children?: RawChildrenType | RawSlots
|
||||
): VNode
|
||||
// fragment
|
||||
(tag: typeof Fragment, children?: RawChildrenType): VNode
|
||||
(
|
||||
tag: typeof Fragment,
|
||||
data?: ({ key?: Key } & Differ) | null,
|
||||
children?: RawChildrenType | RawSlots
|
||||
): VNode
|
||||
// portal
|
||||
(tag: typeof Portal, children?: RawChildrenType): VNode
|
||||
(
|
||||
tag: typeof Portal,
|
||||
data?: ({ target: any } & BuiltInProps & Differ) | null,
|
||||
children?: RawChildrenType | RawSlots
|
||||
): VNode
|
||||
// object
|
||||
<P>(tag: OptionsComponent<P>, children?: RawChildrenType): VNode
|
||||
<P>(
|
||||
tag: OptionsComponent<P>,
|
||||
data?: (P & BuiltInProps & Differ) | null,
|
||||
children?: RawChildrenType | RawSlots
|
||||
): VNode
|
||||
// functional
|
||||
<P>(tag: FunctionalComponent<P>, children?: RawChildrenType): VNode
|
||||
<P>(
|
||||
tag: FunctionalComponent<P>,
|
||||
data?: (P & BuiltInProps & Differ) | null,
|
||||
children?: RawChildrenType | RawSlots
|
||||
): VNode
|
||||
// class
|
||||
<P>(tag: new () => Component<P>, children?: RawChildrenType): VNode
|
||||
<P>(
|
||||
tag: new () => Component<P>,
|
||||
data?: (P & BuiltInProps & Differ) | null,
|
||||
children?: RawChildrenType | RawSlots
|
||||
): VNode
|
||||
export interface VNode {
|
||||
type: VNodeTypes
|
||||
props: { [key: string]: any } | null
|
||||
key: string | number | null
|
||||
children: string | VNodeChildren | null
|
||||
patchFlag: number | null
|
||||
dynamicProps: string[] | null
|
||||
dynamicChildren: VNode[] | null
|
||||
}
|
||||
|
||||
export const h = ((tag: ElementType, data?: any, children?: any): VNode => {
|
||||
if (data !== null && (isArray(data) || !isObject(data) || data._isVNode)) {
|
||||
children = data
|
||||
data = null
|
||||
}
|
||||
const blockStack: (VNode[])[] = []
|
||||
|
||||
if (data === void 0) data = null
|
||||
if (children === void 0) children = null
|
||||
// open block
|
||||
export function openBlock() {
|
||||
blockStack.push([])
|
||||
}
|
||||
|
||||
// if value is observable, create a clone of original
|
||||
// so that we can normalize its class/style
|
||||
// since this guard is only placed here, this means any direct createXXXVnode
|
||||
// functions only accept fresh data objects.
|
||||
if (isObservable(data)) {
|
||||
data = Object.assign({}, data)
|
||||
}
|
||||
let shouldTrack = true
|
||||
|
||||
let key = null
|
||||
let ref = null
|
||||
let portalTarget = null
|
||||
if (data != null) {
|
||||
if (data.slots != null) {
|
||||
children = data.slots
|
||||
}
|
||||
if (data.key != null) {
|
||||
;({ key } = data)
|
||||
}
|
||||
if (data.ref != null) {
|
||||
;({ ref } = data)
|
||||
}
|
||||
if (data.target != null) {
|
||||
portalTarget = data.target
|
||||
}
|
||||
}
|
||||
// block
|
||||
export function createBlock(
|
||||
type: VNodeTypes,
|
||||
props?: { [key: string]: any } | null,
|
||||
children?: any,
|
||||
patchFlag?: number,
|
||||
dynamicProps?: string[]
|
||||
): VNode {
|
||||
// avoid a block with optFlag tracking itself
|
||||
shouldTrack = false
|
||||
const vnode = createVNode(type, props, children, patchFlag, dynamicProps)
|
||||
shouldTrack = true
|
||||
vnode.dynamicChildren = blockStack.pop() || null
|
||||
// a block is always going to be patched
|
||||
trackDynamicNode(vnode)
|
||||
return vnode
|
||||
}
|
||||
|
||||
if (isString(tag)) {
|
||||
// element
|
||||
return createElementVNode(
|
||||
tag,
|
||||
data,
|
||||
children,
|
||||
ChildrenFlags.UNKNOWN_CHILDREN,
|
||||
key,
|
||||
ref
|
||||
)
|
||||
} else if (tag === Fragment) {
|
||||
if (__DEV__ && ref) {
|
||||
warn('Ref cannot be used on Fragments. Use it on inner elements instead.')
|
||||
}
|
||||
return createFragment(children, ChildrenFlags.UNKNOWN_CHILDREN, key)
|
||||
} else if (tag === Portal) {
|
||||
if (__DEV__ && !portalTarget) {
|
||||
warn('Portal must have a target: ', portalTarget)
|
||||
}
|
||||
return createPortal(
|
||||
portalTarget,
|
||||
children,
|
||||
ChildrenFlags.UNKNOWN_CHILDREN,
|
||||
key,
|
||||
ref
|
||||
)
|
||||
} else {
|
||||
if (__DEV__ && !isFunction(tag) && !isObject(tag)) {
|
||||
warn('Invalid component passed to h(): ', tag)
|
||||
}
|
||||
// component
|
||||
return createComponentVNode(
|
||||
tag,
|
||||
data,
|
||||
children,
|
||||
ChildrenFlags.UNKNOWN_CHILDREN,
|
||||
key,
|
||||
ref
|
||||
)
|
||||
// element
|
||||
export function createVNode(
|
||||
type: VNodeTypes,
|
||||
props: { [key: string]: any } | null = null,
|
||||
children: any = null,
|
||||
patchFlag: number | null = null,
|
||||
dynamicProps: string[] | null = null
|
||||
): VNode {
|
||||
const vnode: VNode = {
|
||||
type,
|
||||
props,
|
||||
key: props && props.key,
|
||||
children,
|
||||
patchFlag,
|
||||
dynamicProps,
|
||||
dynamicChildren: null
|
||||
}
|
||||
}) as createElement
|
||||
if (patchFlag != null && shouldTrack) {
|
||||
trackDynamicNode(vnode)
|
||||
}
|
||||
return vnode
|
||||
}
|
||||
|
||||
function trackDynamicNode(vnode: VNode) {
|
||||
const currentBlockDynamicNodes = blockStack[blockStack.length - 1]
|
||||
if (currentBlockDynamicNodes) {
|
||||
currentBlockDynamicNodes.push(vnode)
|
||||
}
|
||||
}
|
||||
|
||||
export function cloneVNode(vnode: VNode): VNode {
|
||||
// TODO
|
||||
}
|
||||
|
@ -1,41 +1,5 @@
|
||||
// Core API
|
||||
export { h, Fragment, Portal } from './h'
|
||||
export { Component } from './component'
|
||||
export {
|
||||
cloneVNode,
|
||||
createElementVNode,
|
||||
createComponentVNode,
|
||||
createTextVNode,
|
||||
createFragment,
|
||||
createPortal
|
||||
} from './vdom'
|
||||
export {
|
||||
createRenderer,
|
||||
NodeOps,
|
||||
PatchDataFunction,
|
||||
RendererOptions
|
||||
} from './createRenderer'
|
||||
export { TEXT, CLASS, STYLE, PROPS, KEYED, UNKEYED } from './patchFlags'
|
||||
|
||||
// Observer API
|
||||
export * from '@vue/observer'
|
||||
export { openBlock, createBlock, createVNode, Fragment, Text, Empty } from './h'
|
||||
|
||||
// Scheduler API
|
||||
export { nextTick } from '@vue/scheduler'
|
||||
|
||||
// Optional APIs
|
||||
// these are imported on-demand and can be tree-shaken
|
||||
export { createAsyncComponent } from './optional/asyncComponent'
|
||||
export { KeepAlive } from './optional/keepAlive'
|
||||
export { applyDirectives } from './optional/directives'
|
||||
export { mixins } from './optional/mixins'
|
||||
export { EventEmitter } from './optional/eventEmitter'
|
||||
export { memoize } from './optional/memoize'
|
||||
|
||||
// flags & types
|
||||
export { ComponentType, ComponentClass, FunctionalComponent } from './component'
|
||||
export { VNodeFlags, ChildrenFlags } from './flags'
|
||||
export { VNode, Slots } from './vdom'
|
||||
|
||||
// Internal API, for libraries or renderers that need to perform low level work
|
||||
export * from './componentOptions'
|
||||
export { createComponentInstance } from './componentInstance'
|
||||
export { createRenderer } from './createRenderer'
|
||||
|
@ -1,100 +0,0 @@
|
||||
import { ChildrenFlags } from '../flags'
|
||||
import { createComponentVNode, Slots } from '../vdom'
|
||||
import { Component, ComponentType, ComponentClass } from '../component'
|
||||
import { unwrap } from '@vue/observer'
|
||||
import { isFunction } from '@vue/shared'
|
||||
|
||||
interface AsyncComponentFactory {
|
||||
(): Promise<ComponentType>
|
||||
resolved?: ComponentType
|
||||
}
|
||||
|
||||
interface AsyncComponentFullOptions {
|
||||
factory: AsyncComponentFactory
|
||||
loading?: ComponentType
|
||||
error?: ComponentType
|
||||
delay?: number
|
||||
timeout?: number
|
||||
}
|
||||
|
||||
type AsyncComponentOptions = AsyncComponentFactory | AsyncComponentFullOptions
|
||||
|
||||
export function createAsyncComponent(
|
||||
options: AsyncComponentOptions
|
||||
): ComponentClass {
|
||||
if (isFunction(options)) {
|
||||
options = { factory: options }
|
||||
}
|
||||
|
||||
const {
|
||||
factory,
|
||||
timeout,
|
||||
delay = 200,
|
||||
loading: loadingComp,
|
||||
error: errorComp
|
||||
} = options
|
||||
|
||||
return class AsyncContainer extends Component {
|
||||
comp: ComponentType | null = null
|
||||
err: Error | null = null
|
||||
delayed: boolean = false
|
||||
timedOut: boolean = false
|
||||
|
||||
// doing this in beforeMount so this is non-SSR only
|
||||
beforeMount() {
|
||||
if (factory.resolved) {
|
||||
this.comp = factory.resolved
|
||||
} else {
|
||||
factory()
|
||||
.then(resolved => {
|
||||
this.comp = factory.resolved = resolved
|
||||
})
|
||||
.catch(err => {
|
||||
this.err = err
|
||||
})
|
||||
}
|
||||
if (timeout != null) {
|
||||
setTimeout(() => {
|
||||
this.timedOut = true
|
||||
}, timeout)
|
||||
}
|
||||
if (delay != null) {
|
||||
this.delayed = true
|
||||
setTimeout(() => {
|
||||
this.delayed = false
|
||||
}, delay)
|
||||
}
|
||||
}
|
||||
|
||||
render(props: any, slots: Slots) {
|
||||
if (this.err || (this.timedOut && !this.comp)) {
|
||||
const error =
|
||||
this.err || new Error(`Async component timed out after ${timeout}ms.`)
|
||||
return errorComp
|
||||
? createComponentVNode(
|
||||
errorComp,
|
||||
{ error },
|
||||
null,
|
||||
ChildrenFlags.NO_CHILDREN
|
||||
)
|
||||
: null
|
||||
} else if (this.comp) {
|
||||
return createComponentVNode(
|
||||
this.comp,
|
||||
unwrap(props),
|
||||
slots,
|
||||
ChildrenFlags.STABLE_SLOTS
|
||||
)
|
||||
} else {
|
||||
return loadingComp && !this.delayed
|
||||
? createComponentVNode(
|
||||
loadingComp,
|
||||
null,
|
||||
null,
|
||||
ChildrenFlags.NO_CHILDREN
|
||||
)
|
||||
: null
|
||||
}
|
||||
}
|
||||
} as ComponentClass
|
||||
}
|
@ -1,108 +0,0 @@
|
||||
/**
|
||||
Runtime helper for applying directives to a vnode. Example usage:
|
||||
|
||||
const comp = resolveComponent(this, 'comp')
|
||||
const foo = resolveDirective(this, 'foo')
|
||||
const bar = resolveDirective(this, 'bar')
|
||||
|
||||
return applyDirectives(
|
||||
h(comp),
|
||||
this,
|
||||
[foo, this.x],
|
||||
[bar, this.y]
|
||||
)
|
||||
*/
|
||||
|
||||
import { VNode, cloneVNode, VNodeData } from '../vdom'
|
||||
import { ComponentInstance } from '../component'
|
||||
import { EMPTY_OBJ } from '@vue/shared'
|
||||
|
||||
interface DirectiveBinding {
|
||||
instance: ComponentInstance
|
||||
value?: any
|
||||
oldValue?: any
|
||||
arg?: string
|
||||
modifiers?: DirectiveModifiers
|
||||
}
|
||||
|
||||
type DirectiveHook = (
|
||||
el: any,
|
||||
binding: DirectiveBinding,
|
||||
vnode: VNode,
|
||||
prevVNode: VNode | void
|
||||
) => void
|
||||
|
||||
interface Directive {
|
||||
beforeMount: DirectiveHook
|
||||
mounted: DirectiveHook
|
||||
beforeUpdate: DirectiveHook
|
||||
updated: DirectiveHook
|
||||
beforeUnmount: DirectiveHook
|
||||
unmounted: DirectiveHook
|
||||
}
|
||||
|
||||
type DirectiveModifiers = Record<string, boolean>
|
||||
|
||||
const valueCache = new WeakMap<Directive, WeakMap<any, any>>()
|
||||
|
||||
export function applyDirective(
|
||||
data: VNodeData,
|
||||
instance: ComponentInstance,
|
||||
directive: Directive,
|
||||
value?: any,
|
||||
arg?: string,
|
||||
modifiers?: DirectiveModifiers
|
||||
) {
|
||||
let valueCacheForDir = valueCache.get(directive) as WeakMap<VNode, any>
|
||||
if (!valueCacheForDir) {
|
||||
valueCacheForDir = new WeakMap<VNode, any>()
|
||||
valueCache.set(directive, valueCacheForDir)
|
||||
}
|
||||
for (const key in directive) {
|
||||
const hook = directive[key as keyof Directive]
|
||||
const hookKey = `vnode` + key[0].toUpperCase() + key.slice(1)
|
||||
const vnodeHook = (vnode: VNode, prevVNode?: VNode) => {
|
||||
let oldValue
|
||||
if (prevVNode !== void 0) {
|
||||
oldValue = valueCacheForDir.get(prevVNode)
|
||||
valueCacheForDir.delete(prevVNode)
|
||||
}
|
||||
valueCacheForDir.set(vnode, value)
|
||||
hook(
|
||||
vnode.el,
|
||||
{
|
||||
instance,
|
||||
value,
|
||||
oldValue,
|
||||
arg,
|
||||
modifiers
|
||||
},
|
||||
vnode,
|
||||
prevVNode
|
||||
)
|
||||
}
|
||||
const existing = data[hookKey]
|
||||
data[hookKey] = existing
|
||||
? [].concat(existing as any, vnodeHook as any)
|
||||
: vnodeHook
|
||||
}
|
||||
}
|
||||
|
||||
type DirectiveArguments = [
|
||||
Directive,
|
||||
any,
|
||||
string | undefined,
|
||||
DirectiveModifiers | undefined
|
||||
][]
|
||||
|
||||
export function applyDirectives(
|
||||
vnode: VNode,
|
||||
instance: ComponentInstance,
|
||||
...directives: DirectiveArguments
|
||||
) {
|
||||
vnode = cloneVNode(vnode, EMPTY_OBJ)
|
||||
for (let i = 0; i < directives.length; i++) {
|
||||
applyDirective(vnode.data as VNodeData, instance, ...directives[i])
|
||||
}
|
||||
return vnode
|
||||
}
|
@ -1,76 +0,0 @@
|
||||
import { isArray } from '@vue/shared'
|
||||
|
||||
export class EventEmitter {
|
||||
ctx: any
|
||||
events: { [event: string]: Function[] | null } = {}
|
||||
|
||||
constructor(ctx: any) {
|
||||
this.ctx = ctx
|
||||
}
|
||||
|
||||
// eventEmitter interface
|
||||
on(event: string, fn: Function) {
|
||||
if (isArray(event)) {
|
||||
for (let i = 0; i < event.length; i++) {
|
||||
this.on(event[i], fn)
|
||||
}
|
||||
} else {
|
||||
const { events } = this
|
||||
;(events[event] || (events[event] = [])).push(fn)
|
||||
}
|
||||
}
|
||||
|
||||
once(event: string, fn: Function) {
|
||||
const onceFn = (...args: any[]) => {
|
||||
this.off(event, onceFn)
|
||||
fn.apply(this, args)
|
||||
}
|
||||
;(onceFn as any).fn = fn
|
||||
this.on(event, onceFn)
|
||||
}
|
||||
|
||||
off(event?: string, fn?: Function) {
|
||||
if (!event && !fn) {
|
||||
this.events = {}
|
||||
} else if (isArray(event)) {
|
||||
for (let i = 0; i < event.length; i++) {
|
||||
this.off(event[i], fn)
|
||||
}
|
||||
} else if (!fn) {
|
||||
this.events[event as string] = null
|
||||
} else {
|
||||
const fns = this.events[event as string]
|
||||
if (fns) {
|
||||
for (let i = 0; i < fns.length; i++) {
|
||||
const f = fns[i]
|
||||
if (fn === f || fn === (f as any).fn) {
|
||||
fns.splice(i, 1)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
emit(name: string, ...payload: any[]) {
|
||||
const handlers = this.events[name]
|
||||
if (handlers) {
|
||||
invokeListeners(handlers, payload, this.ctx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function invokeListeners(
|
||||
value: Function | Function[],
|
||||
payload: any[],
|
||||
ctx: any = null
|
||||
) {
|
||||
// TODO handle error
|
||||
if (isArray(value)) {
|
||||
for (let i = 0; i < value.length; i++) {
|
||||
value[i].call(ctx, ...payload)
|
||||
}
|
||||
} else {
|
||||
value.call(ctx, ...payload)
|
||||
}
|
||||
}
|
@ -1,134 +0,0 @@
|
||||
import { Component, ComponentClass, ComponentInstance } from '../component'
|
||||
import { VNode, Slots, cloneVNode } from '../vdom'
|
||||
import { VNodeFlags } from '../flags'
|
||||
import { warn } from '../warning'
|
||||
import { isString, isArray } from '@vue/shared'
|
||||
|
||||
type MatchPattern = string | RegExp | string[] | RegExp[]
|
||||
|
||||
interface KeepAliveProps {
|
||||
include?: MatchPattern
|
||||
exclude?: MatchPattern
|
||||
max?: number | string
|
||||
}
|
||||
|
||||
type CacheKey = string | number | ComponentClass
|
||||
type Cache = Map<CacheKey, VNode>
|
||||
|
||||
export const KeepAliveSymbol = Symbol()
|
||||
|
||||
export class KeepAlive extends Component<KeepAliveProps> {
|
||||
private cache: Cache
|
||||
private keys: Set<CacheKey>
|
||||
|
||||
created() {
|
||||
this.cache = new Map()
|
||||
this.keys = new Set()
|
||||
}
|
||||
|
||||
// to be set in createRenderer when instance is created
|
||||
$unmount: (instance: ComponentInstance) => void
|
||||
|
||||
beforeUnmount() {
|
||||
this.cache.forEach(vnode => {
|
||||
// change flag so it can be properly unmounted
|
||||
vnode.flags = VNodeFlags.COMPONENT_STATEFUL_NORMAL
|
||||
this.$unmount(vnode.children as ComponentInstance)
|
||||
})
|
||||
}
|
||||
|
||||
pruneCache(filter?: (name: string) => boolean) {
|
||||
this.cache.forEach((vnode, key) => {
|
||||
const name = getName(vnode.tag as ComponentClass)
|
||||
if (name && (!filter || !filter(name))) {
|
||||
this.pruneCacheEntry(key)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pruneCacheEntry(key: CacheKey) {
|
||||
const cached = this.cache.get(key) as VNode
|
||||
const current = this.$vnode
|
||||
if (!current || cached.tag !== current.tag) {
|
||||
this.$unmount(cached.children as ComponentInstance)
|
||||
}
|
||||
this.cache.delete(key)
|
||||
this.keys.delete(key)
|
||||
}
|
||||
|
||||
render(props: KeepAliveProps, slots: Slots) {
|
||||
if (!slots.default) {
|
||||
return
|
||||
}
|
||||
const children = slots.default()
|
||||
let vnode = children[0]
|
||||
if (children.length > 1) {
|
||||
if (__DEV__) {
|
||||
warn(`KeepAlive can only have a single child.`)
|
||||
}
|
||||
return children
|
||||
} else if ((vnode.flags & VNodeFlags.COMPONENT_STATEFUL) === 0) {
|
||||
return children
|
||||
}
|
||||
|
||||
const comp = vnode.tag as ComponentClass
|
||||
const name = getName(comp)
|
||||
const { include, exclude, max } = props
|
||||
|
||||
if (
|
||||
(include && (!name || !matches(include, name))) ||
|
||||
(exclude && name && matches(exclude, name))
|
||||
) {
|
||||
return vnode
|
||||
}
|
||||
|
||||
const { cache, keys } = this
|
||||
const key = vnode.key == null ? comp : vnode.key
|
||||
const cached = cache.get(key)
|
||||
|
||||
// clone vnode if it's reused because we are going to mutate its flags
|
||||
if (vnode.el) {
|
||||
vnode = cloneVNode(vnode)
|
||||
}
|
||||
cache.set(key, vnode)
|
||||
|
||||
if (cached) {
|
||||
vnode.children = cached.children
|
||||
// avoid vnode being mounted as fresh
|
||||
vnode.flags |= VNodeFlags.COMPONENT_STATEFUL_KEPT_ALIVE
|
||||
// make this key the freshest
|
||||
keys.delete(key)
|
||||
keys.add(key)
|
||||
} else {
|
||||
keys.add(key)
|
||||
// prune oldest entry
|
||||
if (max && keys.size > parseInt(max as string, 10)) {
|
||||
this.pruneCacheEntry(Array.from(this.keys)[0])
|
||||
}
|
||||
}
|
||||
// avoid vnode being unmounted
|
||||
vnode.flags |= VNodeFlags.COMPONENT_STATEFUL_SHOULD_KEEP_ALIVE
|
||||
return vnode
|
||||
}
|
||||
}
|
||||
|
||||
// mark constructor
|
||||
// we use a symbol instead of comparing to the constructor itself
|
||||
// so that the implementation can be tree-shaken
|
||||
;(KeepAlive as any)[KeepAliveSymbol] = true
|
||||
|
||||
function getName(comp: ComponentClass): string | void {
|
||||
return comp.displayName || comp.name
|
||||
}
|
||||
|
||||
function matches(pattern: MatchPattern, name: string): boolean {
|
||||
if (isArray(pattern)) {
|
||||
return (pattern as any).some((p: string | RegExp) => matches(p, name))
|
||||
} else if (isString(pattern)) {
|
||||
return pattern.split(',').indexOf(name) > -1
|
||||
} else if (pattern.test) {
|
||||
return pattern.test(name)
|
||||
}
|
||||
/* istanbul ignore next */
|
||||
return false
|
||||
}
|
@ -1,56 +0,0 @@
|
||||
// Used for memoizing trees inside render functions.
|
||||
//
|
||||
// Example (equivalent of v-once):
|
||||
//
|
||||
// render() {
|
||||
// return memoize(h('div', this.msg), this, 0)
|
||||
// }
|
||||
//
|
||||
// Memoize baesd on keys:
|
||||
//
|
||||
// render() {
|
||||
// return memoize(h('div', this.msg + this.count), this, 0, [this.msg])
|
||||
// }
|
||||
|
||||
// TODO how does this work in v-for?
|
||||
// probably need to take vnode key into consideration
|
||||
|
||||
import { Component } from '../component'
|
||||
import { warn } from '../warning'
|
||||
|
||||
const memoizeMap = new WeakMap()
|
||||
|
||||
export function memoize<T>(
|
||||
getter: () => T,
|
||||
instance: Component,
|
||||
id: number,
|
||||
keys?: any[]
|
||||
): T {
|
||||
if (__DEV__ && arguments.length > 3 && !Array.isArray(keys)) {
|
||||
warn(
|
||||
`keys passed to v-memo or memoize must be an array. Got ${String(keys)}`
|
||||
)
|
||||
}
|
||||
let storage = memoizeMap.get(instance)
|
||||
if (!storage) {
|
||||
storage = []
|
||||
memoizeMap.set(instance, storage)
|
||||
}
|
||||
const record = storage[id]
|
||||
if (!record) {
|
||||
const value = getter()
|
||||
storage[id] = [value, keys]
|
||||
return value
|
||||
} else {
|
||||
const [prevValue, prevKeys] = record
|
||||
record[1] = keys
|
||||
if (keys) {
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
if (keys[i] !== prevKeys[i]) {
|
||||
return (record[0] = getter())
|
||||
}
|
||||
}
|
||||
}
|
||||
return prevValue
|
||||
}
|
||||
}
|
@ -1,84 +0,0 @@
|
||||
import { Component } from '../component'
|
||||
import { createComponentClassFromOptions } from '../componentOptions'
|
||||
import {
|
||||
ComponentOptions,
|
||||
resolveComponentOptionsFromClass,
|
||||
mergeComponentOptions,
|
||||
mergeDataFn
|
||||
} from '../componentOptions'
|
||||
import { normalizePropsOptions } from '../componentProps'
|
||||
import { extractInitializers } from '../componentState'
|
||||
import { isFunction } from '@vue/shared'
|
||||
|
||||
interface ComponentConstructor<This = Component> {
|
||||
new (): This
|
||||
}
|
||||
|
||||
interface ComponentConstructorWithMixins<This> {
|
||||
new <P = {}, D = {}>(): This & { $data: D } & D & { $props: Readonly<P> } & P
|
||||
}
|
||||
|
||||
// mind = blown
|
||||
// https://stackoverflow.com/questions/50374908/transform-union-type-to-intersection-type
|
||||
type UnionToIntersection<U> = (U extends any
|
||||
? (k: U) => void
|
||||
: never) extends ((k: infer I) => void)
|
||||
? I
|
||||
: never
|
||||
|
||||
type ExtractInstance<T> = T extends (infer U)[]
|
||||
? UnionToIntersection<U extends ComponentConstructor<infer V> ? V : never>
|
||||
: never
|
||||
|
||||
export function mixins<
|
||||
T extends ComponentConstructor[] = [],
|
||||
V = ExtractInstance<T>
|
||||
>(...args: T): ComponentConstructorWithMixins<V>
|
||||
export function mixins(...args: any[]): any {
|
||||
let options: ComponentOptions = {}
|
||||
args.forEach(mixin => {
|
||||
if (isFunction(mixin)) {
|
||||
const Class = mixin
|
||||
mixin = resolveComponentOptionsFromClass(Class)
|
||||
// in order to extract properties initialized in the mixin's constructor,
|
||||
// we create an instance of it and pass in the actual props - this
|
||||
// short-circuits the normal component initialization and allows us to
|
||||
// relatively-cheaply extract the properties added in the constructor.
|
||||
function extractData() {
|
||||
return extractInitializers(new Class(this.$props))
|
||||
}
|
||||
const { data } = mixin
|
||||
mixin.data = data ? mergeDataFn(data, extractData) : extractData
|
||||
} else {
|
||||
mixin.props = normalizePropsOptions(mixin.props)
|
||||
}
|
||||
options = mergeComponentOptions(options, mixin)
|
||||
})
|
||||
return createComponentClassFromOptions(options)
|
||||
}
|
||||
|
||||
/* Example usage
|
||||
|
||||
class Foo extends Component<{ foo: number }> {
|
||||
test() {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
class Bar extends Component<{ bar: string }> {
|
||||
ok() {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
class Baz extends mixins(Foo, Bar)<{ baz: number }> {
|
||||
created() {
|
||||
this.foo
|
||||
this.bar
|
||||
this.baz
|
||||
this.test()
|
||||
this.ok()
|
||||
}
|
||||
}
|
||||
|
||||
*/
|
6
packages/runtime-core/src/patchFlags.ts
Normal file
6
packages/runtime-core/src/patchFlags.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export const TEXT = 1
|
||||
export const CLASS = 1 << 1
|
||||
export const STYLE = 1 << 2
|
||||
export const PROPS = 1 << 3
|
||||
export const KEYED = 1 << 4
|
||||
export const UNKEYED = 1 << 5
|
@ -1,449 +0,0 @@
|
||||
import {
|
||||
ComponentInstance,
|
||||
ComponentClass,
|
||||
FunctionalComponent
|
||||
} from './component'
|
||||
import { VNodeFlags, ChildrenFlags } from './flags'
|
||||
import { createComponentClassFromOptions } from './componentOptions'
|
||||
import { EMPTY_OBJ, isObject, isArray, isFunction, isString } from '@vue/shared'
|
||||
import { RawChildrenType, RawSlots } from './h'
|
||||
import { FunctionalHandle } from './createRenderer'
|
||||
|
||||
const handlersRE = /^on|^vnode/
|
||||
const STABLE_SLOTS_HINT = '$stable'
|
||||
|
||||
// Vue core is platform agnostic, so we are not using Element for "DOM" nodes.
|
||||
export interface RenderNode {
|
||||
vnode?: VNode | null
|
||||
// technically this doesn't exist on platform render nodes,
|
||||
// but we list it here so that TS can figure out union types
|
||||
$f: false
|
||||
}
|
||||
|
||||
export interface VNode {
|
||||
_isVNode: true
|
||||
flags: VNodeFlags
|
||||
tag: string | FunctionalComponent | ComponentClass | RenderNode | null
|
||||
data: VNodeData | null
|
||||
children: VNodeChildren
|
||||
childFlags: ChildrenFlags
|
||||
key: Key | null
|
||||
ref: Ref | null
|
||||
slots: Slots | null
|
||||
// only on mounted nodes
|
||||
el: RenderNode | null
|
||||
// only on mounted component nodes that is also a root node (HOCs)
|
||||
// points to parent component's placeholder vnode
|
||||
// this is used to update vnode.el for nested HOCs.
|
||||
parentVNode: VNode | null
|
||||
// only on mounted component nodes
|
||||
// points to the parent stateful/functional component's placeholder node
|
||||
contextVNode: VNode | null
|
||||
// only on mounted functional component nodes
|
||||
// a consistent handle so that a functional component can be identified
|
||||
// by the scheduler
|
||||
handle: FunctionalHandle | null
|
||||
// only on cloned vnodes, points to the original cloned vnode
|
||||
clonedFrom: VNode | null
|
||||
}
|
||||
|
||||
export interface MountedVNode extends VNode {
|
||||
el: RenderNode
|
||||
}
|
||||
|
||||
export interface BuiltInProps {
|
||||
key?: Key | null
|
||||
ref?: Ref | null
|
||||
slots?: RawSlots | null
|
||||
}
|
||||
|
||||
export type VNodeData = {
|
||||
[key: string]: any
|
||||
} & BuiltInProps
|
||||
|
||||
export type VNodeChildren =
|
||||
| VNode[] // ELEMENT | PORTAL
|
||||
| ComponentInstance // COMPONENT_STATEFUL
|
||||
| VNode // COMPONENT_FUNCTIONAL
|
||||
| string // TEXT
|
||||
| null
|
||||
|
||||
export type Key = string | number
|
||||
|
||||
export type Ref = (t: RenderNode | ComponentInstance | null) => void
|
||||
|
||||
export type Slot = (...args: any[]) => VNode[]
|
||||
|
||||
export type Slots = Readonly<{
|
||||
[name: string]: Slot
|
||||
}>
|
||||
|
||||
export function createVNode(
|
||||
flags: VNodeFlags,
|
||||
tag: string | FunctionalComponent | ComponentClass | RenderNode | null,
|
||||
data: VNodeData | null,
|
||||
children: RawChildrenType | null,
|
||||
childFlags: ChildrenFlags,
|
||||
key: Key | null | undefined,
|
||||
ref: Ref | null | undefined,
|
||||
slots: Slots | null | undefined
|
||||
): VNode {
|
||||
const vnode: VNode = {
|
||||
_isVNode: true,
|
||||
flags,
|
||||
tag,
|
||||
data,
|
||||
children: children as VNodeChildren,
|
||||
childFlags,
|
||||
key: key === void 0 ? null : key,
|
||||
ref: ref === void 0 ? null : ref,
|
||||
slots: slots === void 0 ? null : slots,
|
||||
el: null,
|
||||
parentVNode: null,
|
||||
contextVNode: null,
|
||||
handle: null,
|
||||
clonedFrom: null
|
||||
}
|
||||
if (childFlags === ChildrenFlags.UNKNOWN_CHILDREN) {
|
||||
normalizeChildren(vnode, children)
|
||||
}
|
||||
return vnode
|
||||
}
|
||||
|
||||
export function createElementVNode(
|
||||
tag: string,
|
||||
data: VNodeData | null,
|
||||
children: RawChildrenType | null,
|
||||
childFlags: ChildrenFlags,
|
||||
key?: Key | null,
|
||||
ref?: Ref | null
|
||||
) {
|
||||
const flags = tag === 'svg' ? VNodeFlags.ELEMENT_SVG : VNodeFlags.ELEMENT_HTML
|
||||
if (data !== null) {
|
||||
normalizeClassAndStyle(data)
|
||||
}
|
||||
return createVNode(flags, tag, data, children, childFlags, key, ref, null)
|
||||
}
|
||||
|
||||
function normalizeClassAndStyle(data: VNodeData) {
|
||||
if (data.class != null) {
|
||||
data.class = normalizeClass(data.class)
|
||||
}
|
||||
if (data.style != null) {
|
||||
data.style = normalizeStyle(data.style)
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeStyle(value: any): Record<string, string | number> | void {
|
||||
if (isArray(value)) {
|
||||
const res: Record<string, string | number> = {}
|
||||
for (let i = 0; i < value.length; i++) {
|
||||
const normalized = normalizeStyle(value[i])
|
||||
if (normalized) {
|
||||
for (const key in normalized) {
|
||||
res[key] = normalized[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
return res
|
||||
} else if (isObject(value)) {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeClass(value: any): string {
|
||||
let res = ''
|
||||
if (isString(value)) {
|
||||
res = value
|
||||
} else if (isArray(value)) {
|
||||
for (let i = 0; i < value.length; i++) {
|
||||
res += normalizeClass(value[i]) + ' '
|
||||
}
|
||||
} else if (isObject(value)) {
|
||||
for (const name in value) {
|
||||
if (value[name]) {
|
||||
res += name + ' '
|
||||
}
|
||||
}
|
||||
}
|
||||
return res.trim()
|
||||
}
|
||||
|
||||
export function createComponentVNode(
|
||||
comp: any,
|
||||
data: VNodeData | null,
|
||||
children: RawChildrenType | Slots,
|
||||
childFlags: ChildrenFlags,
|
||||
key?: Key | null,
|
||||
ref?: Ref | null
|
||||
) {
|
||||
// resolve type
|
||||
let flags: VNodeFlags
|
||||
|
||||
// flags
|
||||
if (isObject(comp)) {
|
||||
if (comp.functional) {
|
||||
// object literal functional
|
||||
flags = VNodeFlags.COMPONENT_FUNCTIONAL
|
||||
const { render } = comp
|
||||
if (!comp._normalized) {
|
||||
render.pure = comp.pure
|
||||
render.props = comp.props
|
||||
comp._normalized = true
|
||||
}
|
||||
comp = render
|
||||
} else {
|
||||
// object literal stateful
|
||||
flags = VNodeFlags.COMPONENT_STATEFUL_NORMAL
|
||||
comp =
|
||||
comp._normalized ||
|
||||
(comp._normalized = createComponentClassFromOptions(comp))
|
||||
}
|
||||
} else {
|
||||
// assumes comp is function here now
|
||||
if (__DEV__ && !isFunction(comp)) {
|
||||
// TODO warn invalid comp value in dev
|
||||
}
|
||||
if (comp.prototype && comp.prototype.render) {
|
||||
flags = VNodeFlags.COMPONENT_STATEFUL_NORMAL
|
||||
} else {
|
||||
flags = VNodeFlags.COMPONENT_FUNCTIONAL
|
||||
}
|
||||
}
|
||||
|
||||
if (__DEV__ && flags === VNodeFlags.COMPONENT_FUNCTIONAL && ref) {
|
||||
// TODO warn functional component cannot have ref
|
||||
}
|
||||
|
||||
// slots
|
||||
let slots: any
|
||||
if (childFlags === ChildrenFlags.STABLE_SLOTS) {
|
||||
slots = children
|
||||
} else if (childFlags === ChildrenFlags.UNKNOWN_CHILDREN) {
|
||||
childFlags = children
|
||||
? ChildrenFlags.DYNAMIC_SLOTS
|
||||
: ChildrenFlags.NO_CHILDREN
|
||||
if (children != null) {
|
||||
if (isFunction(children)) {
|
||||
// function as children
|
||||
slots = { default: children }
|
||||
} else if (isObject(children) && !(children as any)._isVNode) {
|
||||
// slot object as children
|
||||
slots = children
|
||||
// special manual optimization hint for raw render fn users
|
||||
if (slots[STABLE_SLOTS_HINT]) {
|
||||
childFlags = ChildrenFlags.STABLE_SLOTS
|
||||
}
|
||||
} else {
|
||||
slots = { default: () => children }
|
||||
}
|
||||
slots = normalizeSlots(slots)
|
||||
}
|
||||
}
|
||||
|
||||
// class & style
|
||||
if (data !== null) {
|
||||
normalizeClassAndStyle(data)
|
||||
}
|
||||
|
||||
return createVNode(
|
||||
flags,
|
||||
comp,
|
||||
data,
|
||||
null, // to be set during mount
|
||||
childFlags,
|
||||
key,
|
||||
ref,
|
||||
slots
|
||||
)
|
||||
}
|
||||
|
||||
export function createTextVNode(text: string): VNode {
|
||||
return createVNode(
|
||||
VNodeFlags.TEXT,
|
||||
null,
|
||||
null,
|
||||
text == null ? '' : text,
|
||||
ChildrenFlags.NO_CHILDREN,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
export function createFragment(
|
||||
children: RawChildrenType,
|
||||
childFlags?: ChildrenFlags,
|
||||
key?: Key | null
|
||||
) {
|
||||
return createVNode(
|
||||
VNodeFlags.FRAGMENT,
|
||||
null,
|
||||
null,
|
||||
children,
|
||||
childFlags === void 0 ? ChildrenFlags.UNKNOWN_CHILDREN : childFlags,
|
||||
key,
|
||||
null,
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
export function createPortal(
|
||||
target: RenderNode | string,
|
||||
children: RawChildrenType,
|
||||
childFlags?: ChildrenFlags,
|
||||
key?: Key | null,
|
||||
ref?: Ref | null
|
||||
): VNode {
|
||||
return createVNode(
|
||||
VNodeFlags.PORTAL,
|
||||
target,
|
||||
null,
|
||||
children,
|
||||
childFlags === void 0 ? ChildrenFlags.UNKNOWN_CHILDREN : childFlags,
|
||||
key,
|
||||
ref,
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
export function cloneVNode(vnode: VNode, extraData?: VNodeData): VNode {
|
||||
const { flags, data } = vnode
|
||||
if (flags & VNodeFlags.ELEMENT || flags & VNodeFlags.COMPONENT) {
|
||||
let clonedData = data
|
||||
if (extraData != null) {
|
||||
clonedData = {}
|
||||
if (data != null) {
|
||||
for (const key in data) {
|
||||
clonedData[key] = data[key]
|
||||
}
|
||||
}
|
||||
if (extraData !== EMPTY_OBJ) {
|
||||
for (const key in extraData) {
|
||||
if (key === 'class') {
|
||||
clonedData.class = normalizeClass([
|
||||
clonedData.class,
|
||||
extraData.class
|
||||
])
|
||||
} else if (key === 'style') {
|
||||
clonedData.style = normalizeStyle([
|
||||
clonedData.style,
|
||||
extraData.style
|
||||
])
|
||||
} else if (handlersRE.test(key)) {
|
||||
// on*, vnode*
|
||||
const existing = clonedData[key]
|
||||
clonedData[key] = existing
|
||||
? [].concat(existing, extraData[key])
|
||||
: extraData[key]
|
||||
} else {
|
||||
clonedData[key] = extraData[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const cloned = createVNode(
|
||||
flags,
|
||||
vnode.tag,
|
||||
clonedData,
|
||||
vnode.children as RawChildrenType,
|
||||
vnode.childFlags,
|
||||
vnode.key,
|
||||
vnode.ref,
|
||||
vnode.slots
|
||||
)
|
||||
cloned.clonedFrom = vnode.clonedFrom || vnode
|
||||
return cloned
|
||||
} else if (flags & VNodeFlags.TEXT) {
|
||||
return createTextVNode(vnode.children as string)
|
||||
} else {
|
||||
return vnode
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeChildren(vnode: VNode, children: any) {
|
||||
let childFlags
|
||||
if (isArray(children)) {
|
||||
const { length } = children
|
||||
if (length === 0) {
|
||||
childFlags = ChildrenFlags.NO_CHILDREN
|
||||
children = null
|
||||
} else if (length === 1) {
|
||||
childFlags = ChildrenFlags.SINGLE_VNODE
|
||||
children = children[0]
|
||||
if (children.el) {
|
||||
children = cloneVNode(children)
|
||||
}
|
||||
} else {
|
||||
childFlags = ChildrenFlags.KEYED_VNODES
|
||||
children = normalizeVNodes(children)
|
||||
}
|
||||
} else if (children == null) {
|
||||
childFlags = ChildrenFlags.NO_CHILDREN
|
||||
} else if (children._isVNode) {
|
||||
childFlags = ChildrenFlags.SINGLE_VNODE
|
||||
if (children.el) {
|
||||
children = cloneVNode(children)
|
||||
}
|
||||
} else {
|
||||
// primitives or invalid values, cast to string
|
||||
childFlags = ChildrenFlags.SINGLE_VNODE
|
||||
children = createTextVNode(children + '')
|
||||
}
|
||||
vnode.children = children
|
||||
vnode.childFlags = childFlags
|
||||
}
|
||||
|
||||
export function normalizeVNodes(
|
||||
children: any[],
|
||||
newChildren: VNode[] = [],
|
||||
currentPrefix: string = ''
|
||||
): VNode[] {
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
const child = children[i]
|
||||
let newChild
|
||||
if (child == null) {
|
||||
newChild = createTextVNode('')
|
||||
} else if (child._isVNode) {
|
||||
newChild = child.el ? cloneVNode(child) : child
|
||||
} else if (isArray(child)) {
|
||||
normalizeVNodes(child, newChildren, currentPrefix + i + '|')
|
||||
} else {
|
||||
newChild = createTextVNode(child + '')
|
||||
}
|
||||
if (newChild) {
|
||||
if (newChild.key == null) {
|
||||
newChild.key = currentPrefix + i
|
||||
}
|
||||
newChildren.push(newChild)
|
||||
}
|
||||
}
|
||||
return newChildren
|
||||
}
|
||||
|
||||
// ensure all slot functions return Arrays
|
||||
function normalizeSlots(slots: { [name: string]: any }): Slots {
|
||||
if (slots._normalized) {
|
||||
return slots
|
||||
}
|
||||
const normalized = { _normalized: true } as any
|
||||
for (const name in slots) {
|
||||
if (name === STABLE_SLOTS_HINT) {
|
||||
continue
|
||||
}
|
||||
normalized[name] = (...args: any[]) => normalizeSlot(slots[name](...args))
|
||||
}
|
||||
return normalized
|
||||
}
|
||||
|
||||
function normalizeSlot(value: any): VNode[] {
|
||||
if (value == null) {
|
||||
return [createTextVNode('')]
|
||||
} else if (isArray(value)) {
|
||||
return normalizeVNodes(value)
|
||||
} else if (value._isVNode) {
|
||||
return [value]
|
||||
} else {
|
||||
return [createTextVNode(value + '')]
|
||||
}
|
||||
}
|
@ -1,131 +0,0 @@
|
||||
import { ComponentType, ComponentClass, FunctionalComponent } from './component'
|
||||
import { EMPTY_OBJ, isString } from '@vue/shared'
|
||||
import { VNode } from './vdom'
|
||||
import { Data } from './componentOptions'
|
||||
|
||||
let stack: VNode[] = []
|
||||
|
||||
type TraceEntry = {
|
||||
type: VNode
|
||||
recurseCount: number
|
||||
}
|
||||
|
||||
type ComponentTraceStack = TraceEntry[]
|
||||
|
||||
export function pushWarningContext(vnode: VNode) {
|
||||
stack.push(vnode)
|
||||
}
|
||||
|
||||
export function popWarningContext() {
|
||||
stack.pop()
|
||||
}
|
||||
|
||||
export function warn(msg: string, ...args: any[]) {
|
||||
// TODO warn handler
|
||||
console.warn(`[Vue warn]: ${msg}`, ...args)
|
||||
const trace = getComponentTrace()
|
||||
if (!trace.length) {
|
||||
return
|
||||
}
|
||||
if (console.groupCollapsed) {
|
||||
console.groupCollapsed('at', ...formatTraceEntry(trace[0]))
|
||||
const logs: string[] = []
|
||||
trace.slice(1).forEach((entry, i) => {
|
||||
if (i !== 0) logs.push('\n')
|
||||
logs.push(...formatTraceEntry(entry, i + 1))
|
||||
})
|
||||
console.log(...logs)
|
||||
console.groupEnd()
|
||||
} else {
|
||||
const logs: string[] = []
|
||||
trace.forEach((entry, i) => {
|
||||
const formatted = formatTraceEntry(entry, i)
|
||||
if (i === 0) {
|
||||
logs.push('at', ...formatted)
|
||||
} else {
|
||||
logs.push('\n', ...formatted)
|
||||
}
|
||||
})
|
||||
console.log(...logs)
|
||||
}
|
||||
}
|
||||
|
||||
function getComponentTrace(): ComponentTraceStack {
|
||||
let current: VNode | null | undefined = stack[stack.length - 1]
|
||||
if (!current) {
|
||||
return []
|
||||
}
|
||||
|
||||
// we can't just use the stack because it will be incomplete during updates
|
||||
// that did not start from the root. Re-construct the parent chain using
|
||||
// contextVNode information.
|
||||
const normlaizedStack: ComponentTraceStack = []
|
||||
|
||||
while (current) {
|
||||
const last = normlaizedStack[0]
|
||||
if (last && last.type === current) {
|
||||
last.recurseCount++
|
||||
} else {
|
||||
normlaizedStack.push({
|
||||
type: current,
|
||||
recurseCount: 0
|
||||
})
|
||||
}
|
||||
current = current.contextVNode
|
||||
}
|
||||
|
||||
return normlaizedStack
|
||||
}
|
||||
|
||||
function formatTraceEntry(
|
||||
{ type, recurseCount }: TraceEntry,
|
||||
depth: number = 0
|
||||
): string[] {
|
||||
const padding = depth === 0 ? '' : ' '.repeat(depth * 2 + 1)
|
||||
const postfix =
|
||||
recurseCount > 0 ? `... (${recurseCount} recursive calls)` : ``
|
||||
const open = padding + `<${formatComponentName(type.tag as ComponentType)}`
|
||||
const close = `>` + postfix
|
||||
return type.data ? [open, ...formatProps(type.data), close] : [open + close]
|
||||
}
|
||||
|
||||
const classifyRE = /(?:^|[-_])(\w)/g
|
||||
const classify = (str: string): string =>
|
||||
str.replace(classifyRE, c => c.toUpperCase()).replace(/[-_]/g, '')
|
||||
|
||||
function formatComponentName(c: ComponentType, file?: string): string {
|
||||
let name: string
|
||||
|
||||
if (c.prototype && c.prototype.render) {
|
||||
// stateful
|
||||
const cc = c as ComponentClass
|
||||
const options = cc.options || EMPTY_OBJ
|
||||
name = options.displayName || cc.name
|
||||
} else {
|
||||
// functional
|
||||
const fc = c as FunctionalComponent
|
||||
name = fc.displayName || fc.name
|
||||
}
|
||||
|
||||
if (file && name === 'AnonymousComponent') {
|
||||
const match = file.match(/([^/\\]+)\.vue$/)
|
||||
if (match) {
|
||||
name = match[1]
|
||||
}
|
||||
}
|
||||
|
||||
return classify(name)
|
||||
}
|
||||
|
||||
function formatProps(props: Data) {
|
||||
const res = []
|
||||
for (const key in props) {
|
||||
const value = props[key]
|
||||
if (isString(value)) {
|
||||
res.push(`${key}=${JSON.stringify(value)}`)
|
||||
} else {
|
||||
res.push(`${key}=`, value)
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
Loading…
Reference in New Issue
Block a user