init (graduate from prototype)
This commit is contained in:
commit
3401f6b460
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
dist
|
||||
.DS_Store
|
||||
node_modules
|
||||
explorations
|
||||
TODOs.md
|
3
.prettierrc
Normal file
3
.prettierrc
Normal file
@ -0,0 +1,3 @@
|
||||
semi: false
|
||||
singleQuote: true
|
||||
printWidth: 80
|
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"typescript.tsdk": "node_modules/typescript/lib"
|
||||
}
|
6
lerna.json
Normal file
6
lerna.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"packages": [
|
||||
"packages/*"
|
||||
],
|
||||
"version": "3.0.0-alpha.1"
|
||||
}
|
26
package.json
Normal file
26
package.json
Normal file
@ -0,0 +1,26 @@
|
||||
{
|
||||
"private": true,
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
],
|
||||
"scripts": {
|
||||
"dev": "node scripts/dev.js",
|
||||
"build": "node scripts/build.js",
|
||||
"lint": "prettier --write --parser typescript 'packages/*/src/**/*.ts'"
|
||||
},
|
||||
"devDependencies": {
|
||||
"chalk": "^2.4.1",
|
||||
"dts-bundle": "^0.7.3",
|
||||
"execa": "^1.0.0",
|
||||
"fs-extra": "^7.0.0",
|
||||
"lerna": "^3.4.0",
|
||||
"minimist": "^1.2.0",
|
||||
"prettier": "^1.14.2",
|
||||
"rollup": "^0.65.0",
|
||||
"rollup-plugin-alias": "^1.4.0",
|
||||
"rollup-plugin-replace": "^2.0.0",
|
||||
"rollup-plugin-terser": "^2.0.2",
|
||||
"rollup-plugin-typescript2": "^0.17.0",
|
||||
"typescript": "^3.0.3"
|
||||
}
|
||||
}
|
3
packages/core/.npmignore
Normal file
3
packages/core/.npmignore
Normal file
@ -0,0 +1,3 @@
|
||||
__tests__/
|
||||
__mocks__/
|
||||
dist/packages
|
3
packages/core/README.md
Normal file
3
packages/core/README.md
Normal file
@ -0,0 +1,3 @@
|
||||
# @vue/core
|
||||
|
||||
> This package is published only for typing and building custom renderers. It is NOT meant to be used in applications.
|
7
packages/core/index.js
Normal file
7
packages/core/index.js
Normal file
@ -0,0 +1,7 @@
|
||||
'use strict'
|
||||
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
module.exports = require('./dist/core.cjs.prod.js')
|
||||
} else {
|
||||
module.exports = require('./dist/core.cjs.js')
|
||||
}
|
24
packages/core/package.json
Normal file
24
packages/core/package.json
Normal file
@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "@vue/core",
|
||||
"version": "3.0.0-alpha.1",
|
||||
"description": "@vue/core",
|
||||
"main": "index.js",
|
||||
"module": "dist/core.esm.js",
|
||||
"typings": "dist/index.d.ts",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/vuejs/vue.git"
|
||||
},
|
||||
"keywords": [
|
||||
"vue"
|
||||
],
|
||||
"author": "Evan You",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/vuejs/vue/issues"
|
||||
},
|
||||
"homepage": "https://github.com/vuejs/vue/tree/dev/packages/core#readme",
|
||||
"dependencies": {
|
||||
"@vue/observer": "3.0.0-alpha.1"
|
||||
}
|
||||
}
|
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 + '')]
|
||||
}
|
||||
}
|
3
packages/global.d.ts
vendored
Normal file
3
packages/global.d.ts
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
// Global compile-time constants
|
||||
declare var __DEV__: boolean
|
||||
declare var __COMPAT__: boolean
|
3
packages/observer/.npmignore
Normal file
3
packages/observer/.npmignore
Normal file
@ -0,0 +1,3 @@
|
||||
__tests__/
|
||||
__mocks__/
|
||||
dist/packages
|
3
packages/observer/README.md
Normal file
3
packages/observer/README.md
Normal file
@ -0,0 +1,3 @@
|
||||
# @vue/observer
|
||||
|
||||
> This package is inlined into UMD & Browser ESM builds of user-facing renderers (e.g. `@vue/runtime-dom`), but also published as a package that can be used standalone. The standalone build should not be used alongside a pre-bundled build of a user-facing renderer, as they will have different internal storage for reactivity connections. A user-facing renderer should re-export all APIs from this package.
|
7
packages/observer/index.js
Normal file
7
packages/observer/index.js
Normal file
@ -0,0 +1,7 @@
|
||||
'use strict'
|
||||
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
module.exports = require('./dist/observer.cjs.prod.js')
|
||||
} else {
|
||||
module.exports = require('./dist/observer.cjs.js')
|
||||
}
|
26
packages/observer/package.json
Normal file
26
packages/observer/package.json
Normal file
@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "@vue/observer",
|
||||
"version": "3.0.0-alpha.1",
|
||||
"description": "@vue/observer",
|
||||
"main": "index.js",
|
||||
"module": "dist/observer.esm.js",
|
||||
"typings": "dist/index.d.ts",
|
||||
"unpkg": "dist/observer.umd.js",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/vuejs/vue.git"
|
||||
},
|
||||
"buildOptions": {
|
||||
"name": "VueObserver",
|
||||
"formats": ["esm", "cjs", "umd", "esm-browser"]
|
||||
},
|
||||
"keywords": [
|
||||
"vue"
|
||||
],
|
||||
"author": "Evan You",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/vuejs/vue/issues"
|
||||
},
|
||||
"homepage": "https://github.com/vuejs/vue/tree/dev/packages/observer#readme"
|
||||
}
|
164
packages/observer/src/autorun.ts
Normal file
164
packages/observer/src/autorun.ts
Normal file
@ -0,0 +1,164 @@
|
||||
import { OperationTypes } from './operations'
|
||||
import { Dep, KeyToDepMap, targetMap } from './state'
|
||||
|
||||
export interface Autorun {
|
||||
(): any
|
||||
isAutorun: true
|
||||
active: boolean
|
||||
raw: Function
|
||||
deps: Array<Dep>
|
||||
scheduler?: Scheduler
|
||||
onTrack?: Debugger
|
||||
onTrigger?: Debugger
|
||||
}
|
||||
|
||||
export interface AutorunOptions {
|
||||
lazy?: boolean
|
||||
scheduler?: Scheduler
|
||||
onTrack?: Debugger
|
||||
onTrigger?: Debugger
|
||||
}
|
||||
|
||||
export type Scheduler = (run: () => any) => void
|
||||
|
||||
export type DebuggerEvent = {
|
||||
runner: Autorun
|
||||
target: any
|
||||
type: OperationTypes
|
||||
key: string | symbol | undefined
|
||||
}
|
||||
|
||||
export type Debugger = (event: DebuggerEvent) => void
|
||||
|
||||
export const activeAutorunStack: Autorun[] = []
|
||||
|
||||
const ITERATE_KEY = Symbol('iterate')
|
||||
|
||||
export function createAutorun(fn: Function, options: AutorunOptions): Autorun {
|
||||
const runner = function runner(...args): any {
|
||||
return run(runner as Autorun, fn, args)
|
||||
} as Autorun
|
||||
runner.active = true
|
||||
runner.raw = fn
|
||||
runner.scheduler = options.scheduler
|
||||
runner.onTrack = options.onTrack
|
||||
runner.onTrigger = options.onTrigger
|
||||
runner.deps = []
|
||||
return runner
|
||||
}
|
||||
|
||||
function run(runner: Autorun, fn: Function, args: any[]): any {
|
||||
if (!runner.active) {
|
||||
return fn(...args)
|
||||
}
|
||||
if (activeAutorunStack.indexOf(runner) === -1) {
|
||||
cleanup(runner)
|
||||
try {
|
||||
activeAutorunStack.push(runner)
|
||||
return fn(...args)
|
||||
} finally {
|
||||
activeAutorunStack.pop()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function cleanup(runner: Autorun) {
|
||||
for (let i = 0; i < runner.deps.length; i++) {
|
||||
runner.deps[i].delete(runner)
|
||||
}
|
||||
runner.deps = []
|
||||
}
|
||||
|
||||
export function track(
|
||||
target: any,
|
||||
type: OperationTypes,
|
||||
key?: string | symbol
|
||||
) {
|
||||
const runner = activeAutorunStack[activeAutorunStack.length - 1]
|
||||
if (runner) {
|
||||
if (type === OperationTypes.ITERATE) {
|
||||
key = ITERATE_KEY
|
||||
}
|
||||
// keyMap must exist because only an observed target can call this function
|
||||
const depsMap = targetMap.get(target) as KeyToDepMap
|
||||
let dep = depsMap.get(key as string | symbol)
|
||||
if (!dep) {
|
||||
depsMap.set(key as string | symbol, (dep = new Set()))
|
||||
}
|
||||
if (!dep.has(runner)) {
|
||||
dep.add(runner)
|
||||
runner.deps.push(dep)
|
||||
if (__DEV__ && runner.onTrack) {
|
||||
runner.onTrack({
|
||||
runner,
|
||||
target,
|
||||
type,
|
||||
key
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function trigger(
|
||||
target: any,
|
||||
type: OperationTypes,
|
||||
key?: string | symbol,
|
||||
extraInfo?: any
|
||||
) {
|
||||
const depsMap = targetMap.get(target) as KeyToDepMap
|
||||
const runners = new Set()
|
||||
if (type === OperationTypes.CLEAR) {
|
||||
// collection being cleared, trigger all runners for target
|
||||
depsMap.forEach(dep => {
|
||||
addRunners(runners, dep)
|
||||
})
|
||||
} else {
|
||||
// schedule runs for SET | ADD | DELETE
|
||||
addRunners(runners, depsMap.get(key as string | symbol))
|
||||
// also run for iteration key on ADD | DELETE
|
||||
if (type === OperationTypes.ADD || type === OperationTypes.DELETE) {
|
||||
const iterationKey = Array.isArray(target) ? 'length' : ITERATE_KEY
|
||||
addRunners(runners, depsMap.get(iterationKey))
|
||||
}
|
||||
}
|
||||
runners.forEach(runner => {
|
||||
scheduleRun(runner, target, type, key, extraInfo)
|
||||
})
|
||||
}
|
||||
|
||||
function addRunners(
|
||||
runners: Set<Autorun>,
|
||||
runnersToAdd: Set<Autorun> | undefined
|
||||
) {
|
||||
if (runnersToAdd !== void 0) {
|
||||
runnersToAdd.forEach(runners.add, runners)
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleRun(
|
||||
runner: Autorun,
|
||||
target: any,
|
||||
type: OperationTypes,
|
||||
key: string | symbol | undefined,
|
||||
extraInfo: any
|
||||
) {
|
||||
if (__DEV__ && runner.onTrigger) {
|
||||
runner.onTrigger(
|
||||
Object.assign(
|
||||
{
|
||||
runner,
|
||||
target,
|
||||
key,
|
||||
type
|
||||
},
|
||||
extraInfo
|
||||
)
|
||||
)
|
||||
}
|
||||
if (runner.scheduler !== void 0) {
|
||||
runner.scheduler(runner)
|
||||
} else {
|
||||
runner()
|
||||
}
|
||||
}
|
120
packages/observer/src/baseHandlers.ts
Normal file
120
packages/observer/src/baseHandlers.ts
Normal file
@ -0,0 +1,120 @@
|
||||
import { observable, immutable, unwrap } from './index'
|
||||
import { OperationTypes } from './operations'
|
||||
import { track, trigger } from './autorun'
|
||||
import { LOCKED } from './lock'
|
||||
|
||||
const hasOwnProperty = Object.prototype.hasOwnProperty
|
||||
|
||||
const builtInSymbols = new Set(
|
||||
Object.getOwnPropertyNames(Symbol)
|
||||
.map(key => (Symbol as any)[key])
|
||||
.filter(value => typeof value === 'symbol')
|
||||
)
|
||||
|
||||
function get(
|
||||
target: any,
|
||||
key: string | symbol,
|
||||
receiver: any,
|
||||
toObsevable: (t: any) => any
|
||||
) {
|
||||
const res = Reflect.get(target, key, receiver)
|
||||
if (typeof key === 'symbol' && builtInSymbols.has(key)) {
|
||||
return res
|
||||
}
|
||||
track(target, OperationTypes.GET, key)
|
||||
return res !== null && typeof res === 'object' ? toObsevable(res) : res
|
||||
}
|
||||
|
||||
function set(
|
||||
target: any,
|
||||
key: string | symbol,
|
||||
value: any,
|
||||
receiver: any
|
||||
): boolean {
|
||||
value = unwrap(value)
|
||||
const hadKey = hasOwnProperty.call(target, key)
|
||||
const oldValue = target[key]
|
||||
const result = Reflect.set(target, key, value, receiver)
|
||||
// don't trigger if target is something up in the prototype chain of original
|
||||
if (target === unwrap(receiver)) {
|
||||
if (__DEV__) {
|
||||
const extraInfo = { oldValue, newValue: value }
|
||||
if (!hadKey) {
|
||||
trigger(target, OperationTypes.ADD, key, extraInfo)
|
||||
} else if (value !== oldValue) {
|
||||
trigger(target, OperationTypes.SET, key, extraInfo)
|
||||
}
|
||||
} else {
|
||||
if (!hadKey) {
|
||||
trigger(target, OperationTypes.ADD, key)
|
||||
} else if (value !== oldValue) {
|
||||
trigger(target, OperationTypes.SET, key)
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function deleteProperty(target: any, key: string | symbol): boolean {
|
||||
const hadKey = hasOwnProperty.call(target, key)
|
||||
const oldValue = target[key]
|
||||
const result = Reflect.deleteProperty(target, key)
|
||||
if (hadKey) {
|
||||
if (__DEV__) {
|
||||
trigger(target, OperationTypes.DELETE, key, { oldValue })
|
||||
} else {
|
||||
trigger(target, OperationTypes.DELETE, key)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function has(target: any, key: string | symbol): boolean {
|
||||
const result = Reflect.has(target, key)
|
||||
track(target, OperationTypes.HAS, key)
|
||||
return result
|
||||
}
|
||||
|
||||
function ownKeys(target: any): (string | number | symbol)[] {
|
||||
track(target, OperationTypes.ITERATE)
|
||||
return Reflect.ownKeys(target)
|
||||
}
|
||||
|
||||
export const mutableHandlers: ProxyHandler<any> = {
|
||||
get: (target: any, key: string | symbol, receiver: any) =>
|
||||
get(target, key, receiver, observable),
|
||||
set,
|
||||
deleteProperty,
|
||||
has,
|
||||
ownKeys
|
||||
}
|
||||
|
||||
export const immutableHandlers: ProxyHandler<any> = {
|
||||
get: (target: any, key: string | symbol, receiver: any) =>
|
||||
get(target, key, receiver, LOCKED ? immutable : observable),
|
||||
|
||||
set(target: any, key: string | symbol, value: any, receiver: any): boolean {
|
||||
if (LOCKED) {
|
||||
if (__DEV__) {
|
||||
console.warn(`Set operation failed: target is immutable.`, target)
|
||||
}
|
||||
return true
|
||||
} else {
|
||||
return set(target, key, value, receiver)
|
||||
}
|
||||
},
|
||||
|
||||
deleteProperty(target: any, key: string | symbol): boolean {
|
||||
if (LOCKED) {
|
||||
if (__DEV__) {
|
||||
console.warn(`Delete operation failed: target is immutable.`, target)
|
||||
}
|
||||
return true
|
||||
} else {
|
||||
return deleteProperty(target, key)
|
||||
}
|
||||
},
|
||||
|
||||
has,
|
||||
ownKeys
|
||||
}
|
161
packages/observer/src/collectionHandlers.ts
Normal file
161
packages/observer/src/collectionHandlers.ts
Normal file
@ -0,0 +1,161 @@
|
||||
import { unwrap } from './index'
|
||||
import { track, trigger } from './autorun'
|
||||
import { OperationTypes } from './operations'
|
||||
|
||||
function instrument(
|
||||
target: any,
|
||||
key: string | symbol,
|
||||
args: any[],
|
||||
type: OperationTypes
|
||||
) {
|
||||
target = unwrap(target)
|
||||
const proto: any = Reflect.getPrototypeOf(target)
|
||||
track(target, type)
|
||||
return proto[key].apply(target, args)
|
||||
}
|
||||
|
||||
function get(key: string | symbol) {
|
||||
return instrument(this, key, [key], OperationTypes.GET)
|
||||
}
|
||||
|
||||
function has(key: string | symbol): boolean {
|
||||
return instrument(this, key, [key], OperationTypes.HAS)
|
||||
}
|
||||
|
||||
function size(target: any) {
|
||||
target = unwrap(target)
|
||||
const proto = Reflect.getPrototypeOf(target)
|
||||
track(target, OperationTypes.ITERATE)
|
||||
return Reflect.get(proto, 'size', target)
|
||||
}
|
||||
|
||||
function makeWarning(type: OperationTypes) {
|
||||
return function() {
|
||||
if (__DEV__) {
|
||||
console.warn(
|
||||
`${type} operation failed: target is immutable.`,
|
||||
unwrap(this)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const mutableInstrumentations: any = {
|
||||
get,
|
||||
has,
|
||||
|
||||
get size() {
|
||||
return size(this)
|
||||
},
|
||||
|
||||
add(key: any) {
|
||||
const target = unwrap(this)
|
||||
const proto: any = Reflect.getPrototypeOf(this)
|
||||
const hadKey = proto.has.call(target, key)
|
||||
const result = proto.add.apply(target, arguments)
|
||||
if (!hadKey) {
|
||||
if (__DEV__) {
|
||||
trigger(target, OperationTypes.ADD, key, { value: key })
|
||||
} else {
|
||||
trigger(target, OperationTypes.ADD, key)
|
||||
}
|
||||
}
|
||||
return result
|
||||
},
|
||||
|
||||
set(key: any, value: any) {
|
||||
const target = unwrap(this)
|
||||
const proto: any = Reflect.getPrototypeOf(this)
|
||||
const hadKey = proto.has.call(target, key)
|
||||
const oldValue = proto.get.call(target, key)
|
||||
const result = proto.set.apply(target, arguments)
|
||||
if (__DEV__) {
|
||||
const extraInfo = { oldValue, newValue: value }
|
||||
if (!hadKey) {
|
||||
trigger(target, OperationTypes.ADD, key, extraInfo)
|
||||
} else {
|
||||
trigger(target, OperationTypes.SET, key, extraInfo)
|
||||
}
|
||||
} else {
|
||||
if (!hadKey) {
|
||||
trigger(target, OperationTypes.ADD, key)
|
||||
} else {
|
||||
trigger(target, OperationTypes.SET, key)
|
||||
}
|
||||
}
|
||||
return result
|
||||
},
|
||||
|
||||
delete(key: any) {
|
||||
const target = unwrap(this)
|
||||
const proto: any = Reflect.getPrototypeOf(this)
|
||||
const hadKey = proto.has.call(target, key)
|
||||
const oldValue = proto.get ? proto.get.call(target, key) : undefined
|
||||
// forward the operation before queueing reactions
|
||||
const result = proto.delete.apply(target, arguments)
|
||||
if (hadKey) {
|
||||
if (__DEV__) {
|
||||
trigger(target, OperationTypes.DELETE, key, { oldValue })
|
||||
} else {
|
||||
trigger(target, OperationTypes.DELETE, key)
|
||||
}
|
||||
}
|
||||
return result
|
||||
},
|
||||
|
||||
clear() {
|
||||
const target = unwrap(this)
|
||||
const proto: any = Reflect.getPrototypeOf(this)
|
||||
const hadItems = target.size !== 0
|
||||
const oldTarget = target instanceof Map ? new Map(target) : new Set(target)
|
||||
// forward the operation before queueing reactions
|
||||
const result = proto.clear.apply(target, arguments)
|
||||
if (hadItems) {
|
||||
if (__DEV__) {
|
||||
trigger(target, OperationTypes.CLEAR, void 0, { oldTarget })
|
||||
} else {
|
||||
trigger(target, OperationTypes.CLEAR)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
const immutableInstrumentations: any = {
|
||||
get,
|
||||
has,
|
||||
get size() {
|
||||
return size(this)
|
||||
},
|
||||
add: makeWarning(OperationTypes.ADD),
|
||||
set: makeWarning(OperationTypes.SET),
|
||||
delete: makeWarning(OperationTypes.DELETE),
|
||||
clear: makeWarning(OperationTypes.CLEAR)
|
||||
}
|
||||
;['forEach', 'keys', 'values', 'entries', Symbol.iterator].forEach(key => {
|
||||
mutableInstrumentations[key] = immutableInstrumentations[key] = function(
|
||||
...args: any[]
|
||||
) {
|
||||
return instrument(this, key, args, OperationTypes.ITERATE)
|
||||
}
|
||||
})
|
||||
|
||||
function getInstrumented(
|
||||
target: any,
|
||||
key: string | symbol,
|
||||
receiver: any,
|
||||
instrumentations: any
|
||||
) {
|
||||
target = instrumentations.hasOwnProperty(key) ? instrumentations : target
|
||||
return Reflect.get(target, key, receiver)
|
||||
}
|
||||
|
||||
export const mutableCollectionHandlers: ProxyHandler<any> = {
|
||||
get: (target: any, key: string | symbol, receiver: any) =>
|
||||
getInstrumented(target, key, receiver, mutableInstrumentations)
|
||||
}
|
||||
|
||||
export const immutableCollectionHandlers: ProxyHandler<any> = {
|
||||
get: (target: any, key: string | symbol, receiver: any) =>
|
||||
getInstrumented(target, key, receiver, immutableInstrumentations)
|
||||
}
|
44
packages/observer/src/computed.ts
Normal file
44
packages/observer/src/computed.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import { autorun, stop } from './index'
|
||||
import { Autorun, activeAutorunStack } from './autorun'
|
||||
|
||||
export interface ComputedGetter {
|
||||
(): any
|
||||
stop: () => void
|
||||
}
|
||||
|
||||
export function computed(getter: Function, context?: any): ComputedGetter {
|
||||
let dirty: boolean = true
|
||||
let value: any = undefined
|
||||
const runner = autorun(() => getter.call(context, context), {
|
||||
lazy: true,
|
||||
scheduler: () => {
|
||||
dirty = true
|
||||
}
|
||||
})
|
||||
const computedGetter = (() => {
|
||||
if (dirty) {
|
||||
value = runner()
|
||||
dirty = false
|
||||
}
|
||||
// When computed autoruns are accessed in a parent autorun, the parent
|
||||
// should track all the dependencies the computed property has tracked.
|
||||
// This should also apply for chained computed properties.
|
||||
trackChildRun(runner)
|
||||
return value
|
||||
}) as ComputedGetter
|
||||
computedGetter.stop = () => stop(runner)
|
||||
return computedGetter
|
||||
}
|
||||
|
||||
function trackChildRun(childRunner: Autorun) {
|
||||
const parentRunner = activeAutorunStack[activeAutorunStack.length - 1]
|
||||
if (parentRunner) {
|
||||
for (let i = 0; i < childRunner.deps.length; i++) {
|
||||
const dep = childRunner.deps[i]
|
||||
if (!dep.has(parentRunner)) {
|
||||
dep.add(parentRunner)
|
||||
parentRunner.deps.push(dep)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
152
packages/observer/src/index.ts
Normal file
152
packages/observer/src/index.ts
Normal file
@ -0,0 +1,152 @@
|
||||
import { mutableHandlers, immutableHandlers } from './baseHandlers'
|
||||
|
||||
import {
|
||||
mutableCollectionHandlers,
|
||||
immutableCollectionHandlers
|
||||
} from './collectionHandlers'
|
||||
|
||||
import {
|
||||
targetMap,
|
||||
observedToRaw,
|
||||
rawToObserved,
|
||||
immutableToRaw,
|
||||
rawToImmutable,
|
||||
immutableValues,
|
||||
nonReactiveValues
|
||||
} from './state'
|
||||
|
||||
import {
|
||||
createAutorun,
|
||||
cleanup,
|
||||
Autorun,
|
||||
AutorunOptions,
|
||||
DebuggerEvent
|
||||
} from './autorun'
|
||||
|
||||
export { Autorun, DebuggerEvent }
|
||||
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)\]$/
|
||||
|
||||
const canObserve = (value: any): boolean => {
|
||||
return (
|
||||
!value._isVue &&
|
||||
!value._isVNode &&
|
||||
observableValueRE.test(Object.prototype.toString.call(value)) &&
|
||||
!nonReactiveValues.has(value)
|
||||
)
|
||||
}
|
||||
|
||||
type identity = <T>(target: T) => T
|
||||
|
||||
export const observable = ((target: any = {}): any => {
|
||||
// if trying to observe an immutable proxy, return the immutable version.
|
||||
if (immutableToRaw.has(target)) {
|
||||
return target
|
||||
}
|
||||
// target is explicitly marked as immutable by user
|
||||
if (immutableValues.has(target)) {
|
||||
return immutable(target)
|
||||
}
|
||||
return createObservable(
|
||||
target,
|
||||
rawToObserved,
|
||||
observedToRaw,
|
||||
mutableHandlers,
|
||||
mutableCollectionHandlers
|
||||
)
|
||||
}) as identity
|
||||
|
||||
export const immutable = ((target: any = {}): any => {
|
||||
// value is a mutable observable, retrive its original and return
|
||||
// a readonly version.
|
||||
if (observedToRaw.has(target)) {
|
||||
target = observedToRaw.get(target)
|
||||
}
|
||||
return createObservable(
|
||||
target,
|
||||
rawToImmutable,
|
||||
immutableToRaw,
|
||||
immutableHandlers,
|
||||
immutableCollectionHandlers
|
||||
)
|
||||
}) as identity
|
||||
|
||||
function createObservable(
|
||||
target: any,
|
||||
toProxy: WeakMap<any, any>,
|
||||
toRaw: WeakMap<any, any>,
|
||||
baseHandlers: ProxyHandler<any>,
|
||||
collectionHandlers: ProxyHandler<any>
|
||||
) {
|
||||
if ((__DEV__ && target === null) || typeof target !== 'object') {
|
||||
throw new Error(`value is not observable: ${String(target)}`)
|
||||
}
|
||||
// target already has corresponding Proxy
|
||||
let observed = toProxy.get(target)
|
||||
if (observed !== void 0) {
|
||||
return observed
|
||||
}
|
||||
// target is already a Proxy
|
||||
if (toRaw.has(target)) {
|
||||
return target
|
||||
}
|
||||
// only a whitelist of value types can be observed.
|
||||
if (!canObserve(target)) {
|
||||
return target
|
||||
}
|
||||
const handlers = collectionTypes.has(target.constructor)
|
||||
? collectionHandlers
|
||||
: baseHandlers
|
||||
observed = new Proxy(target, handlers)
|
||||
toProxy.set(target, observed)
|
||||
toRaw.set(observed, target)
|
||||
targetMap.set(target, new Map())
|
||||
return observed
|
||||
}
|
||||
|
||||
export function autorun(
|
||||
fn: Function,
|
||||
options: AutorunOptions = EMPTY_OBJ
|
||||
): Autorun {
|
||||
if ((fn as Autorun).isAutorun) {
|
||||
fn = (fn as Autorun).raw
|
||||
}
|
||||
const runner = createAutorun(fn, options)
|
||||
if (!options.lazy) {
|
||||
runner()
|
||||
}
|
||||
return runner
|
||||
}
|
||||
|
||||
export function stop(runner: Autorun) {
|
||||
if (runner.active) {
|
||||
cleanup(runner)
|
||||
runner.active = false
|
||||
}
|
||||
}
|
||||
|
||||
export function isObservable(value: any): boolean {
|
||||
return observedToRaw.has(value) || immutableToRaw.has(value)
|
||||
}
|
||||
|
||||
export function isImmutable(value: any): boolean {
|
||||
return immutableToRaw.has(value)
|
||||
}
|
||||
|
||||
export function unwrap<T>(observed: T): T {
|
||||
return observedToRaw.get(observed) || immutableToRaw.get(observed) || observed
|
||||
}
|
||||
|
||||
export function markImmutable<T>(value: T): T {
|
||||
immutableValues.add(value)
|
||||
return value
|
||||
}
|
||||
|
||||
export function markNonReactive<T>(value: T): T {
|
||||
nonReactiveValues.add(value)
|
||||
return value
|
||||
}
|
10
packages/observer/src/lock.ts
Normal file
10
packages/observer/src/lock.ts
Normal file
@ -0,0 +1,10 @@
|
||||
// global immutability lock
|
||||
export let LOCKED = true
|
||||
|
||||
export function lock() {
|
||||
LOCKED = true
|
||||
}
|
||||
|
||||
export function unlock() {
|
||||
LOCKED = false
|
||||
}
|
11
packages/observer/src/operations.ts
Normal file
11
packages/observer/src/operations.ts
Normal file
@ -0,0 +1,11 @@
|
||||
export const enum OperationTypes {
|
||||
// using literal strings instead of numbers so that it's easier to inspect
|
||||
// debugger events
|
||||
SET = 'set',
|
||||
ADD = 'add',
|
||||
DELETE = 'delete',
|
||||
CLEAR = 'clear',
|
||||
GET = 'get',
|
||||
HAS = 'has',
|
||||
ITERATE = 'iterate'
|
||||
}
|
20
packages/observer/src/state.ts
Normal file
20
packages/observer/src/state.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { Autorun } from './autorun'
|
||||
|
||||
// The main WeakMap that stores {target -> key -> dep} connections.
|
||||
// Conceptually, it's easier to think of a dependency as a Dep class
|
||||
// which maintains a Set of subscribers, but we simply store them as
|
||||
// raw Sets to reduce memory overhead.
|
||||
export type Dep = Set<Autorun>
|
||||
export type KeyToDepMap = Map<string | symbol, Dep>
|
||||
export const targetMap: WeakMap<any, KeyToDepMap> = new WeakMap()
|
||||
|
||||
// WeakMaps that store {raw <-> observed} pairs.
|
||||
export const rawToObserved: WeakMap<any, any> = new WeakMap()
|
||||
export const observedToRaw: WeakMap<any, any> = new WeakMap()
|
||||
export const rawToImmutable: WeakMap<any, any> = new WeakMap()
|
||||
export const immutableToRaw: WeakMap<any, any> = new WeakMap()
|
||||
|
||||
// WeakSets for values that are marked immutable or non-reactive during
|
||||
// observable creation.
|
||||
export const immutableValues: WeakSet<any> = new WeakSet()
|
||||
export const nonReactiveValues: WeakSet<any> = new WeakSet()
|
3
packages/runtime-dom/.npmignore
Normal file
3
packages/runtime-dom/.npmignore
Normal file
@ -0,0 +1,3 @@
|
||||
__tests__/
|
||||
__mocks__/
|
||||
dist/packages
|
21
packages/runtime-dom/README.md
Normal file
21
packages/runtime-dom/README.md
Normal file
@ -0,0 +1,21 @@
|
||||
# @vue/runtime-dom
|
||||
|
||||
``` js
|
||||
import { h, render, Component } from '@vue/runtime-dom'
|
||||
|
||||
class App extends Component {
|
||||
data () {
|
||||
return {
|
||||
msg: 'Hello World!'
|
||||
}
|
||||
}
|
||||
render () {
|
||||
return h('div', this.msg)
|
||||
}
|
||||
}
|
||||
|
||||
render(
|
||||
h(App),
|
||||
document.getElementById('app')
|
||||
)
|
||||
```
|
7
packages/runtime-dom/index.js
Normal file
7
packages/runtime-dom/index.js
Normal file
@ -0,0 +1,7 @@
|
||||
'use strict'
|
||||
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
module.exports = require('./dist/runtime-dom.cjs.prod.js')
|
||||
} else {
|
||||
module.exports = require('./dist/runtime-dom.cjs.js')
|
||||
}
|
30
packages/runtime-dom/package.json
Normal file
30
packages/runtime-dom/package.json
Normal file
@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "@vue/runtime-dom",
|
||||
"version": "3.0.0-alpha.1",
|
||||
"description": "@vue/runtime-dom",
|
||||
"main": "index.js",
|
||||
"module": "dist/runtime-dom.esm.js",
|
||||
"typings": "dist/index.d.ts",
|
||||
"unpkg": "dist/runtime-dom.umd.js",
|
||||
"buildOptions": {
|
||||
"name": "Vue",
|
||||
"formats": ["esm", "cjs", "umd", "esm-browser"]
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/vuejs/vue.git"
|
||||
},
|
||||
"keywords": [
|
||||
"vue"
|
||||
],
|
||||
"author": "Evan You",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/vuejs/vue/issues"
|
||||
},
|
||||
"homepage": "https://github.com/vuejs/vue/tree/dev/packages/runtime-dom#readme",
|
||||
"dependencies": {
|
||||
"@vue/core": "3.0.0-alpha.1",
|
||||
"@vue/scheduler": "3.0.0-alpha.1"
|
||||
}
|
||||
}
|
39
packages/runtime-dom/src/index.ts
Normal file
39
packages/runtime-dom/src/index.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import {
|
||||
h,
|
||||
cloneVNode,
|
||||
createPortal,
|
||||
Component,
|
||||
createRenderer
|
||||
} from '@vue/core'
|
||||
|
||||
import { queueJob, nextTick } from '@vue/scheduler'
|
||||
|
||||
import { nodeOps } from './nodeOps'
|
||||
import { patchData } from './patchData'
|
||||
import { teardownVNode } from './teardownVNode'
|
||||
|
||||
const { render } = createRenderer({
|
||||
queueJob,
|
||||
nodeOps,
|
||||
patchData,
|
||||
teardownVNode
|
||||
})
|
||||
|
||||
// important: inline the definition for nextTick
|
||||
const publicNextTick = nextTick as (fn: Function) => Promise<void>
|
||||
|
||||
export { h, cloneVNode, createPortal, Component, render, publicNextTick as nextTick }
|
||||
|
||||
// also expose observer API
|
||||
export {
|
||||
autorun,
|
||||
stop,
|
||||
observable,
|
||||
immutable,
|
||||
computed,
|
||||
isObservable,
|
||||
isImmutable,
|
||||
markImmutable,
|
||||
markNonReactive,
|
||||
unwrap
|
||||
} from '@vue/core'
|
31
packages/runtime-dom/src/modules/attrs.ts
Normal file
31
packages/runtime-dom/src/modules/attrs.ts
Normal file
@ -0,0 +1,31 @@
|
||||
export function patchAttr(
|
||||
el: Element,
|
||||
key: string,
|
||||
value: any,
|
||||
isSVG: boolean
|
||||
) {
|
||||
// isSVG short-circuits isXlink check
|
||||
if (isSVG && isXlink(key)) {
|
||||
if (value == null) {
|
||||
el.removeAttributeNS(xlinkNS, getXlinkProp(key))
|
||||
} else {
|
||||
el.setAttributeNS(xlinkNS, key, value)
|
||||
}
|
||||
} else {
|
||||
if (value == null) {
|
||||
el.removeAttribute(key)
|
||||
} else {
|
||||
el.setAttribute(key, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const xlinkNS = 'http://www.w3.org/1999/xlink'
|
||||
|
||||
function isXlink(name: string): boolean {
|
||||
return name.charAt(5) === ':' && name.slice(0, 5) === 'xlink'
|
||||
}
|
||||
|
||||
function getXlinkProp(name: string): string {
|
||||
return isXlink(name) ? name.slice(6, name.length) : ''
|
||||
}
|
29
packages/runtime-dom/src/modules/class.ts
Normal file
29
packages/runtime-dom/src/modules/class.ts
Normal file
@ -0,0 +1,29 @@
|
||||
// compiler should normlaize class + :class bindings on the same element
|
||||
// into a single binding ['staticClass', dynamic]
|
||||
|
||||
export function patchClass(el: Element, value: any, isSVG: boolean) {
|
||||
// directly setting className should be faster than setAttribute in theory
|
||||
if (isSVG) {
|
||||
el.setAttribute('class', normalizeClass(value))
|
||||
} else {
|
||||
el.className = normalizeClass(value)
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
142
packages/runtime-dom/src/modules/events.ts
Normal file
142
packages/runtime-dom/src/modules/events.ts
Normal file
@ -0,0 +1,142 @@
|
||||
const delegateRE = /^(?:click|dblclick|submit|(?:key|mouse|touch).*)$/
|
||||
|
||||
type EventValue = Function | Function[]
|
||||
type TargetRef = { el: Element | Document }
|
||||
|
||||
export function patchEvent(
|
||||
el: Element,
|
||||
name: string,
|
||||
prevValue: EventValue | null,
|
||||
nextValue: EventValue | null
|
||||
) {
|
||||
if (delegateRE.test(name)) {
|
||||
handleDelegatedEvent(el, name, nextValue)
|
||||
} else {
|
||||
handleNormalEvent(el, name, prevValue, nextValue)
|
||||
}
|
||||
}
|
||||
|
||||
const eventCounts: Record<string, number> = {}
|
||||
const attachedGlobalHandlers: Record<string, Function> = {}
|
||||
|
||||
export function handleDelegatedEvent(
|
||||
el: any,
|
||||
name: string,
|
||||
value: EventValue | null
|
||||
) {
|
||||
const count = eventCounts[name]
|
||||
let store = el.__events
|
||||
if (value) {
|
||||
if (!count) {
|
||||
attachGlobalHandler(name)
|
||||
}
|
||||
if (!store) {
|
||||
store = el.__events = {}
|
||||
}
|
||||
if (!store[name]) {
|
||||
eventCounts[name]++
|
||||
}
|
||||
store[name] = value
|
||||
} else if (store && store[name]) {
|
||||
eventCounts[name]--
|
||||
store[name] = null
|
||||
if (count === 1) {
|
||||
removeGlobalHandler(name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function attachGlobalHandler(name: string) {
|
||||
const handler = (attachedGlobalHandlers[name] = (e: Event) => {
|
||||
const { type } = e
|
||||
const isClick = type === 'click' || type === 'dblclick'
|
||||
if (isClick && (e as MouseEvent).button !== 0) {
|
||||
e.stopPropagation()
|
||||
return false
|
||||
}
|
||||
e.stopPropagation = stopPropagation
|
||||
const targetRef: TargetRef = { el: document }
|
||||
Object.defineProperty(e, 'currentTarget', {
|
||||
configurable: true,
|
||||
get() {
|
||||
return targetRef.el
|
||||
}
|
||||
})
|
||||
dispatchEvent(e, name, isClick, targetRef)
|
||||
})
|
||||
document.addEventListener(name, handler)
|
||||
eventCounts[name] = 0
|
||||
}
|
||||
|
||||
function stopPropagation() {
|
||||
this.cancelBubble = true
|
||||
if (!this.immediatePropagationStopped) {
|
||||
this.stopImmediatePropagation()
|
||||
}
|
||||
}
|
||||
|
||||
function dispatchEvent(
|
||||
e: Event,
|
||||
name: string,
|
||||
isClick: boolean,
|
||||
targetRef: TargetRef
|
||||
) {
|
||||
let el = e.target as any
|
||||
while (el != null) {
|
||||
// Don't process clicks on disabled elements
|
||||
if (isClick && el.disabled) {
|
||||
break
|
||||
}
|
||||
const store = el.__events
|
||||
if (store) {
|
||||
const value = store[name]
|
||||
if (value) {
|
||||
targetRef.el = el
|
||||
invokeEvents(e, value)
|
||||
if (e.cancelBubble) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
el = el.parentNode
|
||||
}
|
||||
}
|
||||
|
||||
function invokeEvents(e: Event, value: EventValue) {
|
||||
if (Array.isArray(value)) {
|
||||
for (let i = 0; i < value.length; i++) {
|
||||
value[i](e)
|
||||
}
|
||||
} else {
|
||||
value(e)
|
||||
}
|
||||
}
|
||||
|
||||
function removeGlobalHandler(name: string) {
|
||||
document.removeEventListener(name, attachedGlobalHandlers[name] as any)
|
||||
eventCounts[name] = 0
|
||||
}
|
||||
|
||||
function handleNormalEvent(el: Element, name: string, prev: any, next: any) {
|
||||
const invoker = prev && prev.invoker
|
||||
if (next) {
|
||||
if (invoker) {
|
||||
prev.invoker = null
|
||||
invoker.value = next
|
||||
next.invoker = invoker
|
||||
} else {
|
||||
el.addEventListener(name, createInvoker(next))
|
||||
}
|
||||
} else if (invoker) {
|
||||
el.removeEventListener(name, invoker)
|
||||
}
|
||||
}
|
||||
|
||||
function createInvoker(value: any) {
|
||||
const invoker = ((e: Event) => {
|
||||
invokeEvents(e, invoker.value)
|
||||
}) as any
|
||||
invoker.value = value
|
||||
value.invoker = invoker
|
||||
return invoker
|
||||
}
|
18
packages/runtime-dom/src/modules/props.ts
Normal file
18
packages/runtime-dom/src/modules/props.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { VNode, ChildrenFlags } from '@vue/core'
|
||||
|
||||
export function patchDOMProp(
|
||||
el: any,
|
||||
key: string,
|
||||
value: any,
|
||||
prevVNode: VNode,
|
||||
unmountChildren: any
|
||||
) {
|
||||
if (key === 'innerHTML' || key === 'textContent') {
|
||||
if (prevVNode && prevVNode.children) {
|
||||
unmountChildren(prevVNode.children, prevVNode.childFlags)
|
||||
prevVNode.children = null
|
||||
prevVNode.childFlags = ChildrenFlags.NO_CHILDREN
|
||||
}
|
||||
}
|
||||
el[key] = value
|
||||
}
|
54
packages/runtime-dom/src/modules/style.ts
Normal file
54
packages/runtime-dom/src/modules/style.ts
Normal file
@ -0,0 +1,54 @@
|
||||
import { isObservable } from '@vue/core'
|
||||
|
||||
// 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
|
||||
|
||||
export function patchStyle(el: any, prev: any, next: any, data: any) {
|
||||
// If next is observed, the user is likely to mutate the style object.
|
||||
// We need to normalize + clone it and replace data.style with the clone.
|
||||
if (isObservable(next)) {
|
||||
data.style = normalizeStyle(next)
|
||||
}
|
||||
|
||||
const { style } = el
|
||||
if (!next) {
|
||||
el.removeAttribute('style')
|
||||
} else if (typeof next === 'string') {
|
||||
style.cssText = next
|
||||
} else {
|
||||
// TODO: warn invalid value in dev
|
||||
next = normalizeStyle(next)
|
||||
for (const key in next) {
|
||||
let value = next[key]
|
||||
if (typeof value === 'number' && !nonNumericRE.test(key)) {
|
||||
value = value + 'px'
|
||||
}
|
||||
style.setProperty(key, value)
|
||||
}
|
||||
if (prev && typeof prev !== 'string') {
|
||||
prev = normalizeStyle(prev)
|
||||
for (const key in prev) {
|
||||
if (!next[key]) {
|
||||
style.setProperty(key, '')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeStyle(value: any): Record<string, string | number> | void {
|
||||
if (value && typeof value === 'object') {
|
||||
return value
|
||||
} else 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
|
||||
}
|
||||
}
|
39
packages/runtime-dom/src/nodeOps.ts
Normal file
39
packages/runtime-dom/src/nodeOps.ts
Normal file
@ -0,0 +1,39 @@
|
||||
const svgNS = 'http://www.w3.org/2000/svg'
|
||||
|
||||
export const nodeOps = {
|
||||
createElement: (tag: string, isSVG?: boolean): Element =>
|
||||
isSVG ? document.createElementNS(svgNS, tag) : document.createElement(tag),
|
||||
|
||||
createText: (text: string): Text => document.createTextNode(text),
|
||||
|
||||
setText: (node: Text, text: string) => {
|
||||
node.nodeValue = text
|
||||
},
|
||||
|
||||
appendChild: (parent: Node, child: Node) => {
|
||||
parent.appendChild(child)
|
||||
},
|
||||
|
||||
insertBefore: (parent: Node, child: Node, ref: Node) => {
|
||||
parent.insertBefore(child, ref)
|
||||
},
|
||||
|
||||
replaceChild: (parent: Node, oldChild: Node, newChild: Node) => {
|
||||
parent.replaceChild(newChild, oldChild)
|
||||
},
|
||||
|
||||
removeChild: (parent: Node, child: Node) => {
|
||||
parent.removeChild(child)
|
||||
},
|
||||
|
||||
clearContent: (node: Node) => {
|
||||
node.textContent = ''
|
||||
},
|
||||
|
||||
parentNode: (node: Node): Node | null => node.parentNode,
|
||||
|
||||
nextSibling: (node: Node): Node | null => node.nextSibling,
|
||||
|
||||
querySelector: (selector: string): Node | null =>
|
||||
document.querySelector(selector)
|
||||
}
|
42
packages/runtime-dom/src/patchData.ts
Normal file
42
packages/runtime-dom/src/patchData.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import { VNode } from '@vue/core'
|
||||
import { patchClass } from './modules/class'
|
||||
import { patchStyle } from './modules/style'
|
||||
import { patchAttr } from './modules/attrs'
|
||||
import { patchDOMProp } from './modules/props'
|
||||
import { patchEvent } from './modules/events'
|
||||
|
||||
export function patchData(
|
||||
el: Element,
|
||||
key: string,
|
||||
prevValue: any,
|
||||
nextValue: any,
|
||||
prevVNode: VNode,
|
||||
nextVNode: VNode,
|
||||
isSVG: boolean,
|
||||
unmountChildren: any
|
||||
) {
|
||||
switch (key) {
|
||||
// special
|
||||
case 'class':
|
||||
patchClass(el, nextValue, isSVG)
|
||||
break
|
||||
case 'style':
|
||||
patchStyle(el, prevValue, nextValue, nextVNode.data)
|
||||
break
|
||||
default:
|
||||
if (key.startsWith('on')) {
|
||||
patchEvent(el, key.toLowerCase().slice(2), prevValue, nextValue)
|
||||
} else if (key.startsWith('domProps')) {
|
||||
patchDOMProp(
|
||||
el,
|
||||
key[8].toLowerCase() + key.slice(9),
|
||||
nextValue,
|
||||
prevVNode,
|
||||
unmountChildren
|
||||
)
|
||||
} else {
|
||||
patchAttr(el, key, nextValue, isSVG)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
13
packages/runtime-dom/src/teardownVNode.ts
Normal file
13
packages/runtime-dom/src/teardownVNode.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { VNode } from '@vue/core'
|
||||
import { handleDelegatedEvent } from './modules/events'
|
||||
|
||||
export function teardownVNode(vnode: VNode) {
|
||||
const { el, data } = vnode
|
||||
if (data != null) {
|
||||
for (const key in data) {
|
||||
if (key.startsWith('on')) {
|
||||
handleDelegatedEvent(el, key.toLowerCase().slice(2), null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
3
packages/scheduler/.npmignore
Normal file
3
packages/scheduler/.npmignore
Normal file
@ -0,0 +1,3 @@
|
||||
__tests__/
|
||||
__mocks__/
|
||||
dist/packages
|
3
packages/scheduler/README.md
Normal file
3
packages/scheduler/README.md
Normal file
@ -0,0 +1,3 @@
|
||||
# @vue/scheduler
|
||||
|
||||
> This package is published only for typing and building custom renderers. It is NOT meant to be used in applications.
|
7
packages/scheduler/index.js
Normal file
7
packages/scheduler/index.js
Normal file
@ -0,0 +1,7 @@
|
||||
'use strict'
|
||||
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
module.exports = require('./dist/scheduler.cjs.prod.js')
|
||||
} else {
|
||||
module.exports = require('./dist/scheduler.cjs.js')
|
||||
}
|
21
packages/scheduler/package.json
Normal file
21
packages/scheduler/package.json
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "@vue/scheduler",
|
||||
"version": "3.0.0-alpha.1",
|
||||
"description": "@vue/scheduler",
|
||||
"main": "index.js",
|
||||
"module": "dist/scheduler.esm.js",
|
||||
"typings": "dist/index.d.ts",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/vuejs/vue.git"
|
||||
},
|
||||
"keywords": [
|
||||
"vue"
|
||||
],
|
||||
"author": "Evan You",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/vuejs/vue/issues"
|
||||
},
|
||||
"homepage": "https://github.com/vuejs/vue/tree/dev/packages/scheduler#readme"
|
||||
}
|
40
packages/scheduler/src/index.ts
Normal file
40
packages/scheduler/src/index.ts
Normal file
@ -0,0 +1,40 @@
|
||||
const queue: Array<() => void> = []
|
||||
const postFlushCbs: Array<() => void> = []
|
||||
const p = Promise.resolve()
|
||||
let flushing = false
|
||||
|
||||
export function nextTick(fn: () => void) {
|
||||
p.then(fn)
|
||||
}
|
||||
|
||||
export function queueJob(job: () => void, postFlushCb?: () => void) {
|
||||
if (queue.indexOf(job) === -1) {
|
||||
if (flushing) {
|
||||
job()
|
||||
} else {
|
||||
queue.push(job)
|
||||
}
|
||||
}
|
||||
if (postFlushCb) {
|
||||
queuePostFlushCb(postFlushCb)
|
||||
}
|
||||
if (!flushing) {
|
||||
nextTick(flushJobs)
|
||||
}
|
||||
}
|
||||
|
||||
export function queuePostFlushCb(cb: () => void) {
|
||||
postFlushCbs.push(cb)
|
||||
}
|
||||
|
||||
export function flushJobs() {
|
||||
flushing = true
|
||||
let job
|
||||
while ((job = queue.shift())) {
|
||||
job()
|
||||
}
|
||||
while ((job = postFlushCbs.shift())) {
|
||||
job()
|
||||
}
|
||||
flushing = false
|
||||
}
|
147
rollup.config.js
Normal file
147
rollup.config.js
Normal file
@ -0,0 +1,147 @@
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const ts = require('rollup-plugin-typescript2')
|
||||
const replace = require('rollup-plugin-replace')
|
||||
const alias = require('rollup-plugin-alias')
|
||||
|
||||
if (!process.env.TARGET) {
|
||||
throw new Error('TARGET package must be specified via --environment flag.')
|
||||
}
|
||||
|
||||
const packagesDir = path.resolve(__dirname, 'packages')
|
||||
const packageDir = path.resolve(packagesDir, process.env.TARGET)
|
||||
const name = path.basename(packageDir)
|
||||
const resolve = p => path.resolve(packageDir, p)
|
||||
const pkg = require(resolve(`package.json`))
|
||||
const packageOptions = pkg.buildOptions || {}
|
||||
|
||||
// build aliases dynamically
|
||||
const aliasOptions = { resolve: ['.ts'] }
|
||||
fs.readdirSync(packagesDir).forEach(dir => {
|
||||
if (fs.statSync(path.resolve(packagesDir, dir)).isDirectory()) {
|
||||
aliasOptions[`@vue/${dir}`] = path.resolve(packagesDir, `${dir}/src/index`)
|
||||
}
|
||||
})
|
||||
const aliasPlugin = alias(aliasOptions)
|
||||
|
||||
// ensure TS checks only once for each build
|
||||
let hasTSChecked = false
|
||||
|
||||
const configs = {
|
||||
esm: {
|
||||
file: resolve(`dist/${name}.esm.js`),
|
||||
format: `es`
|
||||
},
|
||||
cjs: {
|
||||
file: resolve(`dist/${name}.cjs.js`),
|
||||
format: `cjs`
|
||||
},
|
||||
umd: {
|
||||
file: resolve(`dist/${name}.umd.js`),
|
||||
format: `umd`
|
||||
},
|
||||
'esm-browser': {
|
||||
file: resolve(`dist/${name}.esm-browser.js`),
|
||||
format: `es`
|
||||
}
|
||||
}
|
||||
|
||||
const defaultFormats = ['esm', 'cjs']
|
||||
const inlineFromats = process.env.FORMATS && process.env.FORMATS.split(',')
|
||||
const packageFormats = inlineFromats || packageOptions.formats || defaultFormats
|
||||
const packageConfigs = packageFormats.map(format => createConfig(configs[format]))
|
||||
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
packageFormats.forEach(format => {
|
||||
if (format === 'cjs') {
|
||||
packageConfigs.push(createProductionConfig(format))
|
||||
}
|
||||
if (format === 'umd' || format === 'esm-browser') {
|
||||
packageConfigs.push(createMinifiedConfig(format))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = packageConfigs
|
||||
|
||||
function createConfig(output, plugins = []) {
|
||||
const isProductionBuild = /\.prod\.js$/.test(output.file)
|
||||
const isUMDBuild = /\.umd(\.prod)?\.js$/.test(output.file)
|
||||
const isBunlderESMBuild = /\.esm\.js$/.test(output.file)
|
||||
const isBrowserESMBuild = /esm-browser(\.prod)?\.js$/.test(output.file)
|
||||
|
||||
if (isUMDBuild) {
|
||||
output.name = packageOptions.name
|
||||
}
|
||||
|
||||
const tsPlugin = ts({
|
||||
check: process.env.NODE_ENV === 'production' && !hasTSChecked,
|
||||
tsconfig: path.resolve(__dirname, 'tsconfig.json'),
|
||||
cacheRoot: path.resolve(__dirname, 'node_modules/.rts2_cache'),
|
||||
tsconfigOverride: {
|
||||
compilerOptions: {
|
||||
declaration: process.env.NODE_ENV === 'production' && !hasTSChecked
|
||||
}
|
||||
}
|
||||
})
|
||||
// we only need to check TS and generate declarations once for each build.
|
||||
// it also seems to run into weird issues when checking multiple times
|
||||
// during a single build.
|
||||
hasTSChecked = true
|
||||
|
||||
return {
|
||||
input: resolve(`src/index.ts`),
|
||||
// UMD and Browser ESM builds inlines everything so that they can be
|
||||
// used alone.
|
||||
external: isUMDBuild || isBrowserESMBuild
|
||||
? []
|
||||
: Object.keys(aliasOptions),
|
||||
plugins: [
|
||||
tsPlugin,
|
||||
aliasPlugin,
|
||||
createReplacePlugin(isProductionBuild, isBunlderESMBuild),
|
||||
...plugins
|
||||
],
|
||||
output,
|
||||
onwarn: (msg, warn) => {
|
||||
if (!/Circular/.test(msg)) {
|
||||
warn(msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function createReplacePlugin(isProduction, isBunlderESMBuild) {
|
||||
return replace({
|
||||
__DEV__: isBunlderESMBuild
|
||||
// preserve to be handled by bundlers
|
||||
? `process.env.NODE_ENV !== 'production'`
|
||||
// hard coded dev/prod builds
|
||||
: !isProduction,
|
||||
// compatibility builds
|
||||
__COMPAT__: !!process.env.COMPAT
|
||||
})
|
||||
}
|
||||
|
||||
function createProductionConfig(format) {
|
||||
return createConfig({
|
||||
file: resolve(`dist/${name}.${format}.prod.js`),
|
||||
format: /^esm/.test(format) ? 'es' : format
|
||||
})
|
||||
}
|
||||
|
||||
function createMinifiedConfig(format) {
|
||||
const { terser } = require('rollup-plugin-terser')
|
||||
const isESM = /^esm/.test(format)
|
||||
return createConfig(
|
||||
{
|
||||
file: resolve(`dist/${name}.${format}.prod.js`),
|
||||
format: isESM ? 'es' : format
|
||||
},
|
||||
[
|
||||
terser({
|
||||
module: isESM
|
||||
})
|
||||
]
|
||||
)
|
||||
}
|
75
scripts/bootstrap.js
vendored
Normal file
75
scripts/bootstrap.js
vendored
Normal file
@ -0,0 +1,75 @@
|
||||
// create package.json, README, etc. for packages that don't have them yet
|
||||
|
||||
const args = require('minimist')(process.argv.slice(2))
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const baseVersion = require('../lerna.json').version
|
||||
|
||||
const packagesDir = path.resolve(__dirname, '../packages')
|
||||
const files = fs.readdirSync(packagesDir)
|
||||
|
||||
files.forEach(shortName => {
|
||||
if (!fs.statSync(path.join(packagesDir, shortName)).isDirectory()) {
|
||||
return
|
||||
}
|
||||
|
||||
const name = shortName === `vue` ? shortName : `@vue/${shortName}`
|
||||
const pkgPath = path.join(packagesDir, shortName, `package.json`)
|
||||
if (args.force || !fs.existsSync(pkgPath)) {
|
||||
const json = {
|
||||
name,
|
||||
version: baseVersion,
|
||||
description: name,
|
||||
main: 'index.js',
|
||||
module: `dist/${shortName}.esm.js`,
|
||||
typings: 'dist/index.d.ts',
|
||||
repository: {
|
||||
type: 'git',
|
||||
url: 'git+https://github.com/vuejs/vue.git'
|
||||
},
|
||||
keywords: ['vue'],
|
||||
author: 'Evan You',
|
||||
license: 'MIT',
|
||||
bugs: {
|
||||
url: 'https://github.com/vuejs/vue/issues'
|
||||
},
|
||||
homepage: `https://github.com/vuejs/vue/tree/dev/packages/${shortName}#readme`
|
||||
}
|
||||
fs.writeFileSync(pkgPath, JSON.stringify(json, null, 2))
|
||||
}
|
||||
|
||||
const readmePath = path.join(packagesDir, shortName, `README.md`)
|
||||
if (args.force || !fs.existsSync(readmePath)) {
|
||||
fs.writeFileSync(readmePath, `# ${name}`)
|
||||
}
|
||||
|
||||
const npmIgnorePath = path.join(packagesDir, shortName, `.npmignore`)
|
||||
if (args.force || !fs.existsSync(npmIgnorePath)) {
|
||||
fs.writeFileSync(npmIgnorePath, `__tests__/\n__mocks__/\ndist/packages`)
|
||||
}
|
||||
|
||||
const srcDir = path.join(packagesDir, shortName, `src`)
|
||||
const indexPath = path.join(packagesDir, shortName, `src/index.ts`)
|
||||
if (args.force || !fs.existsSync(indexPath)) {
|
||||
if (!fs.existsSync(srcDir)) {
|
||||
fs.mkdirSync(srcDir)
|
||||
}
|
||||
fs.writeFileSync(indexPath, ``)
|
||||
}
|
||||
|
||||
const nodeIndexPath = path.join(packagesDir, shortName, 'index.js')
|
||||
if (args.force || !fs.existsSync(nodeIndexPath)) {
|
||||
fs.writeFileSync(
|
||||
nodeIndexPath,
|
||||
`
|
||||
'use strict'
|
||||
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
module.exports = require('./dist/${shortName}.cjs.prod.js')
|
||||
} else {
|
||||
module.exports = require('./dist/${shortName}.cjs.js')
|
||||
}
|
||||
`.trim() + '\n'
|
||||
)
|
||||
}
|
||||
})
|
70
scripts/build.js
Normal file
70
scripts/build.js
Normal file
@ -0,0 +1,70 @@
|
||||
const fs = require('fs-extra')
|
||||
const path = require('path')
|
||||
const zlib = require('zlib')
|
||||
const chalk = require('chalk')
|
||||
const execa = require('execa')
|
||||
const dts = require('dts-bundle')
|
||||
const { targets, fuzzyMatchTarget } = require('./utils')
|
||||
|
||||
const target = process.argv[2]
|
||||
|
||||
;(async () => {
|
||||
if (!target) {
|
||||
await buildAll(targets)
|
||||
checkAllSizes(targets)
|
||||
} else {
|
||||
await buildAll(fuzzyMatchTarget(target))
|
||||
checkAllSizes(fuzzyMatchTarget(target))
|
||||
}
|
||||
})()
|
||||
|
||||
async function buildAll (targets) {
|
||||
for (const target of targets) {
|
||||
await build(target)
|
||||
}
|
||||
}
|
||||
|
||||
async function build (target) {
|
||||
const pkgDir = path.resolve(`packages/${target}`)
|
||||
|
||||
await fs.remove(`${pkgDir}/dist`)
|
||||
|
||||
await execa('rollup', [
|
||||
'-c',
|
||||
'--environment',
|
||||
`NODE_ENV:production,TARGET:${target}`
|
||||
], { stdio: 'inherit' })
|
||||
|
||||
const dtsOptions = {
|
||||
name: target === 'vue' ? target : `@vue/${target}`,
|
||||
main: `${pkgDir}/dist/packages/${target}/src/index.d.ts`,
|
||||
out: `${pkgDir}/dist/index.d.ts`
|
||||
}
|
||||
dts.bundle(dtsOptions)
|
||||
console.log()
|
||||
console.log(chalk.blue(chalk.bold(`generated typings at ${dtsOptions.out}`)))
|
||||
|
||||
await fs.remove(`${pkgDir}/dist/packages`)
|
||||
}
|
||||
|
||||
function checkAllSizes (targets) {
|
||||
console.log()
|
||||
for (const target of targets) {
|
||||
checkSize(target)
|
||||
}
|
||||
console.log()
|
||||
}
|
||||
|
||||
function checkSize (target) {
|
||||
const pkgDir = path.resolve(`packages/${target}`)
|
||||
const esmProdBuild = `${pkgDir}/dist/${target}.esm-browser.prod.js`
|
||||
if (fs.existsSync(esmProdBuild)) {
|
||||
const file = fs.readFileSync(esmProdBuild)
|
||||
const minSize = (file.length / 1024).toFixed(2) + 'kb'
|
||||
const gzipped = zlib.gzipSync(file)
|
||||
const gzipSize = (gzipped.length / 1024).toFixed(2) + 'kb'
|
||||
console.log(`${
|
||||
chalk.gray(chalk.bold(target))
|
||||
} min:${minSize} / gzip:${gzipSize}`)
|
||||
}
|
||||
}
|
25
scripts/dev.js
Normal file
25
scripts/dev.js
Normal file
@ -0,0 +1,25 @@
|
||||
// Run Rollup in watch mode for a single package for development.
|
||||
// Only the ES modules format will be generated, as it is expected to be tested
|
||||
// in a modern browser using <script type="module">.
|
||||
// Defaults to watch the `vue` meta package.
|
||||
// To specific the package to watch, simply pass its name. e.g.
|
||||
// ```
|
||||
// yarn dev observer
|
||||
// ```
|
||||
|
||||
const execa = require('execa')
|
||||
const { targets, fuzzyMatchTarget } = require('./utils')
|
||||
|
||||
const target = fuzzyMatchTarget(process.argv[2] || 'runtime-dom')
|
||||
|
||||
execa(
|
||||
'rollup',
|
||||
[
|
||||
'-wc',
|
||||
'--environment',
|
||||
`TARGET:${target},FORMATS:umd`
|
||||
],
|
||||
{
|
||||
stdio: 'inherit'
|
||||
}
|
||||
)
|
19
scripts/utils.js
Normal file
19
scripts/utils.js
Normal file
@ -0,0 +1,19 @@
|
||||
const fs = require('fs')
|
||||
|
||||
const targets = exports.targets = fs.readdirSync('packages').filter(f => {
|
||||
return fs.statSync(`packages/${f}`).isDirectory()
|
||||
})
|
||||
|
||||
exports.fuzzyMatchTarget = partialTarget => {
|
||||
const matched = []
|
||||
for (const target of targets) {
|
||||
if (target.match(partialTarget)) {
|
||||
matched.push(target)
|
||||
}
|
||||
}
|
||||
if (matched.length) {
|
||||
return matched
|
||||
} else {
|
||||
throw new Error(`Target ${partialTarget} not found!`)
|
||||
}
|
||||
}
|
32
scripts/verifyCommit.js
Normal file
32
scripts/verifyCommit.js
Normal file
@ -0,0 +1,32 @@
|
||||
// Invoked on the commit-msg git hook by yorkie.
|
||||
|
||||
const chalk = require('chalk')
|
||||
const msgPath = process.env.GIT_PARAMS
|
||||
const msg = require('fs')
|
||||
.readFileSync(msgPath, 'utf-8')
|
||||
.trim()
|
||||
|
||||
const commitRE = /^(revert: )?(feat|fix|docs|style|refactor|perf|test|workflow|ci|chore|types)(\(.+\))?: .{1,50}/
|
||||
|
||||
if (!commitRE.test(msg)) {
|
||||
console.log()
|
||||
console.error(
|
||||
` ${chalk.bgRed.white(' ERROR ')} ${chalk.red(
|
||||
`invalid commit message format.`
|
||||
)}\n\n` +
|
||||
chalk.red(
|
||||
` Proper commit message format is required for automated changelog generation. Examples:\n\n`
|
||||
) +
|
||||
` ${chalk.green(`feat(compiler): add 'comments' option`)}\n` +
|
||||
` ${chalk.green(
|
||||
`fix(v-model): handle events on blur (close #28)`
|
||||
)}\n\n` +
|
||||
chalk.red(` See .github/COMMIT_CONVENTION.md for more details.\n`) +
|
||||
chalk.red(
|
||||
` You can also use ${chalk.cyan(
|
||||
`npm run commit`
|
||||
)} to interactively generate a commit message.\n`
|
||||
)
|
||||
)
|
||||
process.exit(1)
|
||||
}
|
31
tsconfig.json
Normal file
31
tsconfig.json
Normal file
@ -0,0 +1,31 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"outDir": "dist",
|
||||
"sourceMap": false,
|
||||
"target": "esnext",
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
// "declaration": true,
|
||||
"allowJs": false,
|
||||
"noUnusedLocals": true,
|
||||
"strictNullChecks": true,
|
||||
"noImplicitAny": true,
|
||||
"experimentalDecorators": true,
|
||||
"removeComments": false,
|
||||
"lib": [
|
||||
"esnext",
|
||||
"dom"
|
||||
],
|
||||
"rootDir": ".",
|
||||
"paths": {
|
||||
"@vue/core": ["packages/core/src"],
|
||||
"@vue/observer": ["packages/observer/src"],
|
||||
"@vue/scheduler": ["packages/scheduler/src"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"packages/global.d.ts",
|
||||
"packages/*/src"
|
||||
]
|
||||
}
|
Loading…
Reference in New Issue
Block a user