diff --git a/packages/runtime-core/__tests__/hooks.spec.ts b/packages/runtime-core/__tests__/hooks.spec.ts
index 84e44719..29d1073b 100644
--- a/packages/runtime-core/__tests__/hooks.spec.ts
+++ b/packages/runtime-core/__tests__/hooks.spec.ts
@@ -24,32 +24,6 @@ describe('hooks', () => {
expect(serialize(counter.$el)).toBe(`
1
`)
})
- it('useEffect', async () => {
- let effect = -1
-
- const Counter = withHooks(() => {
- const [count, setCount] = useState(0)
- useEffect(() => {
- effect = count
- })
- return h(
- 'div',
- {
- onClick: () => {
- setCount(count + 1)
- }
- },
- count
- )
- })
-
- const counter = renderIntsance(Counter)
- expect(effect).toBe(0)
- triggerEvent(counter.$el, 'click')
- await nextTick()
- expect(effect).toBe(1)
- })
-
it('should be usable inside class', async () => {
class Counter extends Component {
render() {
@@ -105,6 +79,32 @@ describe('hooks', () => {
expect(serialize(counter.$el)).toBe(`1
`)
})
+ it('useEffect', async () => {
+ let effect = -1
+
+ const Counter = withHooks(() => {
+ const [count, setCount] = useState(0)
+ useEffect(() => {
+ effect = count
+ })
+ return h(
+ 'div',
+ {
+ onClick: () => {
+ setCount(count + 1)
+ }
+ },
+ count
+ )
+ })
+
+ const counter = renderIntsance(Counter)
+ expect(effect).toBe(0)
+ triggerEvent(counter.$el, 'click')
+ await nextTick()
+ expect(effect).toBe(1)
+ })
+
it('useEffect with empty keys', async () => {
// TODO
})
@@ -112,4 +112,20 @@ describe('hooks', () => {
it('useEffect with keys', async () => {
// TODO
})
+
+ it('useData', () => {
+ // TODO
+ })
+
+ it('useMounted/useUnmounted/useUpdated', () => {
+ // TODO
+ })
+
+ it('useWatch', () => {
+ // TODO
+ })
+
+ it('useComputed', () => {
+ // TODO
+ })
})
diff --git a/packages/runtime-core/src/experimental/hooks.ts b/packages/runtime-core/src/experimental/hooks.ts
index 241adfe1..e2d67be4 100644
--- a/packages/runtime-core/src/experimental/hooks.ts
+++ b/packages/runtime-core/src/experimental/hooks.ts
@@ -1,7 +1,8 @@
import { ComponentInstance, FunctionalComponent, Component } from '../component'
-import { mergeLifecycleHooks, Data } from '../componentOptions'
+import { mergeLifecycleHooks, Data, WatchOptions } from '../componentOptions'
import { VNode, Slots } from '../vdom'
-import { observable } from '@vue/observer'
+import { observable, computed, stop, ComputedGetter } from '@vue/observer'
+import { setupWatcher } from '../componentWatch'
type RawEffect = () => (() => void) | void
@@ -15,9 +16,12 @@ type EffectRecord = {
deps: any[] | void
}
+type Ref = { current: T }
+
type HookState = {
state: any
- effects: EffectRecord[]
+ effects: Record
+ refs: Record>
}
let currentInstance: ComponentInstance | null = null
@@ -36,26 +40,37 @@ export function unsetCurrentInstance() {
currentInstance = null
}
-function getHookStateForInstance(instance: ComponentInstance): HookState {
- let hookState = hooksStateMap.get(instance)
+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: []
+ effects: {},
+ refs: {}
}
- hooksStateMap.set(instance, hookState)
+ hooksStateMap.set(currentInstance as ComponentInstance, hookState)
}
return hookState
}
+// React compatible hooks ------------------------------------------------------
+
export function useState(initial: T): [T, (newValue: T) => void] {
- if (!currentInstance) {
- throw new Error(
- `useState must be called in a function passed to withHooks.`
- )
- }
const id = ++callIndex
- const { state } = getHookStateForInstance(currentInstance)
+ const { state } = getCurrentHookState()
const set = (newValue: any) => {
state[id] = newValue
}
@@ -66,11 +81,6 @@ export function useState(initial: T): [T, (newValue: T) => void] {
}
export function useEffect(rawEffect: Effect, deps?: any[]) {
- if (!currentInstance) {
- throw new Error(
- `useEffect must be called in a function passed to withHooks.`
- )
- }
const id = ++callIndex
if (isMounting) {
const cleanup: Effect = () => {
@@ -88,26 +98,40 @@ export function useEffect(rawEffect: Effect, deps?: any[]) {
}
}
effect.current = rawEffect
- getHookStateForInstance(currentInstance).effects[id] = {
+ getCurrentHookState().effects[id] = {
effect,
cleanup,
deps
}
- injectEffect(currentInstance, 'mounted', effect)
- injectEffect(currentInstance, 'unmounted', cleanup)
- injectEffect(currentInstance, 'updated', effect)
+ 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 = getHookStateForInstance(currentInstance).effects[id]
+ const record = getCurrentHookState().effects[id]
const { effect, cleanup, deps: prevDeps = [] } = record
record.deps = deps
- if (!deps || deps.some((d, i) => d !== prevDeps[i])) {
+ 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,
@@ -119,6 +143,64 @@ function injectEffect(
: effect
}
+export function useRef(initial?: T): Ref {
+ const id = ++callIndex
+ const { refs } = getCurrentHookState()
+ return isMounting ? (refs[id] = { current: initial }) : refs[id]
+}
+
+// Vue API hooks ---------------------------------------------------------------
+
+export function useData(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(
+ getter: () => T,
+ cb: (val: T, oldVal: T) => void,
+ options?: WatchOptions
+) {
+ ensureCurrentInstance()
+ if (isMounting) {
+ setupWatcher(currentInstance as ComponentInstance, getter, cb, options)
+ }
+}
+
+export function useComputed(getter: () => T): T {
+ const computedRef = useRef()
+ useUnmounted(() => {
+ stop((computedRef.current as ComputedGetter).runner)
+ })
+ if (isMounting) {
+ computedRef.current = computed(getter)
+ }
+ return (computedRef.current as ComputedGetter)()
+}
+
export function withHooks(render: FunctionalComponent): new () => Component {
return class ComponentWithHooks extends Component {
static displayName = render.name
diff --git a/packages/runtime-core/src/index.ts b/packages/runtime-core/src/index.ts
index be6d1fc6..f038318d 100644
--- a/packages/runtime-core/src/index.ts
+++ b/packages/runtime-core/src/index.ts
@@ -21,7 +21,18 @@ export { EventEmitter } from './optional/eventEmitter'
export { memoize } from './optional/memoize'
// Experimental APIs
-export { withHooks, useState, useEffect } from './experimental/hooks'
+export {
+ withHooks,
+ useState,
+ useEffect,
+ useRef,
+ useData,
+ useWatch,
+ useComputed,
+ useMounted,
+ useUnmounted,
+ useUpdated
+} from './experimental/hooks'
// flags & types
export { ComponentType, ComponentClass, FunctionalComponent } from './component'