refactor: remove experimental hooks

This commit is contained in:
Evan You 2019-02-26 18:10:08 -05:00
parent 77dae71062
commit 2f165c1e87
8 changed files with 3 additions and 362 deletions

View File

@ -1,111 +0,0 @@
import { useState, h, nextTick, useEffect, Component } from '../src'
import { renderInstance, serialize, triggerEvent } from '@vue/runtime-test'
describe('hooks', () => {
it('useState', async () => {
class Counter extends Component {
render() {
const [count, setCount] = useState(0)
return h(
'div',
{
onClick: () => {
setCount(count + 1)
}
},
count
)
}
}
const counter = await renderInstance(Counter)
expect(serialize(counter.$el)).toBe(`<div>0</div>`)
triggerEvent(counter.$el, 'click')
await nextTick()
expect(serialize(counter.$el)).toBe(`<div>1</div>`)
})
it('should be usable via hooks() method', async () => {
class Counter extends Component {
hooks() {
const [count, setCount] = useState(0)
return {
count,
setCount
}
}
render() {
const { count, setCount } = this as any
return h(
'div',
{
onClick: () => {
setCount(count + 1)
}
},
count
)
}
}
const counter = await renderInstance(Counter)
expect(serialize(counter.$el)).toBe(`<div>0</div>`)
triggerEvent(counter.$el, 'click')
await nextTick()
expect(serialize(counter.$el)).toBe(`<div>1</div>`)
})
it('useEffect', async () => {
let effect = -1
class Counter extends Component {
render() {
const [count, setCount] = useState(0)
useEffect(() => {
effect = count
})
return h(
'div',
{
onClick: () => {
setCount(count + 1)
}
},
count
)
}
}
const counter = await renderInstance(Counter)
expect(effect).toBe(0)
triggerEvent(counter.$el, 'click')
await nextTick()
expect(effect).toBe(1)
})
it('useEffect with empty keys', async () => {
// TODO
})
it('useEffect with keys', async () => {
// TODO
})
it('useData', () => {
// TODO
})
it('useMounted/useUnmounted/useUpdated', () => {
// TODO
})
it('useWatch', () => {
// TODO
})
it('useComputed', () => {
// TODO
})
})

View File

