diff --git a/packages/runtime-core/src/component.ts b/packages/runtime-core/src/component.ts deleted file mode 100644 index 4e2aff24..00000000 --- a/packages/runtime-core/src/component.ts +++ /dev/null @@ -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

extends PublicInstanceMethods { - readonly $el: any - readonly $vnode: MountedVNode - readonly $parentVNode: MountedVNode - readonly $data: D - readonly $props: Readonly

- readonly $attrs: Readonly - readonly $slots: Slots - readonly $root: Component - readonly $parent: Component - readonly $children: Component[] - readonly $options: ComponentOptions - readonly $refs: Record - readonly $proxy: this -} - -interface PublicInstanceMethods { - $forceUpdate(): void - $nextTick(fn: () => any): Promise - $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

{ - data(): Partial - render(props: Readonly

, 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

(): Component -} - -export interface FunctionalComponent

{ - (props: P, slots: Slots, attrs: Data, parentVNode: VNode): any - props?: ComponentPropsOptions

- displayName?: string -} - -export type ComponentType = ComponentClass | FunctionalComponent - -// Internal type that represents a mounted instance. -// It extends ComponentImplementation with mounted instance properties. -export interface ComponentInstance

- extends ComponentImplementation, - Partial>, - Partial { - constructor: ComponentClass - render: APIMethods['render'] - - $vnode: MountedVNode - $data: D - $props: P - $attrs: Data - $slots: Slots - $root: ComponentProxy - $children: ComponentProxy[] - $options: ComponentOptions - $proxy: ComponentProxy - - _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 = {} - $proxy: ComponentProxy | null = null - - _rawData: Data | null = null - _computedGetters: Record | null = null - _watchHandles: Set | 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 { - 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 diff --git a/packages/runtime-core/src/componentComputed.ts b/packages/runtime-core/src/componentComputed.ts deleted file mode 100644 index 0cc7796e..00000000 --- a/packages/runtime-core/src/componentComputed.ts +++ /dev/null @@ -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 - -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) - } - } -} diff --git a/packages/runtime-core/src/componentInstance.ts b/packages/runtime-core/src/componentInstance.ts deleted file mode 100644 index f0dc70f4..00000000 --- a/packages/runtime-core/src/componentInstance.ts +++ /dev/null @@ -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( - instance: T -): ComponentProxy { - 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) -} diff --git a/packages/runtime-core/src/componentOptions.ts b/packages/runtime-core/src/componentOptions.ts deleted file mode 100644 index 6ce1092b..00000000 --- a/packages/runtime-core/src/componentOptions.ts +++ /dev/null @@ -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 - -export interface ComponentClassOptions

