init (graduate from prototype)
This commit is contained in:
171
packages/core/src/component.ts
Normal file
171
packages/core/src/component.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
import { EMPTY_OBJ } from './utils'
|
||||
import { VNode, Slots, RenderNode, RenderFragment } from './vdom'
|
||||
import {
|
||||
Data,
|
||||
RenderFunction,
|
||||
ComponentOptions,
|
||||
ComponentPropsOptions
|
||||
} from './componentOptions'
|
||||
import { setupWatcher } from './componentWatch'
|
||||
import { Autorun, DebuggerEvent, ComputedGetter } from '@vue/observer'
|
||||
|
||||
type Flatten<T> = { [K in keyof T]: T[K] }
|
||||
|
||||
export interface ComponentClass extends Flatten<typeof Component> {
|
||||
new <D = Data, P = Data>(): MountedComponent<D, P> & D & P
|
||||
}
|
||||
|
||||
export interface FunctionalComponent<P = Data> extends RenderFunction<P> {
|
||||
pure?: boolean
|
||||
props?: ComponentPropsOptions<P>
|
||||
}
|
||||
|
||||
// this interface is merged with the class type
|
||||
// to represent a mounted component
|
||||
export interface MountedComponent<D = Data, P = Data> extends Component {
|
||||
$vnode: VNode
|
||||
$data: D
|
||||
$props: P
|
||||
$computed: Data
|
||||
$slots: Slots
|
||||
$root: MountedComponent
|
||||
$children: MountedComponent[]
|
||||
$options: ComponentOptions<D, P>
|
||||
|
||||
render: RenderFunction<P>
|
||||
data?(): Partial<D>
|
||||
beforeCreate?(): void
|
||||
created?(): void
|
||||
beforeMount?(): void
|
||||
mounted?(): void
|
||||
beforeUpdate?(e: DebuggerEvent): void
|
||||
updated?(): void
|
||||
beforeDestroy?(): void
|
||||
destroyed?(): void
|
||||
|
||||
_updateHandle: Autorun
|
||||
$forceUpdate: () => void
|
||||
|
||||
_self: MountedComponent<D, P> // on proxies only
|
||||
}
|
||||
|
||||
export class Component {
|
||||
public static options?: ComponentOptions
|
||||
|
||||
public get $el(): RenderNode | RenderFragment | null {
|
||||
return this.$vnode && this.$vnode.el
|
||||
}
|
||||
|
||||
public $vnode: VNode | null = null
|
||||
public $parentVNode: VNode | null = null
|
||||
public $data: Data | null = null
|
||||
public $props: Data | null = null
|
||||
public $computed: Data | null = null
|
||||
public $slots: Slots | null = null
|
||||
public $root: MountedComponent | null = null
|
||||
public $parent: MountedComponent | null = null
|
||||
public $children: MountedComponent[] = []
|
||||
public $options: any
|
||||
public $proxy: any = null
|
||||
public $forceUpdate: (() => void) | null = null
|
||||
|
||||
public _rawData: Data | null = null
|
||||
public _computedGetters: Record<string, ComputedGetter> | null = null
|
||||
public _watchHandles: Set<Autorun> | null = null
|
||||
public _mounted: boolean = false
|
||||
public _destroyed: boolean = false
|
||||
public _events: { [event: string]: Function[] | null } | null = null
|
||||
public _updateHandle: Autorun | null = null
|
||||
public _revokeProxy: () => void
|
||||
public _isVue: boolean = true
|
||||
|
||||
constructor(options?: ComponentOptions) {
|
||||
this.$options = options || (this.constructor as any).options || EMPTY_OBJ
|
||||
// root instance
|
||||
if (options !== void 0) {
|
||||
// mount this
|
||||
}
|
||||
}
|
||||
|
||||
$watch(
|
||||
this: MountedComponent,
|
||||
keyOrFn: string | (() => any),
|
||||
cb: () => void
|
||||
) {
|
||||
return setupWatcher(this, keyOrFn, cb)
|
||||
}
|
||||
|
||||
// eventEmitter interface
|
||||
$on(event: string, fn: Function): Component {
|
||||
if (Array.isArray(event)) {
|
||||
for (let i = 0; i < event.length; i++) {
|
||||
this.$on(event[i], fn)
|
||||
}
|
||||
} else {
|
||||
const events = this._events || (this._events = Object.create(null))
|
||||
;(events[event] || (events[event] = [])).push(fn)
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
$once(event: string, fn: Function): Component {
|
||||
const onceFn = (...args: any[]) => {
|
||||
this.$off(event, onceFn)
|
||||
fn.apply(this, args)
|
||||
}
|
||||
;(onceFn as any).fn = fn
|
||||
return this.$on(event, onceFn)
|
||||
}
|
||||
|
||||
$off(event?: string, fn?: Function) {
|
||||
if (this._events) {
|
||||
if (!event && !fn) {
|
||||
this._events = null
|
||||
} else if (Array.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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
$emit(this: MountedComponent, name: string, ...payload: any[]) {
|
||||
const parentListener =
|
||||
this.$props['on' + name] || this.$props['on' + name.toLowerCase()]
|
||||
if (parentListener) {
|
||||
invokeListeners(parentListener, payload)
|
||||
}
|
||||
if (this._events) {
|
||||
const handlers = this._events[name]
|
||||
if (handlers) {
|
||||
invokeListeners(handlers, payload)
|
||||
}
|
||||
}
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
function invokeListeners(value: Function | Function[], payload: any[]) {
|
||||
// TODO handle error
|
||||
if (Array.isArray(value)) {
|
||||
for (let i = 0; i < value.length; i++) {
|
||||
value[i](...payload)
|
||||
}
|
||||
} else {
|
||||
value(...payload)
|
||||
}
|
||||
}
|
||||
65
packages/core/src/componentComputed.ts
Normal file
65
packages/core/src/componentComputed.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { EMPTY_OBJ } from './utils'
|
||||
import { computed, ComputedGetter } from '@vue/observer'
|
||||
import { Component, ComponentClass } from './component'
|
||||
import { ComponentComputedOptions } from './componentOptions'
|
||||
|
||||
const extractionCache: WeakMap<
|
||||
ComponentClass,
|
||||
ComponentComputedOptions
|
||||
> = new WeakMap()
|
||||
|
||||
export function getComputedOptions(
|
||||
comp: ComponentClass
|
||||
): ComponentComputedOptions {
|
||||
let computedOptions = extractionCache.get(comp)
|
||||
if (computedOptions) {
|
||||
return computedOptions
|
||||
}
|
||||
computedOptions = {}
|
||||
const descriptors = Object.getOwnPropertyDescriptors(comp.prototype as any)
|
||||
for (const key in descriptors) {
|
||||
const d = descriptors[key]
|
||||
if (d.get) {
|
||||
computedOptions[key] = d.get
|
||||
// there's no need to do anything for the setter
|
||||
// as it's already defined on the prototype
|
||||
}
|
||||
}
|
||||
return computedOptions
|
||||
}
|
||||
|
||||
export function initializeComputed(
|
||||
instance: Component,
|
||||
computedOptions: ComponentComputedOptions | undefined
|
||||
) {
|
||||
if (!computedOptions) {
|
||||
instance.$computed = EMPTY_OBJ
|
||||
return
|
||||
}
|
||||
const handles: Record<
|
||||
string,
|
||||
ComputedGetter
|
||||
> = (instance._computedGetters = {})
|
||||
const proxy = instance.$proxy
|
||||
for (const key in computedOptions) {
|
||||
handles[key] = computed(computedOptions[key], proxy)
|
||||
}
|
||||
instance.$computed = new Proxy(
|
||||
{},
|
||||
{
|
||||
get(_, key: any) {
|
||||
return handles[key]()
|
||||
}
|
||||
// TODO should be readonly
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
export function teardownComputed(instance: Component) {
|
||||
const handles = instance._computedGetters
|
||||
if (handles !== null) {
|
||||
for (const key in handles) {
|
||||
handles[key].stop()
|
||||
}
|
||||
}
|
||||
}
|
||||
51
packages/core/src/componentOptions.ts
Normal file
51
packages/core/src/componentOptions.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { Slots } from './vdom'
|
||||
import { MountedComponent } from './component'
|
||||
|
||||
export type Data = Record<string, any>
|
||||
|
||||
export interface RenderFunction<P = Data> {
|
||||
(props: P, slots: Slots): any
|
||||
}
|
||||
|
||||
export interface ComponentOptions<D = Data, P = Data> {
|
||||
data?: () => Partial<D>
|
||||
props?: ComponentPropsOptions<P>
|
||||
computed?: ComponentComputedOptions<D, P>
|
||||
watch?: ComponentWatchOptions<D, P>
|
||||
render?: RenderFunction<P>
|
||||
// TODO other options
|
||||
readonly [key: string]: any
|
||||
}
|
||||
|
||||
export type ComponentPropsOptions<P = Data> = {
|
||||
[K in keyof P]: PropValidator<P[K]>
|
||||
}
|
||||
|
||||
export type NormalizedPropsOptions<P = Data> = {
|
||||
[K in keyof P]: PropOptions<P[K]>
|
||||
}
|
||||
|
||||
export type Prop<T> = { (): T } | { new (...args: any[]): T & object }
|
||||
|
||||
export type PropType<T> = Prop<T> | Prop<T>[]
|
||||
|
||||
export type PropValidator<T> = PropOptions<T> | PropType<T>
|
||||
|
||||
export interface PropOptions<T = any> {
|
||||
type?: PropType<T>
|
||||
required?: boolean
|
||||
default?: T | null | undefined | (() => T | null | undefined)
|
||||
validator?(value: T): boolean
|
||||
}
|
||||
|
||||
export interface ComponentComputedOptions<D = Data, P = Data> {
|
||||
[key: string]: (this: MountedComponent<D, P> & D & P, c: any) => any
|
||||
}
|
||||
|
||||
export interface ComponentWatchOptions<D = Data, P = Data> {
|
||||
[key: string]: (
|
||||
this: MountedComponent<D, P> & D & P,
|
||||
oldValue: any,
|
||||
newValue: any
|
||||
) => void
|
||||
}
|
||||
103
packages/core/src/componentProps.ts
Normal file
103
packages/core/src/componentProps.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import { EMPTY_OBJ, isReservedProp } from './utils'
|
||||
import { Component, ComponentClass, MountedComponent } from './component'
|
||||
import { immutable, unwrap, lock, unlock } from '@vue/observer'
|
||||
import {
|
||||
Data,
|
||||
ComponentPropsOptions,
|
||||
NormalizedPropsOptions,
|
||||
PropValidator,
|
||||
PropOptions
|
||||
} from './componentOptions'
|
||||
|
||||
export function initializeProps(instance: Component, props: Data | null) {
|
||||
instance.$props = immutable(props || {})
|
||||
}
|
||||
|
||||
export function updateProps(instance: MountedComponent, nextProps: Data) {
|
||||
// instance.$props is an observable that should not be replaced.
|
||||
// instead, we mutate it to match latest props, which will trigger updates
|
||||
// if any value has changed.
|
||||
if (nextProps != null) {
|
||||
const props = instance.$props
|
||||
const rawProps = unwrap(props)
|
||||
// unlock to temporarily allow mutatiing props
|
||||
unlock()
|
||||
for (const key in rawProps) {
|
||||
if (!nextProps.hasOwnProperty(key)) {
|
||||
delete props[key]
|
||||
}
|
||||
}
|
||||
for (const key in nextProps) {
|
||||
props[key] = nextProps[key]
|
||||
}
|
||||
lock()
|
||||
}
|
||||
}
|
||||
|
||||
// This is called for every component vnode created. This also means the data
|
||||
// on every component vnode is guarunteed to be a fresh object.
|
||||
export function normalizeComponentProps(
|
||||
raw: any,
|
||||
options: ComponentPropsOptions,
|
||||
Component: ComponentClass
|
||||
): Data {
|
||||
if (!raw) {
|
||||
return EMPTY_OBJ
|
||||
}
|
||||
const res: Data = {}
|
||||
const normalizedOptions = options && normalizePropsOptions(options)
|
||||
for (const key in raw) {
|
||||
if (isReservedProp(key)) {
|
||||
continue
|
||||
}
|
||||
if (__DEV__ && normalizedOptions != null) {
|
||||
validateProp(key, raw[key], normalizedOptions[key], Component)
|
||||
} else {
|
||||
res[key] = raw[key]
|
||||
}
|
||||
}
|
||||
// set default values
|
||||
if (normalizedOptions != null) {
|
||||
for (const key in normalizedOptions) {
|
||||
if (res[key] === void 0) {
|
||||
const opt = normalizedOptions[key]
|
||||
if (opt != null && opt.hasOwnProperty('default')) {
|
||||
const defaultValue = opt.default
|
||||
res[key] =
|
||||
typeof defaultValue === 'function' ? defaultValue() : defaultValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
const normalizeCache: WeakMap<
|
||||
ComponentPropsOptions,
|
||||
NormalizedPropsOptions
|
||||
> = new WeakMap()
|
||||
function normalizePropsOptions(
|
||||
raw: ComponentPropsOptions
|
||||
): NormalizedPropsOptions {
|
||||
let cached = normalizeCache.get(raw)
|
||||
if (cached) {
|
||||
return cached
|
||||
}
|
||||
const normalized: NormalizedPropsOptions = {}
|
||||
for (const key in raw) {
|
||||
const opt = raw[key]
|
||||
normalized[key] =
|
||||
typeof opt === 'function' ? { type: opt } : (opt as PropOptions)
|
||||
}
|
||||
normalizeCache.set(raw, normalized)
|
||||
return normalized
|
||||
}
|
||||
|
||||
function validateProp(
|
||||
key: string,
|
||||
value: any,
|
||||
validator: PropValidator<any>,
|
||||
Component: ComponentClass
|
||||
) {
|
||||
// TODO
|
||||
}
|
||||
82
packages/core/src/componentProxy.ts
Normal file
82
packages/core/src/componentProxy.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { Component, MountedComponent } from './component'
|
||||
|
||||
const bindCache = new WeakMap()
|
||||
|
||||
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: MountedComponent, key: string, receiver: any) {
|
||||
if (key === '_self') {
|
||||
return target
|
||||
} else if (
|
||||
target._rawData !== null &&
|
||||
target._rawData.hasOwnProperty(key)
|
||||
) {
|
||||
// data
|
||||
return target.$data[key]
|
||||
} else if (
|
||||
target.$options.props != null &&
|
||||
target.$options.props.hasOwnProperty(key)
|
||||
) {
|
||||
// props are only proxied if declared
|
||||
return target.$props[key]
|
||||
} else if (
|
||||
target._computedGetters !== null &&
|
||||
target._computedGetters.hasOwnProperty(key)
|
||||
) {
|
||||
// computed
|
||||
return target._computedGetters[key]()
|
||||
} else {
|
||||
if (__DEV__ && !(key in target)) {
|
||||
// TODO warn non-present property
|
||||
}
|
||||
const value = Reflect.get(target, key, receiver)
|
||||
if (typeof value === 'function') {
|
||||
// auto bind
|
||||
return getBoundMethod(value, target, receiver)
|
||||
} else {
|
||||
return value
|
||||
}
|
||||
}
|
||||
},
|
||||
set(
|
||||
target: MountedComponent,
|
||||
key: string,
|
||||
value: any,
|
||||
receiver: any
|
||||
): boolean {
|
||||
if (__DEV__) {
|
||||
if (typeof key === 'string' && key[0] === '$') {
|
||||
// TODO warn setting immutable properties
|
||||
return false
|
||||
}
|
||||
if (
|
||||
target.$options.props != null &&
|
||||
target.$options.props.hasOwnProperty(key)
|
||||
) {
|
||||
// TODO warn props are immutable
|
||||
return false
|
||||
}
|
||||
}
|
||||
if (target._rawData !== null && target._rawData.hasOwnProperty(key)) {
|
||||
target.$data[key] = value
|
||||
return true
|
||||
} else {
|
||||
return Reflect.set(target, key, value, receiver)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function createRenderProxy(instance: Component): MountedComponent {
|
||||
return new Proxy(instance, renderProxyHandlers) as MountedComponent
|
||||
}
|
||||
12
packages/core/src/componentState.ts
Normal file
12
packages/core/src/componentState.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { EMPTY_OBJ } from './utils'
|
||||
import { MountedComponent } from './component'
|
||||
import { observable } from '@vue/observer'
|
||||
|
||||
export function initializeState(instance: MountedComponent) {
|
||||
if (instance.data) {
|
||||
instance._rawData = instance.data()
|
||||
instance.$data = observable(instance._rawData)
|
||||
} else {
|
||||
instance.$data = EMPTY_OBJ
|
||||
}
|
||||
}
|
||||
180
packages/core/src/componentUtils.ts
Normal file
180
packages/core/src/componentUtils.ts
Normal file
@@ -0,0 +1,180 @@
|
||||
import { VNodeFlags } from './flags'
|
||||
import { EMPTY_OBJ } from './utils'
|
||||
import { VNode, createFragment } from './vdom'
|
||||
import { Component, MountedComponent, ComponentClass } from './component'
|
||||
import { createTextVNode, cloneVNode } from './vdom'
|
||||
import { initializeState } from './componentState'
|
||||
import { initializeProps } from './componentProps'
|
||||
import {
|
||||
initializeComputed,
|
||||
getComputedOptions,
|
||||
teardownComputed
|
||||
} from './componentComputed'
|
||||
import { initializeWatch, teardownWatch } from './componentWatch'
|
||||
import { Data, ComponentOptions } from './componentOptions'
|
||||
import { createRenderProxy } from './componentProxy'
|
||||
|
||||
export function createComponentInstance(
|
||||
vnode: VNode,
|
||||
Component: ComponentClass,
|
||||
parentComponent: MountedComponent | null
|
||||
): MountedComponent {
|
||||
const instance = (vnode.children = new Component()) as MountedComponent
|
||||
instance.$parentVNode = vnode
|
||||
|
||||
// renderProxy
|
||||
const proxy = (instance.$proxy = createRenderProxy(instance))
|
||||
|
||||
// pointer management
|
||||
if (parentComponent) {
|
||||
instance.$parent = parentComponent.$proxy
|
||||
instance.$root = parentComponent.$root
|
||||
parentComponent.$children.push(proxy)
|
||||
} else {
|
||||
instance.$root = proxy
|
||||
}
|
||||
|
||||
// lifecycle
|
||||
if (instance.beforeCreate) {
|
||||
instance.beforeCreate.call(proxy)
|
||||
}
|
||||
// TODO provide/inject
|
||||
initializeProps(instance, vnode.data)
|
||||
initializeState(instance)
|
||||
initializeComputed(instance, getComputedOptions(Component))
|
||||
initializeWatch(instance, instance.$options.watch)
|
||||
instance.$slots = vnode.slots || EMPTY_OBJ
|
||||
if (instance.created) {
|
||||
instance.created.call(proxy)
|
||||
}
|
||||
|
||||
return instance as MountedComponent
|
||||
}
|
||||
|
||||
export function renderInstanceRoot(instance: MountedComponent) {
|
||||
// TODO handle render error
|
||||
return normalizeComponentRoot(
|
||||
instance.render.call(instance.$proxy, instance.$props, instance.$slots),
|
||||
instance.$parentVNode
|
||||
)
|
||||
}
|
||||
|
||||
export function teardownComponentInstance(instance: MountedComponent) {
|
||||
const parentComponent = instance.$parent && instance.$parent._self
|
||||
if (parentComponent && !parentComponent._destroyed) {
|
||||
parentComponent.$children.splice(
|
||||
parentComponent.$children.indexOf(instance.$proxy),
|
||||
1
|
||||
)
|
||||
}
|
||||
teardownComputed(instance)
|
||||
teardownWatch(instance)
|
||||
}
|
||||
|
||||
export function normalizeComponentRoot(
|
||||
vnode: any,
|
||||
componentVNode: VNode | null
|
||||
): VNode {
|
||||
if (vnode == null) {
|
||||
vnode = createTextVNode('')
|
||||
} else if (typeof vnode !== 'object') {
|
||||
vnode = createTextVNode(vnode + '')
|
||||
} else if (Array.isArray(vnode)) {
|
||||
vnode = createFragment(vnode)
|
||||
} else {
|
||||
const { flags } = vnode
|
||||
// parentVNode data merge down
|
||||
if (
|
||||
componentVNode &&
|
||||
(flags & VNodeFlags.COMPONENT || flags & VNodeFlags.ELEMENT)
|
||||
) {
|
||||
const parentData = componentVNode.data || EMPTY_OBJ
|
||||
const childData = vnode.data || EMPTY_OBJ
|
||||
let extraData: any = null
|
||||
for (const key in parentData) {
|
||||
// class/style bindings on parentVNode are merged down to child
|
||||
// component root.
|
||||
if (key === 'class') {
|
||||
;(extraData || (extraData = {})).class = childData.class
|
||||
? [].concat(childData.class, parentData.class)
|
||||
: parentData.class
|
||||
} else if (key === 'style') {
|
||||
;(extraData || (extraData = {})).style = childData.style
|
||||
? [].concat(childData.style, parentData.style)
|
||||
: parentData.style
|
||||
} else if (key.startsWith('nativeOn')) {
|
||||
// nativeOn* handlers are merged down to child root as native listeners
|
||||
const event = 'on' + key.slice(8)
|
||||
;(extraData || (extraData = {}))[event] = childData.event
|
||||
? [].concat(childData.event, parentData[key])
|
||||
: parentData[key]
|
||||
}
|
||||
}
|
||||
if (extraData) {
|
||||
vnode = cloneVNode(vnode, extraData)
|
||||
}
|
||||
if (vnode.el) {
|
||||
vnode = cloneVNode(vnode)
|
||||
}
|
||||
if (flags & VNodeFlags.COMPONENT) {
|
||||
vnode.parentVNode = componentVNode
|
||||
}
|
||||
} else if (vnode.el) {
|
||||
vnode = cloneVNode(vnode)
|
||||
}
|
||||
}
|
||||
return vnode
|
||||
}
|
||||
|
||||
export function shouldUpdateFunctionalComponent(
|
||||
prevProps: Data | null,
|
||||
nextProps: Data | null
|
||||
): boolean {
|
||||
if (prevProps === nextProps) {
|
||||
return false
|
||||
}
|
||||
if (prevProps === null) {
|
||||
return nextProps !== null
|
||||
}
|
||||
if (nextProps === null) {
|
||||
return prevProps !== null
|
||||
}
|
||||
let shouldUpdate = true
|
||||
const nextKeys = Object.keys(nextProps)
|
||||
if (nextKeys.length === Object.keys(prevProps).length) {
|
||||
shouldUpdate = false
|
||||
for (let i = 0; i < nextKeys.length; i++) {
|
||||
const key = nextKeys[i]
|
||||
if (nextProps[key] !== prevProps[key]) {
|
||||
shouldUpdate = true
|
||||
}
|
||||
}
|
||||
}
|
||||
return shouldUpdate
|
||||
}
|
||||
|
||||
export function createComponentClassFromOptions(
|
||||
options: ComponentOptions
|
||||
): ComponentClass {
|
||||
class ObjectComponent extends Component {
|
||||
constructor() {
|
||||
super()
|
||||
this.$options = options
|
||||
}
|
||||
}
|
||||
for (const key in options) {
|
||||
const value = options[key]
|
||||
if (typeof value === 'function') {
|
||||
;(ObjectComponent.prototype as any)[key] = value
|
||||
}
|
||||
if (key === 'computed') {
|
||||
const isGet = typeof value === 'function'
|
||||
Object.defineProperty(ObjectComponent.prototype, key, {
|
||||
configurable: true,
|
||||
get: isGet ? value : value.get,
|
||||
set: isGet ? undefined : value.set
|
||||
})
|
||||
}
|
||||
}
|
||||
return ObjectComponent as ComponentClass
|
||||
}
|
||||
50
packages/core/src/componentWatch.ts
Normal file
50
packages/core/src/componentWatch.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { MountedComponent } from './component'
|
||||
import { ComponentWatchOptions } from './componentOptions'
|
||||
import { autorun, stop, Autorun } from '@vue/observer'
|
||||
|
||||
export function initializeWatch(
|
||||
instance: MountedComponent,
|
||||
options: ComponentWatchOptions | undefined
|
||||
) {
|
||||
if (options !== void 0) {
|
||||
for (const key in options) {
|
||||
setupWatcher(instance, key, options[key])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO deep watch
|
||||
export function setupWatcher(
|
||||
instance: MountedComponent,
|
||||
keyOrFn: string | Function,
|
||||
cb: Function
|
||||
): () => void {
|
||||
const handles = instance._watchHandles || (instance._watchHandles = new Set())
|
||||
const proxy = instance.$proxy
|
||||
const rawGetter =
|
||||
typeof keyOrFn === 'string'
|
||||
? () => proxy[keyOrFn]
|
||||
: () => keyOrFn.call(proxy)
|
||||
let oldValue: any
|
||||
const runner = autorun(rawGetter, {
|
||||
scheduler: (runner: Autorun) => {
|
||||
const newValue = runner()
|
||||
if (newValue !== oldValue) {
|
||||
cb(newValue, oldValue)
|
||||
oldValue = newValue
|
||||
}
|
||||
}
|
||||
})
|
||||
oldValue = runner()
|
||||
handles.add(runner)
|
||||
return () => {
|
||||
stop(runner)
|
||||
handles.delete(runner)
|
||||
}
|
||||
}
|
||||
|
||||
export function teardownWatch(instance: MountedComponent) {
|
||||
if (instance._watchHandles !== null) {
|
||||
instance._watchHandles.forEach(stop)
|
||||
}
|
||||
}
|
||||
1309
packages/core/src/createRenderer.ts
Normal file
1309
packages/core/src/createRenderer.ts
Normal file
File diff suppressed because it is too large
Load Diff
26
packages/core/src/errorHandling.ts
Normal file
26
packages/core/src/errorHandling.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { MountedComponent } from './component'
|
||||
|
||||
export const enum ErrorTypes {
|
||||
LIFECYCLE = 1,
|
||||
RENDER = 2,
|
||||
NATIVE_EVENT_HANDLER = 3,
|
||||
COMPONENT_EVENT_HANDLER = 4
|
||||
}
|
||||
|
||||
const globalHandlers: Function[] = []
|
||||
|
||||
export function globalHandleError(handler: () => void) {
|
||||
globalHandlers.push(handler)
|
||||
return () => {
|
||||
globalHandlers.splice(globalHandlers.indexOf(handler), 1)
|
||||
}
|
||||
}
|
||||
|
||||
export function handleError(
|
||||
err: Error,
|
||||
instance: MountedComponent,
|
||||
type: ErrorTypes,
|
||||
code: number
|
||||
) {
|
||||
// TODO
|
||||
}
|
||||
31
packages/core/src/flags.ts
Normal file
31
packages/core/src/flags.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
// vnode flags
|
||||
export const enum VNodeFlags {
|
||||
ELEMENT_HTML = 1,
|
||||
ELEMENT_SVG = 1 << 1,
|
||||
ELEMENT = ELEMENT_HTML | ELEMENT_SVG,
|
||||
|
||||
COMPONENT_UNKNOWN = 1 << 2,
|
||||
COMPONENT_STATEFUL = 1 << 3,
|
||||
COMPONENT_FUNCTIONAL = 1 << 4,
|
||||
COMPONENT_ASYNC = 1 << 5,
|
||||
COMPONENT = COMPONENT_UNKNOWN |
|
||||
COMPONENT_STATEFUL |
|
||||
COMPONENT_FUNCTIONAL |
|
||||
COMPONENT_ASYNC,
|
||||
|
||||
TEXT = 1 << 6,
|
||||
FRAGMENT = 1 << 7,
|
||||
PORTAL = 1 << 8
|
||||
}
|
||||
|
||||
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,
|
||||
HAS_SLOTS = STABLE_SLOTS | DYNAMIC_SLOTS,
|
||||
MULTIPLE_VNODES = KEYED_VNODES | NONE_KEYED_VNODES
|
||||
}
|
||||
104
packages/core/src/h.ts
Normal file
104
packages/core/src/h.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import { ChildrenFlags } from './flags'
|
||||
import { ComponentClass, FunctionalComponent } from './component'
|
||||
import { ComponentOptions } from './componentOptions'
|
||||
import {
|
||||
VNode,
|
||||
createElementVNode,
|
||||
createComponentVNode,
|
||||
createTextVNode,
|
||||
createFragment,
|
||||
createPortal
|
||||
} from './vdom'
|
||||
|
||||
export const Fragment = Symbol()
|
||||
export const Portal = Symbol()
|
||||
|
||||
type ElementType =
|
||||
| string
|
||||
| FunctionalComponent
|
||||
| ComponentClass
|
||||
| ComponentOptions
|
||||
| typeof Fragment
|
||||
| typeof Portal
|
||||
|
||||
export interface createElement {
|
||||
(tag: ElementType, data: any, children: any): VNode
|
||||
c: typeof createComponentVNode
|
||||
e: typeof createElementVNode
|
||||
t: typeof createTextVNode
|
||||
f: typeof createFragment
|
||||
p: typeof createPortal
|
||||
}
|
||||
|
||||
export const h = ((tag: ElementType, data: any, children: any): VNode => {
|
||||
if (Array.isArray(data) || (data !== void 0 && typeof data !== 'object')) {
|
||||
children = data
|
||||
data = null
|
||||
}
|
||||
|
||||
// TODO clone data if it is observed
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof tag === 'string') {
|
||||
// element
|
||||
return createElementVNode(
|
||||
tag,
|
||||
data,
|
||||
children,
|
||||
ChildrenFlags.UNKNOWN_CHILDREN,
|
||||
key,
|
||||
ref
|
||||
)
|
||||
} else if (tag === Fragment) {
|
||||
if (__DEV__ && ref) {
|
||||
// TODO warn fragment cannot have ref
|
||||
}
|
||||
return createFragment(children, ChildrenFlags.UNKNOWN_CHILDREN, key)
|
||||
} else if (tag === Portal) {
|
||||
if (__DEV__ && !portalTarget) {
|
||||
// TODO warn portal must have a target
|
||||
}
|
||||
return createPortal(
|
||||
portalTarget,
|
||||
children,
|
||||
ChildrenFlags.UNKNOWN_CHILDREN,
|
||||
key,
|
||||
ref
|
||||
)
|
||||
} else {
|
||||
// TODO: handle fragment & portal types
|
||||
// TODO: warn ref on fragment
|
||||
// component
|
||||
return createComponentVNode(
|
||||
tag,
|
||||
data,
|
||||
children,
|
||||
ChildrenFlags.UNKNOWN_CHILDREN,
|
||||
key,
|
||||
ref
|
||||
)
|
||||
}
|
||||
}) as createElement
|
||||
|
||||
h.c = createComponentVNode
|
||||
h.e = createElementVNode
|
||||
h.t = createTextVNode
|
||||
h.f = createFragment
|
||||
h.p = createPortal
|
||||
29
packages/core/src/index.ts
Normal file
29
packages/core/src/index.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
// render api
|
||||
export { h, Fragment, Portal } from './h'
|
||||
export { cloneVNode, createPortal, createFragment } from './vdom'
|
||||
export { createRenderer } from './createRenderer'
|
||||
|
||||
import { Component as InternalComponent, ComponentClass } from './component'
|
||||
|
||||
// the public component constructor with proper type inference.
|
||||
export const Component = InternalComponent as ComponentClass
|
||||
|
||||
// observer api
|
||||
export {
|
||||
autorun,
|
||||
stop,
|
||||
observable,
|
||||
immutable,
|
||||
computed,
|
||||
isObservable,
|
||||
isImmutable,
|
||||
markImmutable,
|
||||
markNonReactive,
|
||||
unwrap
|
||||
} from '@vue/observer'
|
||||
|
||||
// flags & types
|
||||
export { FunctionalComponent } from './component'
|
||||
export { ComponentOptions, PropType } from './componentOptions'
|
||||
export { VNodeFlags, ChildrenFlags } from './flags'
|
||||
export { VNode, VNodeData, VNodeChildren, Key, Ref, Slots, Slot } from './vdom'
|
||||
12
packages/core/src/utils.ts
Normal file
12
packages/core/src/utils.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export const EMPTY_OBJ: { readonly [key: string]: any } = Object.freeze({})
|
||||
|
||||
export const isReservedProp = (key: string): boolean => {
|
||||
switch (key) {
|
||||
case 'key':
|
||||
case 'ref':
|
||||
case 'slots':
|
||||
return true
|
||||
default:
|
||||
return key.startsWith('nativeOn')
|
||||
}
|
||||
}
|
||||
360
packages/core/src/vdom.ts
Normal file
360
packages/core/src/vdom.ts
Normal file
@@ -0,0 +1,360 @@
|
||||
import {
|
||||
MountedComponent,
|
||||
ComponentClass,
|
||||
FunctionalComponent
|
||||
} from './component'
|
||||
import { VNodeFlags, ChildrenFlags } from './flags'
|
||||
import { normalizeComponentProps } from './componentProps'
|
||||
import { createComponentClassFromOptions } from './componentUtils'
|
||||
import { ComponentPropsOptions } from './componentOptions'
|
||||
|
||||
// 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 platforn render nodes,
|
||||
// but we list it here so that TS can figure out union types
|
||||
$f: false
|
||||
}
|
||||
|
||||
export interface RenderFragment {
|
||||
children: (RenderNode | RenderFragment)[]
|
||||
$f: true
|
||||
}
|
||||
|
||||
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 | RenderFragment | null
|
||||
// only on mounted component root nodes
|
||||
// points to component node in parent tree
|
||||
parentVNode: VNode | null
|
||||
}
|
||||
|
||||
export interface MountedVNode extends VNode {
|
||||
el: RenderNode | RenderFragment
|
||||
}
|
||||
|
||||
export type MountedVNodes = MountedVNode[]
|
||||
|
||||
export interface VNodeData {
|
||||
key?: Key | null
|
||||
ref?: Ref | null
|
||||
slots?: Slots | null
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export type VNodeChildren =
|
||||
| VNode[] // ELEMENT | PORTAL
|
||||
| MountedComponent // COMPONENT_STATEFUL
|
||||
| VNode // COMPONENT_FUNCTIONAL
|
||||
| string // TEXT
|
||||
| null
|
||||
|
||||
export type Key = string | number
|
||||
|
||||
export type Ref = (t: RenderNode | MountedComponent | null) => void
|
||||
|
||||
export interface Slots {
|
||||
[name: string]: Slot
|
||||
}
|
||||
|
||||
export type Slot = (...args: any[]) => VNode[]
|
||||
|
||||
export function createVNode(
|
||||
flags: VNodeFlags,
|
||||
tag: string | FunctionalComponent | ComponentClass | RenderNode | null,
|
||||
data: VNodeData | null,
|
||||
children: VNodeChildren | 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,
|
||||
childFlags,
|
||||
key: key === void 0 ? null : key,
|
||||
ref: ref === void 0 ? null : ref,
|
||||
slots: slots === void 0 ? null : slots,
|
||||
el: null,
|
||||
parentVNode: null
|
||||
}
|
||||
if (childFlags === ChildrenFlags.UNKNOWN_CHILDREN) {
|
||||
normalizeChildren(vnode, children)
|
||||
}
|
||||
return vnode
|
||||
}
|
||||
|
||||
export function createElementVNode(
|
||||
tag: string,
|
||||
data: VNodeData | null,
|
||||
children: VNodeChildren,
|
||||
childFlags: ChildrenFlags,
|
||||
key?: Key | null,
|
||||
ref?: Ref | null
|
||||
) {
|
||||
const flags = tag === 'svg' ? VNodeFlags.ELEMENT_SVG : VNodeFlags.ELEMENT_HTML
|
||||
return createVNode(flags, tag, data, children, childFlags, key, ref, null)
|
||||
}
|
||||
|
||||
export function createComponentVNode(
|
||||
comp: any,
|
||||
data: VNodeData | null,
|
||||
children: VNodeChildren,
|
||||
childFlags: ChildrenFlags,
|
||||
key?: Key | null,
|
||||
ref?: Ref | null
|
||||
) {
|
||||
// resolve type
|
||||
let flags: VNodeFlags
|
||||
let propsOptions: ComponentPropsOptions
|
||||
|
||||
// flags
|
||||
const compType = typeof comp
|
||||
if (__COMPAT__ && compType === 'object') {
|
||||
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
|
||||
propsOptions = comp.props
|
||||
} else {
|
||||
// object literal stateful
|
||||
flags = VNodeFlags.COMPONENT_STATEFUL
|
||||
comp =
|
||||
comp._normalized ||
|
||||
(comp._normalized = createComponentClassFromOptions(comp))
|
||||
propsOptions = comp.options && comp.options.props
|
||||
}
|
||||
} else {
|
||||
// assumes comp is function here now
|
||||
if (__DEV__ && compType !== 'function') {
|
||||
// TODO warn invalid comp value in dev
|
||||
}
|
||||
if (comp.prototype && comp.prototype.render) {
|
||||
flags = VNodeFlags.COMPONENT_STATEFUL
|
||||
propsOptions = comp.options && comp.options.props
|
||||
} else {
|
||||
flags = VNodeFlags.COMPONENT_FUNCTIONAL
|
||||
propsOptions = comp.props
|
||||
}
|
||||
}
|
||||
|
||||
if (__DEV__ && flags === VNodeFlags.COMPONENT_FUNCTIONAL && ref) {
|
||||
// TODO warn functional component cannot have ref
|
||||
}
|
||||
|
||||
// props
|
||||
const props = normalizeComponentProps(data, propsOptions, comp)
|
||||
|
||||
// slots
|
||||
let slots: any
|
||||
if (childFlags == null) {
|
||||
childFlags = children
|
||||
? ChildrenFlags.DYNAMIC_SLOTS
|
||||
: ChildrenFlags.NO_CHILDREN
|
||||
if (children != null) {
|
||||
const childrenType = typeof children
|
||||
if (childrenType === 'function') {
|
||||
// function as children
|
||||
slots = { default: children }
|
||||
} else if (childrenType === 'object' && !(children as VNode)._isVNode) {
|
||||
// slot object as children
|
||||
slots = children
|
||||
} else {
|
||||
slots = { default: () => children }
|
||||
}
|
||||
slots = normalizeSlots(slots)
|
||||
}
|
||||
}
|
||||
|
||||
return createVNode(
|
||||
flags,
|
||||
comp,
|
||||
props,
|
||||
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: VNodeChildren,
|
||||
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: VNodeChildren,
|
||||
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]
|
||||
}
|
||||
}
|
||||
for (const key in extraData) {
|
||||
clonedData[key] = extraData[key]
|
||||
}
|
||||
}
|
||||
return createVNode(
|
||||
flags,
|
||||
vnode.tag,
|
||||
clonedData,
|
||||
vnode.children,
|
||||
vnode.childFlags,
|
||||
vnode.key,
|
||||
vnode.ref,
|
||||
vnode.slots
|
||||
)
|
||||
} else if (flags & VNodeFlags.TEXT) {
|
||||
return createTextVNode(vnode.children as string)
|
||||
} else {
|
||||
return vnode
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeChildren(vnode: VNode, children: any) {
|
||||
let childFlags
|
||||
if (Array.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 (Array.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 {
|
||||
const normalized: Slots = {}
|
||||
for (const name in slots) {
|
||||
normalized[name] = (...args) => normalizeSlot(slots[name](...args))
|
||||
}
|
||||
return normalized
|
||||
}
|
||||
|
||||
function normalizeSlot(value: any): VNode[] {
|
||||
if (value == null) {
|
||||
return [createTextVNode('')]
|
||||
} else if (Array.isArray(value)) {
|
||||
return normalizeVNodes(value)
|
||||
} else if (value._isVNode) {
|
||||
return [value]
|
||||
} else {
|
||||
return [createTextVNode(value + '')]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user