|
@@ -1,7 +1,8 @@
|
|
|
import { ComponentInstance, FunctionalComponent, Component } from '../component'
|
|
import { ComponentInstance, FunctionalComponent, Component } from '../component'
|
|
|
-import { mergeLifecycleHooks, Data } from '../componentOptions'
|
|
|
|
|
|
|
+import { mergeLifecycleHooks, Data, WatchOptions } from '../componentOptions'
|
|
|
import { VNode, Slots } from '../vdom'
|
|
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
|
|
type RawEffect = () => (() => void) | void
|
|
|
|
|
|
|
@@ -15,9 +16,12 @@ type EffectRecord = {
|
|
|
deps: any[] | void
|
|
deps: any[] | void
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+type Ref<T> = { current: T }
|
|
|
|
|
+
|
|
|
type HookState = {
|
|
type HookState = {
|
|
|
state: any
|
|
state: any
|
|
|
- effects: EffectRecord[]
|
|
|
|
|
|
|
+ effects: Record<number, EffectRecord>
|
|
|
|
|
+ refs: Record<number, Ref<any>>
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
let currentInstance: ComponentInstance | null = null
|
|
let currentInstance: ComponentInstance | null = null
|
|
@@ -36,26 +40,37 @@ export function unsetCurrentInstance() {
|
|
|
currentInstance = null
|
|
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) {
|
|
if (!hookState) {
|
|
|
hookState = {
|
|
hookState = {
|
|
|
state: observable({}),
|
|
state: observable({}),
|
|
|
- effects: []
|
|
|
|
|
|
|
+ effects: {},
|
|
|
|
|
+ refs: {}
|
|
|
}
|
|
}
|
|
|
- hooksStateMap.set(instance, hookState)
|
|
|
|
|
|
|
+ hooksStateMap.set(currentInstance as ComponentInstance, hookState)
|
|
|
}
|
|
}
|
|
|
return hookState
|
|
return hookState
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// React compatible hooks ------------------------------------------------------
|
|
|
|
|
+
|
|
|
export function useState<T>(initial: T): [T, (newValue: T) => void] {
|
|
export function useState<T>(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 id = ++callIndex
|
|
|
- const { state } = getHookStateForInstance(currentInstance)
|
|
|
|
|
|
|
+ const { state } = getCurrentHookState()
|
|
|
const set = (newValue: any) => {
|
|
const set = (newValue: any) => {
|
|
|
state[id] = newValue
|
|
state[id] = newValue
|
|
|
}
|
|
}
|
|
@@ -66,11 +81,6 @@ export function useState<T>(initial: T): [T, (newValue: T) => void] {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
export function useEffect(rawEffect: Effect, deps?: any[]) {
|
|
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
|
|
const id = ++callIndex
|
|
|
if (isMounting) {
|
|
if (isMounting) {
|
|
|
const cleanup: Effect = () => {
|
|
const cleanup: Effect = () => {
|
|
@@ -88,26 +98,40 @@ export function useEffect(rawEffect: Effect, deps?: any[]) {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
effect.current = rawEffect
|
|
effect.current = rawEffect
|
|
|
- getHookStateForInstance(currentInstance).effects[id] = {
|
|
|
|
|
|
|
+ getCurrentHookState().effects[id] = {
|
|
|
effect,
|
|
effect,
|
|
|
cleanup,
|
|
cleanup,
|
|
|
deps
|
|
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 {
|
|
} else {
|
|
|
- const record = getHookStateForInstance(currentInstance).effects[id]
|
|
|
|
|
|
|
+ const record = getCurrentHookState().effects[id]
|
|
|
const { effect, cleanup, deps: prevDeps = [] } = record
|
|
const { effect, cleanup, deps: prevDeps = [] } = record
|
|
|
record.deps = deps
|
|
record.deps = deps
|
|
|
- if (!deps || deps.some((d, i) => d !== prevDeps[i])) {
|
|
|
|
|
|
|
+ if (!deps || hasDepsChanged(deps, prevDeps)) {
|
|
|
cleanup()
|
|
cleanup()
|
|
|
effect.current = rawEffect
|
|
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(
|
|
function injectEffect(
|
|
|
instance: ComponentInstance,
|
|
instance: ComponentInstance,
|
|
|
key: string,
|
|
key: string,
|
|
@@ -119,6 +143,64 @@ function injectEffect(
|
|
|
: 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 {
|
|
|
|
|
+ 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 {
|
|
export function withHooks(render: FunctionalComponent): new () => Component {
|
|
|
return class ComponentWithHooks extends Component {
|
|
return class ComponentWithHooks extends Component {
|
|
|
static displayName = render.name
|
|
static displayName = render.name
|