{ - props?: ComponentPropsOptions

- computed?: ComponentComputedOptions - watch?: ComponentWatchOptions - displayName?: string - fromOptions?: boolean -} - -export interface ComponentOptions< - P = {}, - D = {}, - This = ComponentInstance -> - extends ComponentClassOptions, - Partial>, - Partial { - // TODO other options - readonly [key: string]: any -} - -export type ComponentPropsOptions

= { - [K in keyof P]: PropValidator -} - -export type Prop = { (): T } | { new (...args: any[]): T & object } - -export type PropType = Prop | Prop[] - -export type PropValidator = PropOptions | PropType - -export interface PropOptions { - type?: PropType | true | null - required?: boolean - default?: T | null | undefined | (() => T | null | undefined) - validator?(value: T): boolean -} - -export interface ComponentComputedOptions { - [key: string]: ((this: This, c: This) => any) | SingleComputedOptions -} - -type SingleComputedOptions = { - get: (this: This, c: This) => any - set?: (value: any) => void - cache?: boolean -} - -export interface ComponentWatchOptions { - [key: string]: ComponentWatchOption -} - -export type ComponentWatchOption = - | WatchHandler - | WatchHandler[] - | WatchOptionsWithHandler - | string - -export type WatchHandler = ( - this: This, - val: any, - oldVal: any -) => void - -export interface WatchOptionsWithHandler extends WatchOptions { - handler: WatchHandler -} - -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)) - } -} diff --git a/packages/runtime-core/src/componentProps.ts b/packages/runtime-core/src/componentProps.ts deleted file mode 100644 index ba800495..00000000 --- a/packages/runtime-core/src/componentProps.ts +++ /dev/null @@ -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 - -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): string { - const match = ctor && ctor.toString().match(/^\s*function (\w+)/) - return match ? match[1] : '' -} - -function isSameType(a: Prop, b: Prop): boolean { - return getType(a) === getType(b) -} - -function getTypeIndex( - type: Prop, - expectedTypes: PropType | 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, - 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): 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') -} diff --git a/packages/runtime-core/src/componentProxy.ts b/packages/runtime-core/src/componentProxy.ts deleted file mode 100644 index b65cfe23..00000000 --- a/packages/runtime-core/src/componentProxy.ts +++ /dev/null @@ -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, 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, - 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 & { _self: T } - -export function createRenderProxy( - instance: T -): ComponentProxy { - debugger - return new Proxy(instance, renderProxyHandlers) as any -} diff --git a/packages/runtime-core/src/componentRenderUtils.ts b/packages/runtime-core/src/componentRenderUtils.ts deleted file mode 100644 index d327d0fe..00000000 --- a/packages/runtime-core/src/componentRenderUtils.ts +++ /dev/null @@ -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 - } -} diff --git a/packages/runtime-core/src/componentState.ts b/packages/runtime-core/src/componentState.ts deleted file mode 100644 index fec671ea..00000000 --- a/packages/runtime-core/src/componentState.ts +++ /dev/null @@ -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 -} diff --git a/packages/runtime-core/src/componentWatch.ts b/packages/runtime-core/src/componentWatch.ts deleted file mode 100644 index ad607e53..00000000 --- a/packages/runtime-core/src/componentWatch.ts +++ /dev/null @@ -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 = 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 -} diff --git a/packages/runtime-core/src/createRenderer.ts b/packages/runtime-core/src/createRenderer.ts index cd7927d6..e09e8ce3 100644 --- a/packages/runtime-core/src/createRenderer.ts +++ b/packages/runtime-core/src/createRenderer.ts @@ -1,1613 +1,300 @@ -import { - effect as createReactiveEffect, - stop as stopReactiveEffect, - ReactiveEffect, - immutable, - ReactiveEffectOptions -} from '@vue/observer' -import { - queueJob, - handleSchedulerError, - nextTick, - queuePostEffect, - flushEffects, - queueNodeOp -} from '@vue/scheduler' -import { VNodeFlags, ChildrenFlags } from './flags' -import { EMPTY_OBJ, reservedPropRE, isString } from '@vue/shared' -import { - VNode, - MountedVNode, - RenderNode, - createTextVNode, - cloneVNode, - VNodeChildren -} from './vdom' -import { ComponentInstance } from './component' -import { - createComponentInstance, - teardownComponentInstance -} from './componentInstance' -import { - renderInstanceRoot, - renderFunctionalRoot, - shouldUpdateComponent, - getReasonForComponentUpdate -} from './componentRenderUtils' -import { KeepAliveSymbol } from './optional/keepAlive' -import { pushWarningContext, popWarningContext, warn } from './warning' -import { resolveProps } from './componentProps' -import { - handleError, - ErrorTypes, - callLifecycleHookWithHandler -} from './errorHandling' +// TODO: +// - app context +// - component +// - lifecycle +// - refs +// - reused nodes +// - hydration -export interface NodeOps { - createElement: (tag: string, isSVG?: boolean) => any - createText: (text: string) => any - setText: (node: any, text: string) => void - appendChild: (parent: any, child: any) => void - insertBefore: (parent: any, child: any, ref: any) => void - removeChild: (parent: any, child: any) => void - clearContent: (node: any) => void - parentNode: (node: any) => any - nextSibling: (node: any) => any - querySelector: (selector: string) => any -} +import { Text, Fragment, Empty, createVNode } from './h.js' -export interface PatchDataFunction { - ( - el: any, - key: string, - prevValue: any, - nextValue: any, - preVNode: VNode | null, - nextVNode: VNode, - isSVG: boolean, - // passed for DOM operations that removes child content - // e.g. innerHTML & textContent - unmountChildren: (children: VNode[], childFlags: ChildrenFlags) => void - ): void -} +import { TEXT, CLASS, STYLE, PROPS, KEYED, UNKEYED } from './patchFlags' -export interface RendererOptions { - nodeOps: NodeOps - patchData: PatchDataFunction -} +const emptyArr: any[] = [] +const emptyObj = {} -export interface FunctionalHandle { - prev: VNode - next: VNode - update: ReactiveEffect - container: RenderNode | null -} +const isSameType = (n1, n2) => n1.type === n2.type && n1.key === n2.key -handleSchedulerError(err => handleError(err, null, ErrorTypes.SCHEDULER)) - -// The whole mounting / patching / unmouting logic is placed inside this -// single function so that we can create multiple renderes with different -// platform definitions. This allows for use cases like creating a test -// renderer alongside an actual renderer. -export function createRenderer(options: RendererOptions) { +export function createRenderer(hostConfig) { const { - nodeOps: { - createElement: platformCreateElement, - createText: platformCreateText, - setText: platformSetText, - appendChild: platformAppendChild, - insertBefore: platformInsertBefore, - removeChild: platformRemoveChild, - clearContent: platformClearContent, - parentNode: platformParentNode, - nextSibling: platformNextSibling, - querySelector: platformQuerySelector - }, - patchData: platformPatchData - } = options + insert, + remove, + patchProp: hostPatchProp, + createElement: hostCreateElement, + createText: hostCreateText, + createComment: hostCreateComment, + setText: hostSetText, + setElementText: hostSetElementText, + nextSibling: hostNextSibling + } = hostConfig - function queueInsertOrAppend( - container: RenderNode, - newNode: RenderNode, - refNode: RenderNode | null - ) { - if (refNode === null) { - queueNodeOp([platformAppendChild, container, newNode]) + function patch(n1, n2, container, anchor, optimized) { + // patching & not same type, unmount old tree + if (n1 != null && !isSameType(n1, n2)) { + anchor = hostNextSibling(n1.el) + unmount(n1, true) + n1 = null + } + + const { type } = n2 + if (type === Text) { + processText(n1, n2, container, anchor) + } else if (type === Empty) { + processEmptyNode(n1, n2, container, anchor) + } else if (type === Fragment) { + processFragment(n1, n2, container, anchor, optimized) + } else if (typeof type === 'function') { + // TODO Component } else { - queueNodeOp([platformInsertBefore, container, newNode, refNode]) + processElement(n1, n2, container, anchor, optimized) } } - // mounting ------------------------------------------------------------------ - - function mount( - vnode: VNode, - container: RenderNode | null, - contextVNode: MountedVNode | null, - isSVG: boolean, - endNode: RenderNode | null, - ownerArray?: VNode[], - index?: number - ) { - const { flags } = vnode - if (flags & VNodeFlags.ELEMENT) { - mountElement(vnode, container, contextVNode, isSVG, endNode) - } else if (flags & VNodeFlags.COMPONENT) { - mountComponent(vnode, container, contextVNode, isSVG, endNode) - } else if (flags & VNodeFlags.TEXT) { - mountText(vnode, container, endNode) - } else if (flags & VNodeFlags.FRAGMENT) { - mountFragment(vnode, container, contextVNode, isSVG, endNode) - } else if (flags & VNodeFlags.PORTAL) { - mountPortal(vnode, container, contextVNode) - } - } - - function mountArrayChildren( - children: VNode[], - container: RenderNode | null, - contextVNode: MountedVNode | null, - isSVG: boolean, - endNode: RenderNode | null - ) { - for (let i = 0; i < children.length; i++) { - const child = getNextVNode(children, i) - mount(child, container, contextVNode, isSVG, endNode) - } - } - - function mountElement( - vnode: VNode, - container: RenderNode | null, - contextVNode: MountedVNode | null, - isSVG: boolean, - endNode: RenderNode | null - ) { - const { flags, tag, data, children, childFlags, ref } = vnode - isSVG = isSVG || (flags & VNodeFlags.ELEMENT_SVG) > 0 - // element creation is not deferred since it doesn't produce - // user-affecting side effects until inserted into the DOM - const el = (vnode.el = platformCreateElement(tag as string, isSVG)) - if (data != null) { - for (const key in data) { - if (!reservedPropRE.test(key)) { - platformPatchData( - el, - key, - null, - data[key], - null, - vnode, - isSVG, - unmountChildren - ) - } - } - if (data.vnodeBeforeMount) { - data.vnodeBeforeMount(vnode) - } - } - if (childFlags !== ChildrenFlags.NO_CHILDREN) { - const hasSVGChildren = isSVG && tag !== 'foreignObject' - if (childFlags & ChildrenFlags.SINGLE_VNODE) { - mount(children as VNode, el, contextVNode, hasSVGChildren, null) - } else if (childFlags & ChildrenFlags.MULTIPLE_VNODES) { - mountArrayChildren( - children as VNode[], - el, - contextVNode, - hasSVGChildren, - null - ) - } - } - if (container != null) { - queueInsertOrAppend(container, el, endNode) - } - if (ref) { - queuePostEffect(() => { - ref(el) - }) - } - if (data != null && data.vnodeMounted) { - queuePostEffect(() => { - data.vnodeMounted(vnode) - }) - } - } - - function mountComponent( - vnode: VNode, - container: RenderNode | null, - contextVNode: MountedVNode | null, - isSVG: boolean, - endNode: RenderNode | null - ) { - vnode.contextVNode = contextVNode - const { flags } = vnode - if (flags & VNodeFlags.COMPONENT_STATEFUL) { - mountStatefulComponent(vnode, container, isSVG, endNode) + function processText(n1, n2, container, anchor) { + if (n1 == null) { + insert((n2.el = hostCreateText(n2.children)), container, anchor) } else { - mountFunctionalComponent(vnode, container, isSVG, endNode) + const el = (n2.el = n1.el) + if (n2.children !== n1.children) { + hostSetText(el, n2, children) + } } } - function mountStatefulComponent( - vnode: VNode, - container: RenderNode | null, - isSVG: boolean, - endNode: RenderNode | null - ) { - if (vnode.flags & VNodeFlags.COMPONENT_STATEFUL_KEPT_ALIVE) { - // kept-alive - activateComponentInstance(vnode, container, endNode) + function processEmptyNode(n1, n2, container, anchor) { + if (n1 == null) { + insert((n2.el = hostCreateComment('')), container, anchor) } else { - if (__COMPAT__) { - mountComponentInstance(vnode, container, isSVG, endNode) - } else { - queueJob(() => mountComponentInstance(vnode, container, isSVG, endNode)) - } + n2.el = n1.el } } - function mountFunctionalComponent( - vnode: VNode, - container: RenderNode | null, - isSVG: boolean, - endNode: RenderNode | null - ) { - if (__DEV__ && vnode.ref) { - warn( - `cannot use ref on a functional component because there is no ` + - `instance to reference to.` - ) - } - - const handle: FunctionalHandle = (vnode.handle = { - prev: vnode, - next: null as any, - update: null as any, - container - }) - - const doMount = () => { - handle.update = createReactiveEffect( - () => { - if (!handle.next) { - // initial mount - if (__DEV__) { - pushWarningContext(vnode) - } - const subTree = (vnode.children = renderFunctionalRoot(vnode)) - queuePostEffect(() => { - vnode.el = subTree.el as RenderNode - }) - mount(subTree, container, vnode as MountedVNode, isSVG, endNode) - handle.next = vnode - if (__DEV__) { - popWarningContext() - } - } else { - updateFunctionalComponent(handle, isSVG) - } - }, - { - scheduler: queueJob - } - ) - } - - // we are using vnode.ref to store the functional component's update job - if (__COMPAT__) { - doMount() + function processElement(n1, n2, container, anchor, optimized) { + // mount + if (n1 == null) { + mountElement(n2, container, anchor) } else { - queueJob(() => { - doMount() - // cleanup if mount is invalidated before committed - return () => { - stopReactiveEffect(handle.update) - } - }) + patchElement(n1, n2, container, optimized) } } - function updateFunctionalComponent(handle: FunctionalHandle, isSVG: boolean) { - const { prev, next } = handle - if (__DEV__) { - pushWarningContext(next) - } - const prevTree = prev.children as MountedVNode - const nextTree = (next.children = renderFunctionalRoot(next)) - queuePostEffect(() => { - next.el = nextTree.el - }) - patch( - prevTree, - nextTree, - handle.container as RenderNode, - next as MountedVNode, - isSVG - ) - if (__DEV__) { - popWarningContext() - } - } - - function mountText( - vnode: VNode, - container: RenderNode | null, - endNode: RenderNode | null - ) { - const el = (vnode.el = platformCreateText(vnode.children as string)) - if (container != null) { - queueInsertOrAppend(container, el, endNode) - } - } - - function mountFragment( - vnode: VNode, - container: RenderNode | null, - contextVNode: MountedVNode | null, - isSVG: boolean, - endNode: RenderNode | null - ) { - const { children, childFlags } = vnode - switch (childFlags) { - case ChildrenFlags.SINGLE_VNODE: - queuePostEffect(() => { - vnode.el = (children as MountedVNode).el - }) - mount(children as VNode, container, contextVNode, isSVG, endNode) - break - case ChildrenFlags.NO_CHILDREN: - const placeholder = createTextVNode('') - mountText(placeholder, container, null) - vnode.el = placeholder.el - break - default: - queuePostEffect(() => { - vnode.el = (children as MountedVNode[])[0].el - }) - mountArrayChildren( - children as VNode[], - container, - contextVNode, - isSVG, - endNode - ) - } - } - - function mountPortal( - vnode: VNode, - container: RenderNode | null, - contextVNode: MountedVNode | null - ) { - const { tag, children, childFlags, ref } = vnode - const target = isString(tag) ? platformQuerySelector(tag) : tag - - if (__DEV__ && !target) { - // TODO warn poartal target not found - } - - if (childFlags & ChildrenFlags.SINGLE_VNODE) { - mount(children as VNode, target as RenderNode, contextVNode, false, null) - } else if (childFlags & ChildrenFlags.MULTIPLE_VNODES) { - mountArrayChildren( - children as VNode[], - target as RenderNode, - contextVNode, - false, - null - ) - } - if (ref) { - queuePostEffect(() => { - ref(target) - }) - } - const placeholder = createTextVNode('') - mountText(placeholder, container, null) - vnode.el = placeholder.el - } - - // patching ------------------------------------------------------------------ - - function queuePatchData( - el: RenderNode | (() => RenderNode), - key: string, - prevValue: any, - nextValue: any, - preVNode: VNode | null, - nextVNode: VNode, - isSVG: boolean - ) { - if (!reservedPropRE.test(key)) { - queueNodeOp([ - platformPatchData, - el, - key, - prevValue, - nextValue, - preVNode, - nextVNode, - isSVG, - unmountChildren - ]) - } - } - - function patch( - prevVNode: MountedVNode, - nextVNode: VNode, - container: RenderNode, - contextVNode: MountedVNode | null, - isSVG: boolean - ) { - const nextFlags = nextVNode.flags - const prevFlags = prevVNode.flags - - if (prevFlags !== nextFlags) { - replaceVNode(prevVNode, nextVNode, container, contextVNode, isSVG) - } else if (nextFlags & VNodeFlags.ELEMENT) { - patchElement(prevVNode, nextVNode, container, contextVNode, isSVG) - } else if (nextFlags & VNodeFlags.COMPONENT) { - patchComponent(prevVNode, nextVNode, container, contextVNode, isSVG) - } else if (nextFlags & VNodeFlags.TEXT) { - patchText(prevVNode, nextVNode) - } else if (nextFlags & VNodeFlags.FRAGMENT) { - patchFragment(prevVNode, nextVNode, container, contextVNode, isSVG) - } else if (nextFlags & VNodeFlags.PORTAL) { - patchPortal(prevVNode, nextVNode, contextVNode) - } - } - - function patchElement( - prevVNode: MountedVNode, - nextVNode: VNode, - container: RenderNode, - contextVNode: MountedVNode | null, - isSVG: boolean - ) { - const { flags, tag, clonedFrom } = nextVNode - - // cloned vnodes pointing to the same original. - // these are hoisted static trees so just skip entirely - if ( - clonedFrom !== null && - (clonedFrom === prevVNode || clonedFrom === prevVNode.clonedFrom) - ) { - nextVNode.el = prevVNode.el - return - } - - isSVG = isSVG || (flags & VNodeFlags.ELEMENT_SVG) > 0 - - if (prevVNode.tag !== tag) { - replaceVNode(prevVNode, nextVNode, container, contextVNode, isSVG) - return - } - - const el = (nextVNode.el = prevVNode.el) - const prevData = prevVNode.data - const nextData = nextVNode.data - - if (nextData != null && nextData.vnodeBeforeUpdate) { - nextData.vnodeBeforeUpdate(nextVNode, prevVNode) - } - - // patch data - if (prevData !== nextData) { - const prevDataOrEmpty = prevData || EMPTY_OBJ - const nextDataOrEmpty = nextData || EMPTY_OBJ - if (nextDataOrEmpty !== EMPTY_OBJ) { - for (const key in nextDataOrEmpty) { - const prevValue = prevDataOrEmpty[key] - const nextValue = nextDataOrEmpty[key] - if (prevValue !== nextValue) { - queuePatchData( - el, - key, - prevValue, - nextValue, - prevVNode, - nextVNode, - isSVG - ) - } - } - } - if (prevDataOrEmpty !== EMPTY_OBJ) { - for (const key in prevDataOrEmpty) { - const prevValue = prevDataOrEmpty[key] - if (prevValue != null && !nextDataOrEmpty.hasOwnProperty(key)) { - queuePatchData( - el, - key, - prevValue, - null, - prevVNode, - nextVNode, - isSVG - ) - } - } + function mountElement(vnode, container, anchor) { + const el = (vnode.el = hostCreateElement(vnode.type)) + if (vnode.props != null) { + for (const key in vnode.props) { + hostPatchProp(el, key, vnode.props[key], null) } } - - // children - patchChildren( - prevVNode.childFlags, - nextVNode.childFlags, - prevVNode.children, - nextVNode.children, - el, - contextVNode, - isSVG && nextVNode.tag !== 'foreignObject', - null - ) - - if (nextData != null && nextData.vnodeUpdated) { - // TODO fix me - // vnodeUpdatedHooks.push(() => { - // nextData.vnodeUpdated(nextVNode, prevVNode) - // }) - } - } - - function patchComponent( - prevVNode: MountedVNode, - nextVNode: VNode, - container: RenderNode, - contextVNode: MountedVNode | null, - isSVG: boolean - ) { - nextVNode.contextVNode = contextVNode - const { tag, flags } = nextVNode - if (tag !== prevVNode.tag) { - replaceVNode(prevVNode, nextVNode, container, contextVNode, isSVG) - } else if (flags & VNodeFlags.COMPONENT_STATEFUL) { - patchStatefulComponent(prevVNode, nextVNode) + if (typeof vnode.children === 'string') { + hostSetElementText(el, vnode.children) } else { - patchFunctionalComponent(prevVNode, nextVNode, container) + mountChildren(vnode.children, el) + } + insert(el, container, anchor) + } + + function mountChildren(children, container, anchor, start = 0) { + for (let i = start; i < children.length; i++) { + const child = (children[i] = normalizeChild(children[i])) + patch(null, child, container, anchor) } } - function patchStatefulComponent(prevVNode: MountedVNode, nextVNode: VNode) { - const { data: prevData } = prevVNode - const { data: nextData, slots: nextSlots } = nextVNode - - const instance = (nextVNode.children = - prevVNode.children) as ComponentInstance - - if (nextData !== prevData) { - const { 0: props, 1: attrs } = resolveProps( - nextData, - instance.$options.props - ) - instance.$props = __DEV__ ? immutable(props) : props - instance.$attrs = __DEV__ ? immutable(attrs) : attrs - } - instance.$slots = nextSlots || EMPTY_OBJ - instance.$parentVNode = nextVNode as MountedVNode - - if (shouldUpdateComponent(prevVNode, nextVNode)) { - if (__DEV__ && instance.$options.renderTriggered) { - callLifecycleHookWithHandler( - instance.$options.renderTriggered, - instance.$proxy, - ErrorTypes.RENDER_TRIGGERED, - getReasonForComponentUpdate(prevVNode, nextVNode) - ) - } - instance.$forceUpdate() - } else if (instance.$vnode.flags & VNodeFlags.COMPONENT) { - instance.$vnode.contextVNode = nextVNode - } - nextVNode.el = instance.$vnode.el - } - - function patchFunctionalComponent( - prevVNode: MountedVNode, - nextVNode: VNode, - container: RenderNode - ) { - const prevTree = prevVNode.children as VNode - const handle = (nextVNode.handle = prevVNode.handle as FunctionalHandle) - handle.prev = prevVNode - handle.next = nextVNode - handle.container = container - - if (shouldUpdateComponent(prevVNode, nextVNode)) { - queueJob(handle.update) - } else if (prevTree.flags & VNodeFlags.COMPONENT) { - // functional component returned another component - prevTree.contextVNode = nextVNode - } - } - - function patchFragment( - prevVNode: MountedVNode, - nextVNode: VNode, - container: RenderNode, - contextVNode: MountedVNode | null, - isSVG: boolean - ) { - // determine the tail node of the previous fragment, - // then retrieve its next sibling to use as the end node for patchChildren. - const endNode = platformNextSibling(getVNodeLastEl(prevVNode)) - const { childFlags, children } = nextVNode - queuePostEffect(() => { - switch (childFlags) { - case ChildrenFlags.SINGLE_VNODE: - nextVNode.el = (children as MountedVNode).el - break - case ChildrenFlags.NO_CHILDREN: - nextVNode.el = prevVNode.el - break - default: - nextVNode.el = (children as MountedVNode[])[0].el - } - }) - patchChildren( - prevVNode.childFlags, - childFlags, - prevVNode.children, - children, - container, - contextVNode, - isSVG, - endNode - ) - } - - function getVNodeLastEl(vnode: MountedVNode): RenderNode { - const { el, flags, children, childFlags } = vnode - if (flags & VNodeFlags.FRAGMENT) { - if (childFlags & ChildrenFlags.SINGLE_VNODE) { - return getVNodeLastEl(children as MountedVNode) - } else if (childFlags & ChildrenFlags.MULTIPLE_VNODES) { - return getVNodeLastEl( - (children as MountedVNode[])[(children as MountedVNode[]).length - 1] - ) - } else { - return el - } + function normalizeChild(child) { + // empty placeholder + if (child == null) { + return createVNode(Empty) + } else if (typeof child === 'string' || typeof child === 'number') { + return createVNode(Text, null, child + '') + } else if (Array.isArray(child)) { + return createVNode(Fragment, null, child) } else { - return el + return child } } - function patchText(prevVNode: MountedVNode, nextVNode: VNode) { - const el = (nextVNode.el = prevVNode.el) as RenderNode - const nextText = nextVNode.children - if (nextText !== prevVNode.children) { - queueNodeOp([platformSetText, el, nextText]) - } - } + function patchElement(n1, n2, container, optimized) { + const el = (n2.el = n1.el) + const { patchFlag, dynamicChildren } = n2 + const oldProps = (n1 && n1.props) || emptyObj + const newProps = n2.props || emptyObj - function patchPortal( - prevVNode: MountedVNode, - nextVNode: VNode, - contextVNode: MountedVNode | null - ) { - const prevContainer = prevVNode.tag as RenderNode - const nextContainer = nextVNode.tag as RenderNode - const nextChildren = nextVNode.children - patchChildren( - prevVNode.childFlags, - nextVNode.childFlags, - prevVNode.children, - nextChildren, - prevContainer, - contextVNode, - false, - null - ) - nextVNode.el = prevVNode.el - if (nextContainer !== prevContainer) { - switch (nextVNode.childFlags) { - case ChildrenFlags.SINGLE_VNODE: - insertVNode(nextChildren as MountedVNode, nextContainer, null) - break - case ChildrenFlags.NO_CHILDREN: - break - default: - for (let i = 0; i < (nextChildren as MountedVNode[]).length; i++) { - insertVNode( - (nextChildren as MountedVNode[])[i], - nextContainer, - null - ) - } - break + if (patchFlag != null) { + // the presence of a patchFlag means this element's render code was + // generated by the compiler and can take the fast path. + // in this path old node and new node are guaranteed to have the same shape + // (i.e. at the exact same position in the source template) + + // class + // this flag is matched when the element has dynamic class bindings. + if (patchFlag & CLASS) { + // TODO handle full class API, potentially optimize at compilation stage? + if (oldProps.class !== newProps.class) { + el.className = newProps.class + } } - } - } - function replaceVNode( - prevVNode: MountedVNode, - nextVNode: VNode, - container: RenderNode, - contextVNode: MountedVNode | null, - isSVG: boolean - ) { - const refNode = platformNextSibling(getVNodeLastEl(prevVNode)) - queueRemoveVNode(prevVNode, container) - mount(nextVNode, container, contextVNode, isSVG, refNode) - } + // style + // this flag is matched when the element has dynamic style bindings + // TODO separate static and dynamic styles? + if (patchFlag & STYLE) { + setStyles(el.style, oldProps.style, newProps.style) + } - function patchChildren( - prevChildFlags: ChildrenFlags, - nextChildFlags: ChildrenFlags, - prevChildren: VNodeChildren, - nextChildren: VNodeChildren, - container: RenderNode, - contextVNode: MountedVNode | null, - isSVG: boolean, - endNode: RenderNode | null - ) { - switch (prevChildFlags) { - case ChildrenFlags.SINGLE_VNODE: - switch (nextChildFlags) { - case ChildrenFlags.SINGLE_VNODE: - patch( - prevChildren as MountedVNode, - nextChildren as VNode, - container, - contextVNode, - isSVG - ) - break - case ChildrenFlags.NO_CHILDREN: - queueRemoveVNode(prevChildren as MountedVNode, container) - break - default: - queueRemoveVNode(prevChildren as MountedVNode, container) - mountArrayChildren( - nextChildren as VNode[], - container, - contextVNode, - isSVG, - endNode - ) - break - } - break - case ChildrenFlags.NO_CHILDREN: - switch (nextChildFlags) { - case ChildrenFlags.SINGLE_VNODE: - mount( - nextChildren as VNode, - container, - contextVNode, - isSVG, - endNode - ) - break - case ChildrenFlags.NO_CHILDREN: - break - default: - mountArrayChildren( - nextChildren as VNode[], - container, - contextVNode, - isSVG, - endNode - ) - break - } - break - default: - // MULTIPLE_CHILDREN - if (nextChildFlags === ChildrenFlags.SINGLE_VNODE) { - queueRemoveChildren( - prevChildren as MountedVNode[], - container, - endNode - ) - mount(nextChildren as VNode, container, contextVNode, isSVG, endNode) - } else if (nextChildFlags === ChildrenFlags.NO_CHILDREN) { - queueRemoveChildren( - prevChildren as MountedVNode[], - container, - endNode - ) - } else { - const prevLength = (prevChildren as VNode[]).length - const nextLength = (nextChildren as VNode[]).length - if (prevLength === 0) { - if (nextLength > 0) { - mountArrayChildren( - nextChildren as VNode[], - container, - contextVNode, - isSVG, - endNode - ) - } - } else if (nextLength === 0) { - queueRemoveChildren( - prevChildren as MountedVNode[], - container, - endNode - ) - } else if ( - prevChildFlags === ChildrenFlags.KEYED_VNODES && - nextChildFlags === ChildrenFlags.KEYED_VNODES - ) { - patchKeyedChildren( - prevChildren as MountedVNode[], - nextChildren as VNode[], - container, - prevLength, - nextLength, - contextVNode, - isSVG, - endNode - ) - } else { - patchNonKeyedChildren( - prevChildren as MountedVNode[], - nextChildren as VNode[], - container, - prevLength, - nextLength, - contextVNode, - isSVG, - endNode - ) + // props + // This flag is matched when the element has dynamic prop/attr bindings + // other than class and style. The keys of dynamic prop/attrs are saved for + // faster iteration. + // Note dynamic keys like :[foo]="bar" will cause this optimization to + // bail out and go through a full diff because we need to unset the old key + if (patchFlag & PROPS) { + const propsToUpdate = n2.dynamicProps + for (let i = 0; i < propsToUpdate.length; i++) { + const key = propsToUpdate[i] + const prev = oldProps[key] + const next = newProps[key] + if (prev !== next) { + hostPatchProp(el, key, next, prev) } } - break + } + + // text + // This flag is matched when the element has only dynamic text children. + // this flag is terminal (i.e. skips children diffing). + if (patchFlag & TEXT) { + if (n1.children !== n2.children) { + hostSetElementText(el, n2.children) + } + return // terminal + } + } else if (!optimized) { + // unoptimized, full diff + patchProps(el, oldProps, newProps) + } + + if (dynamicChildren != null) { + // children fast path + const olddynamicChildren = n1.dynamicChildren + for (let i = 0; i < dynamicChildren.length; i++) { + patch(olddynamicChildren[i], dynamicChildren[i], el, null, true) + } + } else if (!optimized) { + // full diff + patchChildren(n1, n2, el) } } - function patchNonKeyedChildren( - prevChildren: MountedVNode[], - nextChildren: VNode[], - container: RenderNode, - prevLength: number, - nextLength: number, - contextVNode: MountedVNode | null, - isSVG: boolean, - endNode: RenderNode | null - ) { - const commonLength = prevLength > nextLength ? nextLength : prevLength - let i = 0 - let nextChild - let prevChild - for (i; i < commonLength; i++) { - nextChild = getNextVNode(nextChildren, i) - prevChild = prevChildren[i] - patch(prevChild, nextChild, container, contextVNode, isSVG) - prevChildren[i] = nextChild as MountedVNode - } - if (prevLength < nextLength) { - for (i = commonLength; i < nextLength; i++) { - nextChild = getNextVNode(nextChildren, i) - mount(nextChild, container, contextVNode, isSVG, endNode) + function patchProps(el, oldProps, newProps) { + if (oldProps !== newProps) { + for (const key in newProps) { + const next = newProps[key] + const prev = oldProps[key] + if (next !== prev) { + hostPatchProp(el, key, next, prev) + } } - } else if (prevLength > nextLength) { - for (i = commonLength; i < prevLength; i++) { - queueRemoveVNode(prevChildren[i], container) + if (oldProps !== emptyObj) { + for (const key in oldProps) { + if (!(key in newProps)) { + hostPatchProp(el, key, null, null) + } + } } } } - function patchKeyedChildren( - prevChildren: MountedVNode[], - nextChildren: VNode[], - container: RenderNode, - prevLength: number, - nextLength: number, - contextVNode: MountedVNode | null, - isSVG: boolean, - endNode: RenderNode | null - ) { - let prevEnd = prevLength - 1 - let nextEnd = nextLength - 1 - let i - let j = 0 - let prevVNode = prevChildren[j] - let nextVNode = getNextVNode(nextChildren, j) - let nextPos - - outer: { - // Sync nodes with the same key at the beginning. - while (prevVNode.key === nextVNode.key) { - patch(prevVNode, nextVNode, container, contextVNode, isSVG) - prevChildren[j] = nextVNode as MountedVNode - j++ - if (j > prevEnd || j > nextEnd) { - break outer - } - prevVNode = prevChildren[j] - nextVNode = getNextVNode(nextChildren, j) - } - - prevVNode = prevChildren[prevEnd] - nextVNode = getNextVNode(nextChildren, nextEnd) - - // Sync nodes with the same key at the end. - while (prevVNode.key === nextVNode.key) { - patch(prevVNode, nextVNode, container, contextVNode, isSVG) - prevChildren[prevEnd] = nextVNode as MountedVNode - prevEnd-- - nextEnd-- - if (j > prevEnd || j > nextEnd) { - break outer - } - prevVNode = prevChildren[prevEnd] - nextVNode = getNextVNode(nextChildren, nextEnd) - } - } - - if (j > prevEnd) { - if (j <= nextEnd) { - nextPos = nextEnd + 1 - const nextNode = - nextPos < nextLength ? nextChildren[nextPos].el : endNode - while (j <= nextEnd) { - nextVNode = getNextVNode(nextChildren, j) - j++ - mount(nextVNode, container, contextVNode, isSVG, nextNode) - } - } - } else if (j > nextEnd) { - while (j <= prevEnd) { - queueRemoveVNode(prevChildren[j++], container) - } + function processFragment(n1, n2, container, anchor, optimized) { + const fragmentAnchor = (n2.el = n1 ? n1.el : document.createComment('')) + if (n1 == null) { + insert(fragmentAnchor, container, anchor) + mountChildren(n2.children, container, fragmentAnchor) } else { - let prevStart = j - const nextStart = j - const prevLeft = prevEnd - j + 1 - const nextLeft = nextEnd - j + 1 - const sources: number[] = [] - for (i = 0; i < nextLeft; i++) { - sources.push(0) - } - // Keep track if its possible to remove whole DOM using textContent = '' - let canRemoveWholeContent = prevLeft === prevLength - let moved = false - let pos = 0 - let patched = 0 - - // When sizes are small, just loop them through - if (nextLength < 4 || (prevLeft | nextLeft) < 32) { - for (i = prevStart; i <= prevEnd; i++) { - prevVNode = prevChildren[i] - if (patched < nextLeft) { - for (j = nextStart; j <= nextEnd; j++) { - nextVNode = getNextVNode(nextChildren, j) - if (prevVNode.key === nextVNode.key) { - sources[j - nextStart] = i + 1 - if (canRemoveWholeContent) { - canRemoveWholeContent = false - while (i > prevStart) { - queueRemoveVNode(prevChildren[prevStart++], container) - } - } - if (pos > j) { - moved = true - } else { - pos = j - } - patch(prevVNode, nextVNode, container, contextVNode, isSVG) - patched++ - break - } - } - if (!canRemoveWholeContent && j > nextEnd) { - queueRemoveVNode(prevVNode, container) - } - } else if (!canRemoveWholeContent) { - queueRemoveVNode(prevVNode, container) - } - } - } else { - const keyIndex: Record = {} - - // Map keys by their index - for (i = nextStart; i <= nextEnd; i++) { - keyIndex[nextChildren[i].key as string] = i - } - - // Try to patch same keys - for (i = prevStart; i <= prevEnd; i++) { - prevVNode = prevChildren[i] - - if (patched < nextLeft) { - j = keyIndex[prevVNode.key as string] - - if (j !== void 0) { - if (canRemoveWholeContent) { - canRemoveWholeContent = false - while (i > prevStart) { - queueRemoveVNode(prevChildren[prevStart++], container) - } - } - nextVNode = getNextVNode(nextChildren, j) - sources[j - nextStart] = i + 1 - if (pos > j) { - moved = true - } else { - pos = j - } - patch(prevVNode, nextVNode, container, contextVNode, isSVG) - patched++ - } else if (!canRemoveWholeContent) { - queueRemoveVNode(prevVNode, container) - } - } else if (!canRemoveWholeContent) { - queueRemoveVNode(prevVNode, container) - } - } - } - // fast-path: if nothing patched remove all old and add all new - if (canRemoveWholeContent) { - queueRemoveChildren(prevChildren as MountedVNode[], container, endNode) - mountArrayChildren( - nextChildren, - container, - contextVNode, - isSVG, - endNode - ) - } else { - if (moved) { - const seq = lis(sources) - j = seq.length - 1 - for (i = nextLeft - 1; i >= 0; i--) { - if (sources[i] === 0) { - pos = i + nextStart - nextVNode = getNextVNode(nextChildren, pos) - nextPos = pos + 1 - mount( - nextVNode, - container, - contextVNode, - isSVG, - nextPos < nextLength ? nextChildren[nextPos].el : endNode - ) - } else if (j < 0 || i !== seq[j]) { - pos = i + nextStart - nextVNode = nextChildren[pos] - nextPos = pos + 1 - insertVNode( - nextVNode as MountedVNode, - container, - nextPos < nextLength ? nextChildren[nextPos].el : endNode - ) - } else { - j-- - } - } - } else if (patched !== nextLeft) { - // when patched count doesn't match b length we need to insert those - // new ones loop backwards so we can use insertBefore - for (i = nextLeft - 1; i >= 0; i--) { - if (sources[i] === 0) { - pos = i + nextStart - nextVNode = getNextVNode(nextChildren, pos) - nextPos = pos + 1 - mount( - nextVNode, - container, - contextVNode, - isSVG, - nextPos < nextLength ? nextChildren[nextPos].el : endNode - ) - } - } - } - } + patchChildren(n1, n2, container, fragmentAnchor, optimized) } } - function insertVNode( - vnode: MountedVNode, - container: RenderNode, - refNode: RenderNode | null - ) { - const { flags, childFlags, children } = vnode - if (flags & VNodeFlags.FRAGMENT) { - switch (childFlags) { - case ChildrenFlags.SINGLE_VNODE: - insertVNode(children as MountedVNode, container, refNode) - break - case ChildrenFlags.NO_CHILDREN: - break - default: - for (let i = 0; i < (children as MountedVNode[]).length; i++) { - insertVNode((children as MountedVNode[])[i], container, refNode) - } - } - } else { - queueInsertOrAppend(container, vnode.el as RenderNode, refNode) - } - } + function patchChildren(n1, n2, container, anchor, optimized) { + const c1 = n1 && n1.children + const c2 = n2.children - // unmounting ---------------------------------------------------------------- - - function unmount(vnode: MountedVNode) { - const { flags, data, children, childFlags, ref, handle } = vnode - const isElement = flags & VNodeFlags.ELEMENT - if (isElement || flags & VNodeFlags.FRAGMENT) { - if (isElement && data != null && data.vnodeBeforeUnmount) { - data.vnodeBeforeUnmount(vnode) - } - unmountChildren(children as VNodeChildren, childFlags) - if (isElement && data != null && data.vnodeUnmounted) { - data.vnodeUnmounted(vnode) - } - } else if (flags & VNodeFlags.COMPONENT) { - if (flags & VNodeFlags.COMPONENT_STATEFUL) { - if (flags & VNodeFlags.COMPONENT_STATEFUL_SHOULD_KEEP_ALIVE) { - deactivateComponentInstance(children as ComponentInstance) - } else { - unmountComponentInstance(children as ComponentInstance) - } - } else { - // functional - stopReactiveEffect((handle as FunctionalHandle).update) - unmount(children as MountedVNode) - } - } else if (flags & VNodeFlags.PORTAL) { - if (childFlags & ChildrenFlags.MULTIPLE_VNODES) { - queueRemoveChildren( - children as MountedVNode[], - vnode.tag as RenderNode, - null - ) - } else if (childFlags === ChildrenFlags.SINGLE_VNODE) { - queueRemoveVNode(children as MountedVNode, vnode.tag as RenderNode) - } - } - if (ref) { - ref(null) - } - } - - function unmountChildren(children: VNodeChildren, childFlags: ChildrenFlags) { - if (childFlags & ChildrenFlags.MULTIPLE_VNODES) { - unmountArrayChildren(children as MountedVNode[]) - } else if (childFlags === ChildrenFlags.SINGLE_VNODE) { - unmount(children as MountedVNode) - } - } - - function unmountArrayChildren(children: MountedVNode[]) { - for (let i = 0; i < children.length; i++) { - unmount(children[i]) - } - } - - function queueRemoveVNode(vnode: MountedVNode, container: RenderNode) { - queueNodeOp([removeVNode, vnode, container]) - } - - function removeVNode(vnode: MountedVNode, container: RenderNode) { - unmount(vnode) - const { el, flags, children, childFlags } = vnode - if (container && el) { - if (flags & VNodeFlags.FRAGMENT) { - switch (childFlags) { - case ChildrenFlags.SINGLE_VNODE: - removeVNode(children as MountedVNode, container) - break - case ChildrenFlags.NO_CHILDREN: - platformRemoveChild(container, el) - break - default: - for (let i = 0; i < (children as MountedVNode[]).length; i++) { - removeVNode((children as MountedVNode[])[i], container) - } - } - } else { - platformRemoveChild(container, el) - } - ;(vnode as any).el = null - } - } - - function queueRemoveChildren( - children: MountedVNode[], - container: RenderNode, - refNode: RenderNode | null - ) { - queueNodeOp([removeChildren, children, container, refNode]) - } - - function removeChildren( - children: MountedVNode[], - container: RenderNode, - refNode: RenderNode | null - ) { - unmountArrayChildren(children) - if (refNode === null) { - platformClearContent(container) - } else { - for (let i = 0; i < children.length; i++) { - removeVNode(children[i], container) - } - } - } - - // Component lifecycle ------------------------------------------------------- - - function mountComponentInstance( - vnode: VNode, - container: RenderNode | null, - isSVG: boolean, - endNode: RenderNode | null - ): Function { - if (__DEV__) { - pushWarningContext(vnode) - } - - // a vnode may already have an instance if this is a compat call with - // new Vue() - const instance = ((__COMPAT__ && vnode.children) || - (vnode.children = createComponentInstance( - vnode as any - ))) as ComponentInstance - - // inject platform-specific unmount to keep-alive container - if ((vnode.tag as any)[KeepAliveSymbol] === true) { - ;(instance as any).$unmount = unmountComponentInstance - } - - const { - $proxy, - $options: { beforeMount, mounted, renderTracked, renderTriggered } - } = instance - - instance.$forceUpdate = () => { - queueJob(instance._update) - } - - const effectOptions: ReactiveEffectOptions = { - scheduler: queueJob - } - - if (__DEV__) { - if (renderTracked) { - effectOptions.onTrack = event => { - callLifecycleHookWithHandler( - renderTracked, - $proxy, - ErrorTypes.RENDER_TRACKED, - event - ) - } - } - if (renderTriggered) { - effectOptions.onTrigger = event => { - callLifecycleHookWithHandler( - renderTriggered, - $proxy, - ErrorTypes.RENDER_TRIGGERED, - event - ) - } - } - } - - instance._update = createReactiveEffect(() => { - if (instance._unmounted) { + // fast path + const { patchFlag } = n2 + if (patchFlag != null) { + if (patchFlag & KEYED) { + // this could be either fully-keyed or mixed (some keyed some not) + patchKeyedChildren(c1, c2, container, anchor, optimized) + return + } else if (patchFlag & UNKEYED) { + // unkeyed + patchUnkeyedChildren(c1, c2, container, anchor, optimized) return } - if (instance._mounted) { - updateComponentInstance(instance, isSVG) - } else { - if (beforeMount) { - callLifecycleHookWithHandler( - beforeMount, - $proxy, - ErrorTypes.BEFORE_MOUNT - ) - } + } - instance.$vnode = renderInstanceRoot(instance) as MountedVNode - - queuePostEffect(() => { - vnode.el = instance.$vnode.el - if (__COMPAT__) { - // expose __vue__ for devtools - ;(vnode.el as any).__vue__ = instance - } - if (vnode.ref) { - vnode.ref($proxy) - } - if (mounted) { - callLifecycleHookWithHandler(mounted, $proxy, ErrorTypes.MOUNTED) - } - instance._mounted = true - }) - - mount(instance.$vnode, container, vnode as MountedVNode, isSVG, endNode) - } - }, effectOptions) - - if (__DEV__) { - popWarningContext() - } - - // cleanup if mount is invalidated before committed - return () => { - teardownComponentInstance(instance) - } - } - - function updateComponentInstance( - instance: ComponentInstance, - isSVG: boolean - ) { - if (__DEV__ && instance.$parentVNode) { - pushWarningContext(instance.$parentVNode as VNode) - } - - const { - $vnode: prevVNode, - $parentVNode, - $proxy, - $options: { beforeUpdate } - } = instance - if (beforeUpdate) { - callLifecycleHookWithHandler( - beforeUpdate, - $proxy, - ErrorTypes.BEFORE_UPDATE, - prevVNode - ) - } - - const nextVNode = renderInstanceRoot(instance) as MountedVNode - - queuePostEffect(() => { - instance.$vnode = nextVNode - const el = nextVNode.el as RenderNode - if (__COMPAT__) { - // expose __vue__ for devtools - ;(el as any).__vue__ = instance - } - // recursively update contextVNode el for nested HOCs - if ((nextVNode.flags & VNodeFlags.PORTAL) === 0) { - let vnode = $parentVNode - while (vnode !== null) { - if ((vnode.flags & VNodeFlags.COMPONENT) > 0) { - vnode.el = el - } - vnode = vnode.contextVNode - } - } - const { updated } = instance.$options - if (updated) { - callLifecycleHookWithHandler( - updated, - $proxy, - ErrorTypes.UPDATED, - nextVNode - ) - } - - // TODO fix me - // if (vnodeUpdatedHooks.length > 0) { - // const vnodeUpdatedHooksForCurrentInstance = vnodeUpdatedHooks.slice() - // vnodeUpdatedHooks.length = 0 - // for (let i = 0; i < vnodeUpdatedHooksForCurrentInstance.length; i++) { - // vnodeUpdatedHooksForCurrentInstance[i]() - // } - // } - }) - - const container = platformParentNode(prevVNode.el) as RenderNode - patch(prevVNode, nextVNode, container, $parentVNode as MountedVNode, isSVG) - - if (__DEV__ && instance.$parentVNode) { - popWarningContext() - } - } - - function unmountComponentInstance(instance: ComponentInstance) { - if (instance._unmounted) { - return - } - const { - $vnode, - $proxy, - $options: { beforeUnmount, unmounted } - } = instance - if (beforeUnmount) { - callLifecycleHookWithHandler( - beforeUnmount, - $proxy, - ErrorTypes.BEFORE_UNMOUNT - ) - } - if ($vnode) { - unmount($vnode) - } - teardownComponentInstance(instance) - instance._unmounted = true - if (unmounted) { - callLifecycleHookWithHandler(unmounted, $proxy, ErrorTypes.UNMOUNTED) - } - } - - // Keep Alive ---------------------------------------------------------------- - - function activateComponentInstance( - vnode: VNode, - container: RenderNode | null, - endNode: RenderNode | null - ) { - if (__DEV__) { - pushWarningContext(vnode) - } - const instance = vnode.children as ComponentInstance - vnode.el = instance.$el as RenderNode - if (container != null) { - insertVNode(instance.$vnode, container, endNode) - } - if (__DEV__) { - popWarningContext() - } - queuePostEffect(() => { - callActivatedHook(instance, true) - }) - } - - function callActivatedHook(instance: ComponentInstance, asRoot: boolean) { - // 1. check if we are inside an inactive parent tree. - if (asRoot) { - instance._inactiveRoot = false - if (isInInactiveTree(instance)) return - } - if (asRoot || !instance._inactiveRoot) { - // 2. recursively call activated on child tree, depth-first - const { - $children, - $proxy, - $options: { activated } - } = instance - for (let i = 0; i < $children.length; i++) { - callActivatedHook($children[i], false) - } - if (activated) { - callLifecycleHookWithHandler(activated, $proxy, ErrorTypes.ACTIVATED) - } - } - } - - function deactivateComponentInstance(instance: ComponentInstance) { - callDeactivateHook(instance, true) - } - - function callDeactivateHook(instance: ComponentInstance, asRoot: boolean) { - if (asRoot) { - instance._inactiveRoot = true - if (isInInactiveTree(instance)) return - } - if (asRoot || !instance._inactiveRoot) { - // 2. recursively call deactivated on child tree, depth-first - const { - $children, - $proxy, - $options: { deactivated } - } = instance - for (let i = 0; i < $children.length; i++) { - callDeactivateHook($children[i], false) - } - if (deactivated) { - callLifecycleHookWithHandler( - deactivated, - $proxy, - ErrorTypes.DEACTIVATED - ) - } - } - } - - function isInInactiveTree(instance: ComponentInstance): boolean { - while ((instance = instance.$parent as any) !== null) { - if (instance._inactiveRoot) return true - } - return false - } - - // TODO hydrating ------------------------------------------------------------ - - // API ----------------------------------------------------------------------- - - function render(vnode: VNode | null, container: any) { - const prevVNode = container.vnode - if (vnode && vnode.el) { - vnode = cloneVNode(vnode) - } - if (prevVNode == null) { - if (vnode) { - mount(vnode, container, null, false, null) - container.vnode = vnode + if (typeof c2 === 'string') { + // text children fast path + if (Array.isArray(c1)) { + unmountChildren(c1, false) } + hostSetElementText(container, c2) } else { - if (vnode) { - patch(prevVNode, vnode, container, null, false) - container.vnode = vnode + if (typeof c1 === 'string') { + hostSetElementText('') + mountChildren(c2, container, anchor) } else { - queueRemoveVNode(prevVNode, container) - container.vnode = null + // two arrays, cannot assume anything, do full diff + patchKeyedChildren(c1, c2, container, anchor, optimized) } } - if (__COMPAT__) { - flushEffects() - return vnode && vnode.flags & VNodeFlags.COMPONENT_STATEFUL - ? (vnode.children as ComponentInstance).$proxy - : null + } + + function patchUnkeyedChildren(c1, c2, container, anchor, optimized) { + c1 = c1 || emptyArr + c2 = c2 || emptyArr + const oldLength = c1.length + const newLength = c2.length + const commonLength = Math.min(oldLength, newLength) + let i + for (i = 0; i < commonLength; i++) { + const nextChild = (c2[i] = normalizeChild(c2[i])) + patch(c1[i], nextChild, container, null, optimized) + } + if (oldLength > newLength) { + // remove old + unmountChildren(c1, commonLength, true) } else { - return nextTick(() => { - return vnode && vnode.flags & VNodeFlags.COMPONENT_STATEFUL - ? (vnode.children as ComponentInstance).$proxy - : null - }) + // mount new + mountChildren(c2, container, anchor, commonLength) } } - return { render } -} + // can be all-keyed or mixed + function patchKeyedChildren(c1, c2, container, anchor, optimized) { + // TODO + patchUnkeyedChildren(c1, c2, container, anchor, optimized) + } -// Utils ----------------------------------------------------------------------- - -// retrieves a vnode from a children array, making sure to clone it if the -// vnode is already mounted. -function getNextVNode(ownerArray: VNode[], index: number): VNode { - const vnode = ownerArray[index] - return vnode.el === null ? vnode : (ownerArray[index] = cloneVNode(vnode)) -} - -// https://en.wikipedia.org/wiki/Longest_increasing_subsequence -function lis(arr: number[]): number[] { - const p = arr.slice() - const result = [0] - let i - let j - let u - let v - let c - const len = arr.length - for (i = 0; i < len; i++) { - const arrI = arr[i] - if (arrI !== 0) { - j = result[result.length - 1] - if (arr[j] < arrI) { - p[i] = j - result.push(i) - continue - } - u = 0 - v = result.length - 1 - while (u < v) { - c = ((u + v) / 2) | 0 - if (arr[result[c]] < arrI) { - u = c + 1 - } else { - v = c - } - } - if (arrI < arr[result[u]]) { - if (u > 0) { - p[i] = result[u - 1] - } - result[u] = i + function unmount(vnode, doRemove) { + if (doRemove) { + if (vnode.type === Fragment) { + unmountChildren(vnode.children, 0, doRemove) } + remove(vnode.el) + } + if (Array.isArray(vnode.children)) { + unmountChildren(vnode.children) } } - u = result.length - v = result[u - 1] - while (u-- > 0) { - result[u] = v - v = p[v] + + function unmountChildren(children, start = 0, doRemove) { + for (let i = start; i < children.length; i++) { + unmount(children[i], doRemove) + } + } + + return function render(vnode, dom) { + patch(dom._vnode, vnode, dom) + return (dom._vnode = vnode) } - return result } diff --git a/packages/runtime-core/src/errorHandling.ts b/packages/runtime-core/src/errorHandling.ts deleted file mode 100644 index 2a0abd81..00000000 --- a/packages/runtime-core/src/errorHandling.ts +++ /dev/null @@ -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 = { - [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).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 - } -} diff --git a/packages/runtime-core/src/flags.ts b/packages/runtime-core/src/flags.ts deleted file mode 100644 index 159bd9a6..00000000 --- a/packages/runtime-core/src/flags.ts +++ /dev/null @@ -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 -} diff --git a/packages/runtime-core/src/h.ts b/packages/runtime-core/src/h.ts index c2dc3be4..095191c9 100644 --- a/packages/runtime-core/src/h.ts +++ b/packages/runtime-core/src/h.ts @@ -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 {} -type OptionsComponent

