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