diff --git a/packages/core/__tests__/parentChain.spec.ts b/packages/core/__tests__/parentChain.spec.ts index 93dd7528..c34eaf7b 100644 --- a/packages/core/__tests__/parentChain.spec.ts +++ b/packages/core/__tests__/parentChain.spec.ts @@ -3,7 +3,6 @@ import { Component, render, nodeOps, - ComponentInstance, observable, nextTick } from '@vue/renderer-test' @@ -41,7 +40,7 @@ describe('Parent chain management', () => { } const root = nodeOps.createElement('div') - const parent = render(h(Parent), root) as ComponentInstance + const parent = render(h(Parent), root) as Component expect(child.$parent).toBe(parent) expect(child.$root).toBe(parent) @@ -100,7 +99,7 @@ describe('Parent chain management', () => { } const root = nodeOps.createElement('div') - const parent = render(h(Parent), root) as ComponentInstance + const parent = render(h(Parent), root) as Component expect(child.$parent).toBe(parent) expect(child.$root).toBe(parent) diff --git a/packages/core/src/component.ts b/packages/core/src/component.ts index ee62fd22..34631eec 100644 --- a/packages/core/src/component.ts +++ b/packages/core/src/component.ts @@ -1,4 +1,4 @@ -import { EMPTY_OBJ, NOOP } from './utils' +import { EMPTY_OBJ, NOOP, isArray } from '@vue/shared' import { VNode, Slots, RenderNode, MountedVNode } from './vdom' import { Data, @@ -44,30 +44,30 @@ interface PublicInstanceMethods { $emit(name: string, ...payload: any[]): this } -interface APIMethods { - data?(): Partial +export interface APIMethods

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

, slots: Slots, attrs: Data, parentVNode: VNode): any } -interface LifecycleMethods { - beforeCreate?(): void - created?(): void - beforeMount?(): void - mounted?(): void - beforeUpdate?(vnode: VNode): void - updated?(vnode: VNode): void - beforeUnmount?(): void - unmounted?(): void - errorCaptured?(): ( +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 + activated(): void + deactivated(): void + renderTracked(e: DebuggerEvent): void + renderTriggered(e: DebuggerEvent): void } export interface ComponentClass extends ComponentClassOptions { @@ -88,9 +88,10 @@ export type ComponentType = ComponentClass | FunctionalComponent // It extends InternalComponent with mounted instance properties. export interface ComponentInstance

extends InternalComponent, - APIMethods, - LifecycleMethods { + Partial>, + Partial { constructor: ComponentClass + render: APIMethods['render'] $vnode: MountedVNode $data: D @@ -157,7 +158,7 @@ class InternalComponent implements PublicInstanceMethods { // eventEmitter interface $on(event: string, fn: Function): this { - if (Array.isArray(event)) { + if (isArray(event)) { for (let i = 0; i < event.length; i++) { this.$on(event[i], fn) } @@ -181,7 +182,7 @@ class InternalComponent implements PublicInstanceMethods { if (this._events) { if (!event && !fn) { this._events = null - } else if (Array.isArray(event)) { + } else if (isArray(event)) { for (let i = 0; i < event.length; i++) { this.$off(event[i], fn) } @@ -223,7 +224,7 @@ class InternalComponent implements PublicInstanceMethods { function invokeListeners(value: Function | Function[], payload: any[]) { // TODO handle error - if (Array.isArray(value)) { + if (isArray(value)) { for (let i = 0; i < value.length; i++) { value[i](...payload) } diff --git a/packages/core/src/componentComputed.ts b/packages/core/src/componentComputed.ts index 4f3e99c2..7e914a1c 100644 --- a/packages/core/src/componentComputed.ts +++ b/packages/core/src/componentComputed.ts @@ -1,4 +1,4 @@ -import { NOOP } from './utils' +import { NOOP, isFunction } from '@vue/shared' import { computed, stop, ComputedGetter } from '@vue/observer' import { ComponentInstance } from './component' import { ComponentComputedOptions } from './componentOptions' @@ -17,7 +17,7 @@ export function initializeComputed( const proxy = instance.$proxy for (const key in computedOptions) { const option = computedOptions[key] - const getter = typeof option === 'function' ? option : option.get || NOOP + const getter = isFunction(option) ? option : option.get || NOOP handles[key] = computed(getter, proxy) } } diff --git a/packages/core/src/componentOptions.ts b/packages/core/src/componentOptions.ts index bab81745..2b033940 100644 --- a/packages/core/src/componentOptions.ts +++ b/packages/core/src/componentOptions.ts @@ -1,5 +1,12 @@ -import { ComponentInstance } from './component' +import { + ComponentInstance, + ComponentClass, + APIMethods, + LifecycleMethods +} from './component' import { Slots } from './vdom' +import { isArray, isObject, isFunction } from '@vue/shared' +import { normalizePropsOptions } from './componentProps' export type Data = Record @@ -73,3 +80,100 @@ export interface WatchOptions { 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 +} + +// This is called in the base component constructor and the return value is +// set on the instance as $options. +export function resolveComponentOptionsFromClass( + Component: ComponentClass +): ComponentOptions { + if (Component.options) { + return Component.options + } + const staticDescriptors = Object.getOwnPropertyDescriptors(Component) + const options = {} as any + for (const key in staticDescriptors) { + const { enumerable, get, value } = staticDescriptors[key] + if (enumerable || get) { + options[key] = get ? get() : value + } + } + const instanceDescriptors = Object.getOwnPropertyDescriptors( + Component.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)) { + if (key in reservedMethods) { + options[key] = value + } else { + ;(options.methods || (options.methods = {}))[key] = value + } + } + } + options.props = normalizePropsOptions(options.props) + Component.options = options + return options +} + +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] = function() { + return Object.assign(existing(), value()) + } + } else if (/^render|^errorCaptured/.test(key)) { + // render, renderTracked, renderTriggered & errorCaptured + // are never merged + res[key] = value + } else { + // merge lifecycle hooks + res[key] = function(...args: any[]) { + existing.call(this, ...args) + value.call(this, ...args) + } + } + } 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 +} diff --git a/packages/core/src/componentProps.ts b/packages/core/src/componentProps.ts index 9dae489c..da1fa825 100644 --- a/packages/core/src/componentProps.ts +++ b/packages/core/src/componentProps.ts @@ -2,19 +2,40 @@ import { immutable, unwrap, lock, unlock } from '@vue/observer' import { ComponentInstance } from './component' import { Data, - ComponentPropsOptions, PropOptions, Prop, - PropType + PropType, + ComponentPropsOptions } from './componentOptions' -import { EMPTY_OBJ, camelize, hyphenate, capitalize } from './utils' +import { + EMPTY_OBJ, + camelize, + hyphenate, + capitalize, + isString, + isFunction, + isArray, + isObject +} from '@vue/shared' import { warn } from './warning' const EMPTY_PROPS = { props: EMPTY_OBJ } +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: ComponentPropsOptions | undefined, + options: NormalizedPropsOptions | undefined, data: Data | null ) { const { props, attrs } = resolveProps(data, options) @@ -31,13 +52,13 @@ export function initializeProps( // - else: everything goes in `props`. export function resolveProps( rawData: any, - rawOptions: ComponentPropsOptions | void + _options: NormalizedPropsOptions | void ): { props: Data; attrs?: Data } { - const hasDeclaredProps = rawOptions !== void 0 + const hasDeclaredProps = _options !== void 0 + const options = _options as NormalizedPropsOptions if (!rawData && !hasDeclaredProps) { return EMPTY_PROPS } - const options = normalizePropsOptions(rawOptions) as NormalizedPropsOptions const props: any = {} let attrs: any = void 0 if (rawData != null) { @@ -66,8 +87,7 @@ export function resolveProps( // default values if (hasDefault && currentValue === void 0) { const defaultValue = opt.default - props[key] = - typeof defaultValue === 'function' ? defaultValue() : defaultValue + props[key] = isFunction(defaultValue) ? defaultValue() : defaultValue } // boolean casting if (opt[BooleanFlags.shouldCast]) { @@ -129,58 +149,34 @@ export function updateProps(instance: ComponentInstance, nextData: Data) { } } -const enum BooleanFlags { - shouldCast = '1', - shouldCastTrue = '2' -} - -type NormalizedProp = PropOptions & { - [BooleanFlags.shouldCast]?: boolean - [BooleanFlags.shouldCastTrue]?: boolean -} - -type NormalizedPropsOptions = Record - -const normalizationCache = new WeakMap< - ComponentPropsOptions, - NormalizedPropsOptions ->() - -function normalizePropsOptions( +export function normalizePropsOptions( raw: ComponentPropsOptions | void -): NormalizedPropsOptions { +): NormalizedPropsOptions | void { if (!raw) { - return EMPTY_OBJ - } - const hit = normalizationCache.get(raw) - if (hit) { - return hit + return } const normalized: NormalizedPropsOptions = {} - if (Array.isArray(raw)) { + if (isArray(raw)) { for (let i = 0; i < raw.length; i++) { - if (__DEV__ && typeof raw !== 'string') { - warn(`props must be strings when using array syntax.`) + if (__DEV__ && !isString(raw[i])) { + warn(`props must be strings when using array syntax.`, raw[i]) } normalized[camelize(raw[i])] = EMPTY_OBJ } } else { - if (__DEV__ && typeof raw !== 'object') { + if (__DEV__ && !isObject(raw)) { warn(`invalid props options`, raw) } for (const key in raw) { const opt = raw[key] const prop = (normalized[camelize(key)] = - Array.isArray(opt) || typeof opt === 'function' - ? { type: opt } - : opt) as NormalizedProp + isArray(opt) || isFunction(opt) ? { type: opt } : opt) as NormalizedProp const booleanIndex = getTypeIndex(Boolean, prop.type) const stringIndex = getTypeIndex(String, prop.type) prop[BooleanFlags.shouldCast] = booleanIndex > -1 prop[BooleanFlags.shouldCastTrue] = booleanIndex < stringIndex } } - normalizationCache.set(raw, normalized) return normalized } @@ -199,13 +195,13 @@ function getTypeIndex( type: Prop, expectedTypes: PropType | void | null | true ): number { - if (Array.isArray(expectedTypes)) { + if (isArray(expectedTypes)) { for (let i = 0, len = expectedTypes.length; i < len; i++) { if (isSameType(expectedTypes[i], type)) { return i } } - } else if (expectedTypes != null && typeof expectedTypes === 'object') { + } else if (isObject(expectedTypes)) { return isSameType(expectedTypes, type) ? 0 : -1 } return -1 @@ -235,7 +231,7 @@ function validateProp( // type check if (type != null && type !== true) { let isValid = false - const types = Array.isArray(type) ? type : [type] + 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++) { @@ -269,7 +265,7 @@ function assertType(value: any, type: Prop): AssertionResult { } else if (expectedType === 'Object') { valid = toRawType(value) === 'Object' } else if (expectedType === 'Array') { - valid = Array.isArray(value) + valid = isArray(value) } else { valid = value instanceof type } diff --git a/packages/core/src/componentProxy.ts b/packages/core/src/componentProxy.ts index 33dc6ff5..ba9945a1 100644 --- a/packages/core/src/componentProxy.ts +++ b/packages/core/src/componentProxy.ts @@ -1,4 +1,5 @@ import { ComponentInstance } from './component' +import { isString, isFunction } from '@vue/shared' const bindCache = new WeakMap() @@ -41,7 +42,7 @@ const renderProxyHandlers = { // TODO warn non-present property } const value = Reflect.get(target, key, receiver) - if (typeof value === 'function') { + if (isFunction(value)) { // auto bind return getBoundMethod(value, target, receiver) } else { @@ -56,7 +57,7 @@ const renderProxyHandlers = { receiver: any ): boolean { if (__DEV__) { - if (typeof key === 'string' && key[0] === '$') { + if (isString(key) && key[0] === '$') { // TODO warn setting immutable properties return false } diff --git a/packages/core/src/componentState.ts b/packages/core/src/componentState.ts index 931794f4..5c1477ce 100644 --- a/packages/core/src/componentState.ts +++ b/packages/core/src/componentState.ts @@ -1,4 +1,3 @@ -// import { EMPTY_OBJ } from './utils' import { ComponentInstance } from './component' import { observable } from '@vue/observer' diff --git a/packages/core/src/componentUtils.ts b/packages/core/src/componentUtils.ts index b277c822..3eb69f5a 100644 --- a/packages/core/src/componentUtils.ts +++ b/packages/core/src/componentUtils.ts @@ -1,5 +1,5 @@ import { VNodeFlags } from './flags' -import { EMPTY_OBJ } from './utils' +import { EMPTY_OBJ, isArray, isFunction, isObject } from '@vue/shared' import { h } from './h' import { VNode, MountedVNode, createFragment } from './vdom' import { @@ -13,7 +13,10 @@ import { initializeState } from './componentState' import { initializeProps, resolveProps } from './componentProps' import { initializeComputed, teardownComputed } from './componentComputed' import { initializeWatch, teardownWatch } from './componentWatch' -import { ComponentOptions } from './componentOptions' +import { + ComponentOptions, + resolveComponentOptionsFromClass +} from './componentOptions' import { createRenderProxy } from './componentProxy' import { handleError, ErrorTypes } from './errorHandling' import { warn } from './warning' @@ -58,7 +61,7 @@ export function initializeComponentInstance(instance: ComponentInstance) { ) } - instance.$options = resolveComponentOptions(instance.constructor) + instance.$options = resolveComponentOptionsFromClass(instance.constructor) instance.$parentVNode = currentVNode as MountedVNode // renderProxy @@ -143,9 +146,9 @@ function normalizeComponentRoot( ): VNode { if (vnode == null) { vnode = createTextVNode('') - } else if (typeof vnode !== 'object') { + } else if (!isObject(vnode)) { vnode = createTextVNode(vnode + '') - } else if (Array.isArray(vnode)) { + } else if (isArray(vnode)) { if (vnode.length === 1) { vnode = normalizeComponentRoot(vnode[0], componentVNode) } else { @@ -158,13 +161,13 @@ function normalizeComponentRoot( (flags & VNodeFlags.COMPONENT || flags & VNodeFlags.ELEMENT) ) { if (el) { - vnode = cloneVNode(vnode) + vnode = cloneVNode(vnode as VNode) } if (flags & VNodeFlags.COMPONENT) { vnode.parentVNode = componentVNode } } else if (el) { - vnode = cloneVNode(vnode) + vnode = cloneVNode(vnode as VNode) } } return vnode @@ -209,7 +212,7 @@ export function createComponentClassFromOptions( // name -> displayName if (key === 'name') { options.displayName = options.name - } else if (typeof value === 'function') { + } else if (isFunction(value)) { // lifecycle hook / data / render if (__COMPAT__) { if (key === 'render') { @@ -229,7 +232,7 @@ export function createComponentClassFromOptions( } else if (key === 'computed') { for (const computedKey in value) { const computed = value[computedKey] - const isGet = typeof computed === 'function' + const isGet = isFunction(computed) Object.defineProperty(proto, computedKey, { configurable: true, get: isGet ? computed : computed.get, @@ -250,37 +253,3 @@ export function createComponentClassFromOptions( } return AnonymousComponent as ComponentClass } - -// This is called in the base component constructor and the return value is -// set on the instance as $options. -export function resolveComponentOptions( - Component: ComponentClass -): ComponentOptions { - if (Component.options) { - return Component.options - } - const staticDescriptors = Object.getOwnPropertyDescriptors(Component) - const options = {} as any - for (const key in staticDescriptors) { - const { enumerable, get, value } = staticDescriptors[key] - if (enumerable || get) { - options[key] = get ? get() : value - } - } - const instanceDescriptors = Object.getOwnPropertyDescriptors( - Component.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 (typeof value === 'function') { - ;(options.methods || (options.methods = {}))[key] = value - } - } - Component.options = options - return options -} diff --git a/packages/core/src/componentWatch.ts b/packages/core/src/componentWatch.ts index 79aa47e7..33ceb025 100644 --- a/packages/core/src/componentWatch.ts +++ b/packages/core/src/componentWatch.ts @@ -1,4 +1,11 @@ -import { EMPTY_OBJ, NOOP } from './utils' +import { + EMPTY_OBJ, + NOOP, + isFunction, + isArray, + isString, + isObject +} from '@vue/shared' import { ComponentInstance } from './component' import { ComponentWatchOptions, WatchOptions } from './componentOptions' import { autorun, stop } from '@vue/observer' @@ -13,11 +20,11 @@ export function initializeWatch( if (options !== void 0) { for (const key in options) { const opt = options[key] - if (Array.isArray(opt)) { + if (isArray(opt)) { opt.forEach(o => setupWatcher(instance, key, o)) - } else if (typeof opt === 'function') { + } else if (isFunction(opt)) { setupWatcher(instance, key, opt) - } else if (typeof opt === 'string') { + } else if (isString(opt)) { setupWatcher(instance, key, (instance as any)[opt]) } else if (opt.handler) { setupWatcher(instance, key, opt.handler, opt) @@ -35,10 +42,9 @@ export function setupWatcher( const handles = instance._watchHandles || (instance._watchHandles = new Set()) const proxy = instance.$proxy - const rawGetter = - typeof keyOrFn === 'string' - ? parseDotPath(keyOrFn, proxy) - : () => keyOrFn.call(proxy) + const rawGetter = isString(keyOrFn) + ? parseDotPath(keyOrFn, proxy) + : () => keyOrFn.call(proxy) if (__DEV__ && rawGetter === NOOP) { warn( @@ -116,11 +122,11 @@ function parseDotPath(path: string, ctx: any): Function { } function traverse(value: any, seen: Set = new Set()) { - if (value === null || typeof value !== 'object' || seen.has(value)) { + if (!isObject(value) || seen.has(value)) { return } seen.add(value) - if (Array.isArray(value)) { + if (isArray(value)) { for (let i = 0; i < value.length; i++) { traverse(value[i], seen) } diff --git a/packages/core/src/createRenderer.ts b/packages/core/src/createRenderer.ts index 0817cf68..8ffffca3 100644 --- a/packages/core/src/createRenderer.ts +++ b/packages/core/src/createRenderer.ts @@ -1,7 +1,7 @@ import { autorun, stop } from '@vue/observer' import { queueJob } from '@vue/scheduler' import { VNodeFlags, ChildrenFlags } from './flags' -import { EMPTY_OBJ, reservedPropRE, lis } from './utils' +import { EMPTY_OBJ, reservedPropRE, isString } from '@vue/shared' import { VNode, MountedVNode, @@ -297,7 +297,7 @@ export function createRenderer(options: RendererOptions) { contextVNode: MountedVNode | null ) { const { tag, children, childFlags, ref } = vnode - const target = typeof tag === 'string' ? platformQuerySelector(tag) : tag + const target = isString(tag) ? platformQuerySelector(tag) : tag if (__DEV__ && !target) { // TODO warn poartal target not found @@ -1410,3 +1410,49 @@ export function createRenderer(options: RendererOptions) { return { render } } + +// https://en.wikipedia.org/wiki/Longest_increasing_subsequence +export 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 + } + } + } + u = result.length + v = result[u - 1] + while (u-- > 0) { + result[u] = v + v = p[v] + } + return result +} diff --git a/packages/core/src/h.ts b/packages/core/src/h.ts index 9d4bcb97..f7323f31 100644 --- a/packages/core/src/h.ts +++ b/packages/core/src/h.ts @@ -14,6 +14,7 @@ import { } from './vdom' import { isObservable } from '@vue/observer' import { warn } from './warning' +import { isString, isArray, isFunction, isObject } from '@vue/shared' export const Fragment = Symbol() export const Portal = Symbol() @@ -98,10 +99,7 @@ interface createElement extends VNodeFactories { } export const h = ((tag: ElementType, data?: any, children?: any): VNode => { - if ( - Array.isArray(data) || - (data != null && (typeof data !== 'object' || data._isVNode)) - ) { + if (isArray(data) || !isObject(data) || data._isVNode) { children = data data = null } @@ -133,7 +131,7 @@ export const h = ((tag: ElementType, data?: any, children?: any): VNode => { } } - if (typeof tag === 'string') { + if (isString(tag)) { // element return createElementVNode( tag, @@ -160,10 +158,7 @@ export const h = ((tag: ElementType, data?: any, children?: any): VNode => { ref ) } else { - if ( - __DEV__ && - (!tag || (typeof tag !== 'function' && typeof tag !== 'object')) - ) { + if (__DEV__ && !isFunction(tag) && !isObject(tag)) { warn('Invalid component passed to h(): ', tag) } // component diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 6c2d323c..14b544de 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -10,21 +10,26 @@ export * from '@vue/observer' // Scheduler API export { nextTick } from '@vue/scheduler' -// Internal API -export { - createComponentInstance, - createComponentClassFromOptions -} from './componentUtils' - // Optional APIs // these are imported on-demand and can be tree-shaken export { applyDirectives } from './optional/directive' export { Provide, Inject } from './optional/context' export { createAsyncComponent } from './optional/asyncComponent' export { KeepAlive } from './optional/keepAlive' +export { mixins } from './optional/mixin' // flags & types export { ComponentType, ComponentClass, FunctionalComponent } from './component' -export * from './componentOptions' export { VNodeFlags, ChildrenFlags } from './flags' export { VNode, Slots } from './vdom' + +// Internal API, for libraries or renderers that need to perform low level work +export { + reservedMethods, + resolveComponentOptionsFromClass, + mergeComponentOptions +} from './componentOptions' +export { + createComponentInstance, + createComponentClassFromOptions +} from './componentUtils' diff --git a/packages/core/src/optional/asyncComponent.ts b/packages/core/src/optional/asyncComponent.ts index e0a6d0d4..61920571 100644 --- a/packages/core/src/optional/asyncComponent.ts +++ b/packages/core/src/optional/asyncComponent.ts @@ -2,6 +2,7 @@ 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 @@ -21,7 +22,7 @@ type AsyncComponentOptions = AsyncComponentFactory | AsyncComponentFullOptions export function createAsyncComponent( options: AsyncComponentOptions ): ComponentClass { - if (typeof options === 'function') { + if (isFunction(options)) { options = { factory: options } } diff --git a/packages/core/src/optional/directive.ts b/packages/core/src/optional/directive.ts index 7ee84bb7..ba09cee6 100644 --- a/packages/core/src/optional/directive.ts +++ b/packages/core/src/optional/directive.ts @@ -15,7 +15,7 @@ return applyDirectives( import { VNode, cloneVNode, VNodeData } from '../vdom' import { ComponentInstance } from '../component' -import { EMPTY_OBJ } from '../utils' +import { EMPTY_OBJ } from '@vue/shared' interface DirectiveBinding { instance: ComponentInstance diff --git a/packages/core/src/optional/keepAlive.ts b/packages/core/src/optional/keepAlive.ts index 664b1777..fa973928 100644 --- a/packages/core/src/optional/keepAlive.ts +++ b/packages/core/src/optional/keepAlive.ts @@ -2,6 +2,7 @@ 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[] @@ -119,9 +120,9 @@ function getName(comp: ComponentClass): string | void { } function matches(pattern: MatchPattern, name: string): boolean { - if (Array.isArray(pattern)) { + if (isArray(pattern)) { return (pattern as any).some((p: string | RegExp) => matches(p, name)) - } else if (typeof pattern === 'string') { + } else if (isString(pattern)) { return pattern.split(',').indexOf(name) > -1 } else if (pattern.test) { return pattern.test(name) diff --git a/packages/core/src/optional/mixin.ts b/packages/core/src/optional/mixin.ts index 7b56416a..f6de2861 100644 --- a/packages/core/src/optional/mixin.ts +++ b/packages/core/src/optional/mixin.ts @@ -1,4 +1,12 @@ import { Component } from '../component' +import { createComponentClassFromOptions } from '../componentUtils' +import { + ComponentOptions, + resolveComponentOptionsFromClass, + mergeComponentOptions +} from '../componentOptions' +import { normalizePropsOptions } from '../componentProps' +import { isFunction } from '@vue/shared' interface ComponentConstructor { new (): This @@ -25,7 +33,19 @@ export function mixins< V = ExtractInstance >(...args: T): ComponentConstructorWithMixins export function mixins(...args: any[]): any { - // TODO + let options: ComponentOptions = {} + args.forEach(mixin => { + if (isFunction(mixin)) { + options = mergeComponentOptions( + options, + resolveComponentOptionsFromClass(mixin) + ) + } else { + mixin.props = normalizePropsOptions(mixin.props) + options = mergeComponentOptions(options, mixin) + } + }) + return createComponentClassFromOptions(options) } /* Example usage diff --git a/packages/core/src/utils.ts b/packages/core/src/utils.ts deleted file mode 100644 index 701aa69c..00000000 --- a/packages/core/src/utils.ts +++ /dev/null @@ -1,105 +0,0 @@ -export const EMPTY_OBJ: { readonly [key: string]: any } = Object.freeze({}) - -export const NOOP = () => {} - -export const onRE = /^on/ -export const vnodeHookRE = /^vnode/ -export const handlersRE = /^on|^vnode/ -export const reservedPropRE = /^(?:key|ref|slots)$|^vnode/ - -export function normalizeStyle( - value: any -): Record | void { - if (Array.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 (value && typeof value === 'object') { - return value - } -} - -export function normalizeClass(value: any): string { - let res = '' - if (typeof value === 'string') { - res = value - } else if (Array.isArray(value)) { - for (let i = 0; i < value.length; i++) { - res += normalizeClass(value[i]) + ' ' - } - } else if (typeof value === 'object') { - for (const name in value) { - if (value[name]) { - res += name + ' ' - } - } - } - return res.trim() -} - -const camelizeRE = /-(\w)/g -export const camelize = (str: string): string => { - return str.replace(camelizeRE, (_, c) => (c ? c.toUpperCase() : '')) -} - -const hyphenateRE = /\B([A-Z])/g -export const hyphenate = (str: string): string => { - return str.replace(hyphenateRE, '-$1').toLowerCase() -} - -export const capitalize = (str: string): string => { - return str.charAt(0).toUpperCase() + str.slice(1) -} - -// https://en.wikipedia.org/wiki/Longest_increasing_subsequence -export 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 - } - } - } - u = result.length - v = result[u - 1] - while (u-- > 0) { - result[u] = v - v = p[v] - } - return result -} diff --git a/packages/core/src/vdom.ts b/packages/core/src/vdom.ts index 3db116df..dededddd 100644 --- a/packages/core/src/vdom.ts +++ b/packages/core/src/vdom.ts @@ -5,7 +5,14 @@ import { } from './component' import { VNodeFlags, ChildrenFlags } from './flags' import { createComponentClassFromOptions } from './componentUtils' -import { normalizeClass, normalizeStyle, handlersRE, EMPTY_OBJ } from './utils' +import { + handlersRE, + EMPTY_OBJ, + isObject, + isArray, + isFunction, + isString +} from '@vue/shared' import { RawChildrenType, RawSlots } from './h' // Vue core is platform agnostic, so we are not using Element for "DOM" nodes. @@ -98,15 +105,6 @@ export function createVNode( return vnode } -function normalizeClassAndStyle(data: VNodeData) { - if (data.class != null) { - data.class = normalizeClass(data.class) - } - if (data.style != null) { - data.style = normalizeStyle(data.style) - } -} - export function createElementVNode( tag: string, data: VNodeData | null, @@ -122,6 +120,50 @@ export function createElementVNode( 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, @@ -134,8 +176,7 @@ export function createComponentVNode( let flags: VNodeFlags // flags - const compType = typeof comp - if (compType === 'object') { + if (isObject(comp)) { if (comp.functional) { // object literal functional flags = VNodeFlags.COMPONENT_FUNCTIONAL @@ -155,7 +196,7 @@ export function createComponentVNode( } } else { // assumes comp is function here now - if (__DEV__ && compType !== 'function') { + if (__DEV__ && !isFunction(comp)) { // TODO warn invalid comp value in dev } if (comp.prototype && comp.prototype.render) { @@ -178,14 +219,13 @@ export function createComponentVNode( ? ChildrenFlags.DYNAMIC_SLOTS : ChildrenFlags.NO_CHILDREN if (children != null) { - const childrenType = typeof children - if (childrenType === 'function') { + if (isFunction(children)) { // function as children slots = { default: children } - } else if (Array.isArray(children) || (children as any)._isVNode) { + } else if (isArray(children) || (children as any)._isVNode) { // direct vnode children slots = { default: () => children } - } else if (typeof children === 'object') { + } else if (isObject(children)) { // slot object as children slots = children } @@ -313,7 +353,7 @@ export function cloneVNode(vnode: VNode, extraData?: VNodeData): VNode { function normalizeChildren(vnode: VNode, children: any) { let childFlags - if (Array.isArray(children)) { + if (isArray(children)) { const { length } = children if (length === 0) { childFlags = ChildrenFlags.NO_CHILDREN @@ -356,7 +396,7 @@ export function normalizeVNodes( newChild = createTextVNode('') } else if (child._isVNode) { newChild = child.el ? cloneVNode(child) : child - } else if (Array.isArray(child)) { + } else if (isArray(child)) { normalizeVNodes(child, newChildren, currentPrefix + i + '|') } else { newChild = createTextVNode(child + '') @@ -386,7 +426,7 @@ function normalizeSlots(slots: { [name: string]: any }): Slots { function normalizeSlot(value: any): VNode[] { if (value == null) { return [createTextVNode('')] - } else if (Array.isArray(value)) { + } else if (isArray(value)) { return normalizeVNodes(value) } else if (value._isVNode) { return [value] diff --git a/packages/core/src/warning.ts b/packages/core/src/warning.ts index e7d1a814..7c5e73a3 100644 --- a/packages/core/src/warning.ts +++ b/packages/core/src/warning.ts @@ -1,5 +1,5 @@ import { ComponentType, ComponentClass, FunctionalComponent } from './component' -import { EMPTY_OBJ } from './utils' +import { EMPTY_OBJ, isString } from '@vue/shared' import { VNode } from './vdom' import { Data } from './componentOptions' @@ -119,7 +119,7 @@ function formatProps(props: Data) { const res = [] for (const key in props) { const value = props[key] - if (typeof value === 'string') { + if (isString(value)) { res.push(`${key}=${JSON.stringify(value)}`) } else { res.push(`${key}=`, value) diff --git a/packages/observer/src/baseHandlers.ts b/packages/observer/src/baseHandlers.ts index beb47e8e..0be06e3a 100644 --- a/packages/observer/src/baseHandlers.ts +++ b/packages/observer/src/baseHandlers.ts @@ -2,6 +2,7 @@ import { observable, immutable, unwrap } from './index' import { OperationTypes } from './operations' import { track, trigger } from './autorun' import { LOCKED } from './lock' +import { isObject } from '@vue/shared' const hasOwnProperty = Object.prototype.hasOwnProperty @@ -18,7 +19,7 @@ function createGetter(isImmutable: boolean) { return res } track(target, OperationTypes.GET, key) - return res !== null && typeof res === 'object' + return isObject(res) ? isImmutable ? // need to lazy access immutable and observable here to avoid // circular dependency diff --git a/packages/observer/src/collectionHandlers.ts b/packages/observer/src/collectionHandlers.ts index 4f0531d7..8fd40a94 100644 --- a/packages/observer/src/collectionHandlers.ts +++ b/packages/observer/src/collectionHandlers.ts @@ -2,8 +2,8 @@ import { unwrap, observable, immutable } from './index' import { track, trigger } from './autorun' import { OperationTypes } from './operations' import { LOCKED } from './lock' +import { isObject } from '@vue/shared' -const isObject = (value: any) => value !== null && typeof value === 'object' const toObservable = (value: any) => isObject(value) ? observable(value) : value const toImmutable = (value: any) => (isObject(value) ? immutable(value) : value) diff --git a/packages/observer/src/index.ts b/packages/observer/src/index.ts index 7f8a9f5a..836731ed 100644 --- a/packages/observer/src/index.ts +++ b/packages/observer/src/index.ts @@ -1,3 +1,4 @@ +import { isObject, EMPTY_OBJ } from '@vue/shared' import { mutableHandlers, immutableHandlers } from './baseHandlers' import { @@ -28,7 +29,6 @@ export { OperationTypes } from './operations' export { computed, ComputedGetter } from './computed' export { lock, unlock } from './lock' -const EMPTY_OBJ = {} const collectionTypes: Set = new Set([Set, Map, WeakMap, WeakSet]) const observableValueRE = /^\[object (?:Object|Array|Map|Set|WeakMap|WeakSet)\]$/ @@ -83,7 +83,7 @@ function createObservable( baseHandlers: ProxyHandler, collectionHandlers: ProxyHandler ) { - if (target === null || typeof target !== 'object') { + if (!isObject(target)) { if (__DEV__) { console.warn(`value is not observable: ${String(target)}`) } diff --git a/packages/renderer-dom/src/index.ts b/packages/renderer-dom/src/index.ts index b9162429..4ddcd3c3 100644 --- a/packages/renderer-dom/src/index.ts +++ b/packages/renderer-dom/src/index.ts @@ -1,4 +1,4 @@ -import { createRenderer, VNode, ComponentInstance } from '@vue/core' +import { createRenderer, VNode, Component } from '@vue/core' import { nodeOps } from './nodeOps' import { patchData } from './patchData' import { teardownVNode } from './teardownVNode' @@ -12,7 +12,7 @@ const { render: _render } = createRenderer({ type publicRender = ( node: VNode | null, container: HTMLElement -) => ComponentInstance | null +) => Component | null export const render = _render as publicRender // re-export everything from core diff --git a/packages/renderer-dom/src/modules/style.ts b/packages/renderer-dom/src/modules/style.ts index f347064b..e5c3e0ee 100644 --- a/packages/renderer-dom/src/modules/style.ts +++ b/packages/renderer-dom/src/modules/style.ts @@ -1,3 +1,5 @@ +import { isString } from '@vue/shared' + // style properties that should NOT have "px" added when numeric const nonNumericRE = /acit|ex(?:s|g|n|p|$)|rph|ows|mnc|ntw|ine[ch]|zoo|^ord/i @@ -5,7 +7,7 @@ export function patchStyle(el: any, prev: any, next: any, data: any) { const { style } = el if (!next) { el.removeAttribute('style') - } else if (typeof next === 'string') { + } else if (isString(next)) { style.cssText = next } else { for (const key in next) { @@ -15,7 +17,7 @@ export function patchStyle(el: any, prev: any, next: any, data: any) { } style[key] = value } - if (prev && typeof prev !== 'string') { + if (prev && !isString(prev)) { for (const key in prev) { if (!next[key]) { style[key] = '' diff --git a/packages/renderer-test/src/index.ts b/packages/renderer-test/src/index.ts index ba406389..fda7490c 100644 --- a/packages/renderer-test/src/index.ts +++ b/packages/renderer-test/src/index.ts @@ -1,4 +1,4 @@ -import { createRenderer, VNode, ComponentInstance } from '@vue/core' +import { createRenderer, VNode, Component } from '@vue/core' import { nodeOps, TestElement } from './nodeOps' import { patchData } from './patchData' @@ -10,7 +10,7 @@ const { render: _render } = createRenderer({ type publicRender = ( node: VNode | null, container: TestElement -) => ComponentInstance | null +) => Component | null export const render = _render as publicRender export { serialize } from './serialize' diff --git a/packages/shared/README.md b/packages/shared/README.md new file mode 100644 index 00000000..6f4b137f --- /dev/null +++ b/packages/shared/README.md @@ -0,0 +1 @@ +# @vue/shared \ No newline at end of file diff --git a/packages/shared/package.json b/packages/shared/package.json new file mode 100644 index 00000000..53668013 --- /dev/null +++ b/packages/shared/package.json @@ -0,0 +1,4 @@ +{ + "name": "@vue/shared", + "private": true +} diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts new file mode 100644 index 00000000..798f0961 --- /dev/null +++ b/packages/shared/src/index.ts @@ -0,0 +1,29 @@ +export const EMPTY_OBJ: { readonly [key: string]: any } = Object.freeze({}) + +export const NOOP = () => {} + +export const onRE = /^on/ +export const vnodeHookRE = /^vnode/ +export const handlersRE = /^on|^vnode/ +export const reservedPropRE = /^(?:key|ref|slots)$|^vnode/ + +export const isArray = Array.isArray +export const isFunction = (val: any): val is Function => + typeof val === 'function' +export const isString = (val: any): val is string => typeof val === 'string' +export const isObject = (val: any): val is Record => + val !== null && typeof val === 'object' + +const camelizeRE = /-(\w)/g +export const camelize = (str: string): string => { + return str.replace(camelizeRE, (_, c) => (c ? c.toUpperCase() : '')) +} + +const hyphenateRE = /\B([A-Z])/g +export const hyphenate = (str: string): string => { + return str.replace(hyphenateRE, '-$1').toLowerCase() +} + +export const capitalize = (str: string): string => { + return str.charAt(0).toUpperCase() + str.slice(1) +} diff --git a/packages/vue/src/index.ts b/packages/vue/src/index.ts index 7bcae758..6ae92e1e 100644 --- a/packages/vue/src/index.ts +++ b/packages/vue/src/index.ts @@ -16,7 +16,7 @@ class Vue { // convert it to a class const Component = createComponentClassFromOptions(options || {}) const vnode = h(Component) - const instance = createComponentInstance(vnode, Component, null) + const instance = createComponentInstance(vnode, Component) function mount(el: any) { const dom = typeof el === 'string' ? document.querySelector(el) : el diff --git a/rollup.config.js b/rollup.config.js index a7dd1924..4a31cbb8 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -95,12 +95,13 @@ function createConfig(output, plugins = []) { // during a single build. hasTSChecked = true + const externals = Object.keys(aliasOptions).filter(p => p !== '@vue/shared') + return { input: resolve(`src/index.ts`), // Global and Browser ESM builds inlines everything so that they can be // used alone. - external: - isGlobalBuild || isBrowserESMBuild ? [] : Object.keys(aliasOptions), + external: isGlobalBuild || isBrowserESMBuild ? [] : externals, plugins: [ tsPlugin, aliasPlugin, diff --git a/scripts/utils.js b/scripts/utils.js index aa1ca3db..627ed740 100644 --- a/scripts/utils.js +++ b/scripts/utils.js @@ -1,8 +1,8 @@ const fs = require('fs') -const targets = exports.targets = fs.readdirSync('packages').filter(f => { - return fs.statSync(`packages/${f}`).isDirectory() -}) +const targets = (exports.targets = fs.readdirSync('packages').filter(f => { + return f !== 'shared' && fs.statSync(`packages/${f}`).isDirectory() +})) exports.fuzzyMatchTarget = partialTarget => { const matched = [] diff --git a/tsconfig.json b/tsconfig.json index 42ef9176..1815a49f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,6 +19,7 @@ ], "rootDir": ".", "paths": { + "@vue/shared": ["packages/shared/src"], "@vue/core": ["packages/core/src"], "@vue/observer": ["packages/observer/src"], "@vue/scheduler": ["packages/scheduler/src"],