= - | (ComponentOptions

& { template: string }) - | (ComponentOptions

& { 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 -

(tag: OptionsComponent

, children?: RawChildrenType): VNode -

( - tag: OptionsComponent

, - data?: (P & BuiltInProps & Differ) | null, - children?: RawChildrenType | RawSlots - ): VNode - // functional -

(tag: FunctionalComponent

, children?: RawChildrenType): VNode -

( - tag: FunctionalComponent

, - data?: (P & BuiltInProps & Differ) | null, - children?: RawChildrenType | RawSlots - ): VNode - // class -

(tag: new () => Component

, children?: RawChildrenType): VNode -

( - tag: new () => Component

, - 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 +} diff --git a/packages/runtime-core/src/index.ts b/packages/runtime-core/src/index.ts index be2cb6cc..1ea0a177 100644 --- a/packages/runtime-core/src/index.ts +++ b/packages/runtime-core/src/index.ts @@ -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' diff --git a/packages/runtime-core/src/optional/asyncComponent.ts b/packages/runtime-core/src/optional/asyncComponent.ts deleted file mode 100644 index 61920571..00000000 --- a/packages/runtime-core/src/optional/asyncComponent.ts +++ /dev/null @@ -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 - 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 -} diff --git a/packages/runtime-core/src/optional/await.ts b/packages/runtime-core/src/optional/await.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/packages/runtime-core/src/optional/directives.ts b/packages/runtime-core/src/optional/directives.ts deleted file mode 100644 index ba09cee6..00000000 --- a/packages/runtime-core/src/optional/directives.ts +++ /dev/null @@ -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 - -const valueCache = new WeakMap>() - -export function applyDirective( - data: VNodeData, - instance: ComponentInstance, - directive: Directive, - value?: any, - arg?: string, - modifiers?: DirectiveModifiers -) { - let valueCacheForDir = valueCache.get(directive) as WeakMap - if (!valueCacheForDir) { - valueCacheForDir = new WeakMap() - 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 -} diff --git a/packages/runtime-core/src/optional/eventEmitter.ts b/packages/runtime-core/src/optional/eventEmitter.ts deleted file mode 100644 index 2fba5fc3..00000000 --- a/packages/runtime-core/src/optional/eventEmitter.ts +++ /dev/null @@ -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) - } -} diff --git a/packages/runtime-core/src/optional/keepAlive.ts b/packages/runtime-core/src/optional/keepAlive.ts deleted file mode 100644 index 83595883..00000000 --- a/packages/runtime-core/src/optional/keepAlive.ts +++ /dev/null @@ -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 - -export const KeepAliveSymbol = Symbol() - -export class KeepAlive extends Component { - private cache: Cache - private keys: Set - - 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 -} diff --git a/packages/runtime-core/src/optional/memoize.ts b/packages/runtime-core/src/optional/memoize.ts deleted file mode 100644 index d1a77b63..00000000 --- a/packages/runtime-core/src/optional/memoize.ts +++ /dev/null @@ -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( - 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 - } -} diff --git a/packages/runtime-core/src/optional/mixins.ts b/packages/runtime-core/src/optional/mixins.ts deleted file mode 100644 index 86eb6cbb..00000000 --- a/packages/runtime-core/src/optional/mixins.ts +++ /dev/null @@ -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 { - new (): This -} - -interface ComponentConstructorWithMixins { - new

(): This & { $data: D } & D & { $props: Readonly

} & P -} - -// mind = blown -// https://stackoverflow.com/questions/50374908/transform-union-type-to-intersection-type -type UnionToIntersection = (U extends any - ? (k: U) => void - : never) extends ((k: infer I) => void) - ? I - : never - -type ExtractInstance = T extends (infer U)[] - ? UnionToIntersection ? V : never> - : never - -export function mixins< - T extends ComponentConstructor[] = [], - V = ExtractInstance ->(...args: T): ComponentConstructorWithMixins -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() - } -} - -*/ diff --git a/packages/runtime-core/src/patchFlags.ts b/packages/runtime-core/src/patchFlags.ts new file mode 100644 index 00000000..5d6a0b7f --- /dev/null +++ b/packages/runtime-core/src/patchFlags.ts @@ -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 diff --git a/packages/runtime-core/src/vdom.ts b/packages/runtime-core/src/vdom.ts deleted file mode 100644 index f0fd2282..00000000 --- a/packages/runtime-core/src/vdom.ts +++ /dev/null @@ -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 | void { - if (isArray(value)) { - const res: Record = {} - 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 + '')] - } -} diff --git a/packages/runtime-core/src/warning.ts b/packages/runtime-core/src/warning.ts deleted file mode 100644 index 4bc22a06..00000000 --- a/packages/runtime-core/src/warning.ts +++ /dev/null @@ -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 -}