refactor: option merging + extract helper functions

This commit is contained in:
Evan You 2018-10-16 15:47:51 -04:00
parent 7bc28a6e61
commit 149d82d618
32 changed files with 412 additions and 295 deletions

View File

@ -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)

View File

@ -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<P, D> {
data?(): Partial<D>
export interface APIMethods<P = {}, D = {}> {
data(): Partial<D>
render(props: Readonly<P>, 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<P = {}, D = {}>
extends InternalComponent,
APIMethods<P, D>,
LifecycleMethods {
Partial<APIMethods<P, D>>,
Partial<LifecycleMethods> {
constructor: ComponentClass
render: APIMethods<P, D>['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)
}

View File

@ -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)
}
}

View File

@ -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<string, any>
@ -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
}

View File

@ -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<string, NormalizedProp>
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<string, NormalizedProp>
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<any>,
expectedTypes: PropType<any> | 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<any>): 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
}

View File

@ -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
}

View File

@ -1,4 +1,3 @@
// import { EMPTY_OBJ } from './utils'
import { ComponentInstance } from './component'
import { observable } from '@vue/observer'

View File

@ -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
}

View File

@ -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<any> = 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)
}

View File

@ -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
}

View File

@ -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

View File

@ -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'

View File

@ -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<ComponentType>
@ -21,7 +22,7 @@ type AsyncComponentOptions = AsyncComponentFactory | AsyncComponentFullOptions
export function createAsyncComponent(
options: AsyncComponentOptions
): ComponentClass {
if (typeof options === 'function') {
if (isFunction(options)) {
options = { factory: options }
}

View File

@ -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

View File

@ -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)

View File

@ -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<This = Component> {
new (): This
@ -25,7 +33,19 @@ export function mixins<
V = ExtractInstance<T>
>(...args: T): ComponentConstructorWithMixins<V>
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

View File

@ -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<string, string | number> | void {
if (Array.isArray(value)) {
const res: Record<string, string | number> = {}
for (let i = 0; i < value.length; i++) {
const normalized = normalizeStyle(value[i])
if (normalized) {
for (const key in normalized) {
res[key] = normalized[key]
}
}
}
return res
} else if (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
}

View File

@ -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<string, string | number> | void {
if (isArray(value)) {
const res: Record<string, string | number> = {}
for (let i = 0; i < value.length; i++) {
const normalized = normalizeStyle(value[i])
if (normalized) {
for (const key in normalized) {
res[key] = normalized[key]
}
}
}
return res
} else if (isObject(value)) {
return value
}
}
function normalizeClass(value: any): string {
let res = ''
if (isString(value)) {
res = value
} else if (isArray(value)) {
for (let i = 0; i < value.length; i++) {
res += normalizeClass(value[i]) + ' '
}
} else if (isObject(value)) {
for (const name in value) {
if (value[name]) {
res += name + ' '
}
}
}
return res.trim()
}
export function createComponentVNode(
comp: any,
data: VNodeData | null,
@ -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]

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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<any> = new Set([Set, Map, WeakMap, WeakSet])
const observableValueRE = /^\[object (?:Object|Array|Map|Set|WeakMap|WeakSet)\]$/
@ -83,7 +83,7 @@ function createObservable(
baseHandlers: ProxyHandler<any>,
collectionHandlers: ProxyHandler<any>
) {
if (target === null || typeof target !== 'object') {
if (!isObject(target)) {
if (__DEV__) {
console.warn(`value is not observable: ${String(target)}`)
}

View File

@ -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

View File

@ -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] = ''

View File

@ -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'

View File

@ -0,0 +1 @@
# @vue/shared

View File

@ -0,0 +1,4 @@
{
"name": "@vue/shared",
"private": true
}

View File

@ -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<any, any> =>
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)
}

View File

@ -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

View File

@ -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,

View File

@ -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 = []

View File

@ -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"],