@ -45,7 +45,6 @@ interface PublicInstanceMethods {
export interface APIMethods<P = {}, D = {}> { export interface APIMethods<P = {}, D = {}> {
data(): Partial<D> data(): Partial<D>
hooks(): any
render(props: Readonly<P>, slots: Slots, attrs: Data, parentVNode: VNode): any render(props: Readonly<P>, slots: Slots, attrs: Data, parentVNode: VNode): any
} }
@ -136,7 +135,6 @@ class InternalComponent implements PublicInstanceMethods {
_queueJob: ((fn: () => void) => void) | null = null _queueJob: ((fn: () => void) => void) | null = null
_isVue: boolean = true _isVue: boolean = true
_inactiveRoot: boolean = false _inactiveRoot: boolean = false
_hookProps: any = null
constructor(props?: object) { constructor(props?: object) {
if (props === void 0) { if (props === void 0) {

View File

@ -90,7 +90,6 @@ type ReservedKeys = { [K in keyof (APIMethods & LifecycleMethods)]: 1 }
export const reservedMethods: ReservedKeys = { export const reservedMethods: ReservedKeys = {
data: 1, data: 1,
render: 1, render: 1,
hooks: 1,
beforeCreate: 1, beforeCreate: 1,
created: 1, created: 1,
beforeMount: 1, beforeMount: 1,

View File

@ -1,9 +1,8 @@
import { ComponentInstance } from './component' import { ComponentInstance } from './component'
import { isFunction, isReservedKey } from '@vue/shared' import { isFunction, isReservedKey } from '@vue/shared'
import { warn } from './warning'
import { isRendering } from './componentRenderUtils' import { isRendering } from './componentRenderUtils'
import { isObservable } from '@vue/observer'
import { reservedMethods } from './componentOptions' import { reservedMethods } from './componentOptions'
import { warn } from './warning'
const bindCache = new WeakMap() const bindCache = new WeakMap()
@ -37,9 +36,6 @@ const renderProxyHandlers = {
) { ) {
// computed // computed
return i[key]() return i[key]()
} else if ((i = target._hookProps) !== null && i.hasOwnProperty(key)) {
// hooks injections
return i[key]
} else if (key[0] !== '_') { } else if (key[0] !== '_') {
if ( if (
__DEV__ && __DEV__ &&
@ -81,16 +77,6 @@ const renderProxyHandlers = {
if ((i = target._rawData) !== null && i.hasOwnProperty(key)) { if ((i = target._rawData) !== null && i.hasOwnProperty(key)) {
target.$data[key] = value target.$data[key] = value
return true return true
} else if ((i = target._hookProps) !== null && i.hasOwnProperty(key)) {
if (__DEV__ && !isObservable(i)) {
warn(
`attempting to mutate a property returned from hooks(), but the ` +
`value is not observable.`
)
}
// this enables returning observable objects from hooks()
i[key] = value
return true
} else { } else {
return Reflect.set(target, key, value, receiver) return Reflect.set(target, key, value, receiver)
} }

View File

@ -4,26 +4,13 @@ import { resolveProps } from './componentProps'
import { handleError, ErrorTypes } from './errorHandling' import { handleError, ErrorTypes } from './errorHandling'
import { VNodeFlags, ChildrenFlags } from './flags' import { VNodeFlags, ChildrenFlags } from './flags'
import { EMPTY_OBJ, isArray, isObject } from '@vue/shared' import { EMPTY_OBJ, isArray, isObject } from '@vue/shared'
import { setCurrentInstance, unsetCurrentInstance } from './experimental/hooks'
export let isRendering = false export let isRendering = false
export function renderInstanceRoot(instance: ComponentInstance): VNode { export function renderInstanceRoot(instance: ComponentInstance): VNode {
let vnode let vnode
const { const { render, $proxy, $props, $slots, $attrs, $parentVNode } = instance
$options: { hooks },
render,
$proxy,
$props,
$slots,
$attrs,
$parentVNode
} = instance
try { try {
setCurrentInstance(instance)
if (hooks) {
instance._hookProps = hooks.call($proxy, $props) || null
}
if (__DEV__) { if (__DEV__) {
isRendering = true isRendering = true
} }
@ -31,7 +18,6 @@ export function renderInstanceRoot(instance: ComponentInstance): VNode {
if (__DEV__) { if (__DEV__) {
isRendering = false isRendering = false
} }
unsetCurrentInstance()
} catch (err) { } catch (err) {
handleError(err, instance, ErrorTypes.RENDER) handleError(err, instance, ErrorTypes.RENDER)
} }

View File

@ -1258,7 +1258,7 @@ export function createRenderer(options: RendererOptions) {
const { const {
$proxy, $proxy,
$options: { beforeMount, renderTracked, renderTriggered } $options: { beforeMount, mounted, renderTracked, renderTriggered }
} = instance } = instance
instance.$forceUpdate = () => { instance.$forceUpdate = () => {
@ -1318,9 +1318,6 @@ export function createRenderer(options: RendererOptions) {
if (vnode.ref) { if (vnode.ref) {
vnode.ref($proxy) vnode.ref($proxy)
} }
// retrieve mounted value after initial render so that we get
// to inject effects in hooks
const { mounted } = instance.$options
if (mounted) { if (mounted) {
callLifecycleHookWithHandler(mounted, $proxy, ErrorTypes.MOUNTED) callLifecycleHookWithHandler(mounted, $proxy, ErrorTypes.MOUNTED)
} }

View File

@ -1,201 +0,0 @@
import { ComponentInstance } from '../component'
import { mergeLifecycleHooks, WatchOptions } from '../componentOptions'
import { observable, computed } from '@vue/observer'
import { setupWatcher } from '../componentWatch'
type RawEffect = () => (() => void) | void
type Effect = RawEffect & {
current?: RawEffect | null | void
}
type EffectRecord = {
effect: Effect
cleanup: Effect
deps: any[] | void
}
type Ref<T> = { current: T }
type HookState = {
state: any
effects: Record<number, EffectRecord>
refs: Record<number, Ref<any>>
}
let currentInstance: ComponentInstance | null = null
let isMounting: boolean = false
let callIndex: number = 0
const hooksStateMap = new WeakMap<ComponentInstance, HookState>()
export function setCurrentInstance(instance: ComponentInstance) {
currentInstance = instance
isMounting = !currentInstance._mounted
callIndex = 0
}
export function unsetCurrentInstance() {
currentInstance = null
}
function ensureCurrentInstance() {
if (!currentInstance) {
throw new Error(
`invalid hooks call` +
(__DEV__
? `. Hooks can only be called in one of the following: ` +
`render(), hooks(), or withHooks().`
: ``)
)
}
}
function getCurrentHookState(): HookState {
ensureCurrentInstance()
let hookState = hooksStateMap.get(currentInstance as ComponentInstance)
if (!hookState) {
hookState = {
state: observable({}),
effects: {},
refs: {}
}
hooksStateMap.set(currentInstance as ComponentInstance, hookState)
}
return hookState
}
// React compatible hooks ------------------------------------------------------
export function useState<T>(initial: T): [T, (newValue: T) => void] {
const id = ++callIndex
const { state } = getCurrentHookState()
const set = (newValue: any) => {
state[id] = newValue
}
if (isMounting) {
set(initial)
}
return [state[id], set]
}
export function useEffect(rawEffect: Effect, deps?: any[]) {
const id = ++callIndex
if (isMounting) {
const cleanup: Effect = () => {
const { current } = cleanup
if (current) {
current()
cleanup.current = null
}
}
const effect: Effect = () => {
const { current } = effect
if (current) {
cleanup.current = current()
effect.current = null
}
}
effect.current = rawEffect
getCurrentHookState().effects[id] = {
effect,
cleanup,
deps
}
injectEffect(currentInstance as ComponentInstance, 'mounted', effect)
injectEffect(currentInstance as ComponentInstance, 'unmounted', cleanup)
if (!deps || deps.length !== 0) {
injectEffect(currentInstance as ComponentInstance, 'updated', effect)
}
} else {
const record = getCurrentHookState().effects[id]
const { effect, cleanup, deps: prevDeps = [] } = record
record.deps = deps
if (!deps || hasDepsChanged(deps, prevDeps)) {
cleanup()
effect.current = rawEffect
}
}
}
function hasDepsChanged(deps: any[], prevDeps: any[]): boolean {
if (deps.length !== prevDeps.length) {
return true
}
for (let i = 0; i < deps.length; i++) {
if (deps[i] !== prevDeps[i]) {
return true
}
}
return false
}
function injectEffect(
instance: ComponentInstance,
key: string,
effect: Effect
) {
const existing = instance.$options[key]
;(instance.$options as any)[key] = existing
? mergeLifecycleHooks(existing, effect)
: effect
}
export function useRef<T>(initial?: T): Ref<T> {
const id = ++callIndex
const { refs } = getCurrentHookState()
return isMounting ? (refs[id] = { current: initial }) : refs[id]
}
// Vue API hooks ---------------------------------------------------------------
export function useData<T>(initial: T): T {
const id = ++callIndex
const { state } = getCurrentHookState()
if (isMounting) {
state[id] = initial
}
return state[id]
}
export function useMounted(fn: () => void) {
useEffect(fn, [])
}
export function useUnmounted(fn: () => void) {
useEffect(() => fn, [])
}
export function useUpdated(fn: () => void, deps?: any[]) {
const isMount = useRef(true)
useEffect(() => {
if (isMount.current) {
isMount.current = false
} else {
return fn()
}
}, deps)
}
export function useWatch<T>(
getter: () => T,
cb: (val: T, oldVal: T) => void,
options?: WatchOptions
) {
ensureCurrentInstance()
if (isMounting) {
setupWatcher(currentInstance as ComponentInstance, getter, cb, options)
}
}
export function useComputed<T>(getter: () => T): T {
ensureCurrentInstance()
const id = `__hooksComputed${++callIndex}`
const instance = currentInstance as ComponentInstance
const handles = instance._computedGetters || (instance._computedGetters = {})
if (isMounting) {
handles[id] = computed(getter)
}
return handles[id]()
}

View File

@ -32,19 +32,6 @@ export { mixins } from './optional/mixins'
export { EventEmitter } from './optional/eventEmitter' export { EventEmitter } from './optional/eventEmitter'
export { memoize } from './optional/memoize' export { memoize } from './optional/memoize'
// Experimental APIs
export {
useState,
useEffect,
useRef,
useData,
useWatch,
useComputed,
useMounted,
useUnmounted,
useUpdated
} from './experimental/hooks'
// flags & types // flags & types
export { ComponentType, ComponentClass, FunctionalComponent } from './component' export { ComponentType, ComponentClass, FunctionalComponent } from './component'
export { VNodeFlags, ChildrenFlags } from './flags' export { VNodeFlags, ChildrenFlags } from './flags'