feat: hooks that match current API
This commit is contained in:
parent
894bead914
commit
6767bf83c4
@ -24,32 +24,6 @@ describe('hooks', () => {
|
|||||||
expect(serialize(counter.$el)).toBe(`<div>1</div>`)
|
expect(serialize(counter.$el)).toBe(`<div>1</div>`)
|
||||||
})
|
})
|
||||||
|
|
||||||
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 () => {
|
it('should be usable inside class', async () => {
|
||||||
class Counter extends Component {
|
class Counter extends Component {
|
||||||
render() {
|
render() {
|
||||||
@ -105,6 +79,32 @@ describe('hooks', () => {
|
|||||||
expect(serialize(counter.$el)).toBe(`<div>1</div>`)
|
expect(serialize(counter.$el)).toBe(`<div>1</div>`)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
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 () => {
|
it('useEffect with empty keys', async () => {
|
||||||
// TODO
|
// TODO
|
||||||
})
|
})
|
||||||
@ -112,4 +112,20 @@ describe('hooks', () => {
|
|||||||
it('useEffect with keys', async () => {
|
it('useEffect with keys', async () => {
|
||||||
// TODO
|
// TODO
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('useData', () => {
|
||||||
|
// TODO
|
||||||
|
})
|
||||||
|
|
||||||
|
it('useMounted/useUnmounted/useUpdated', () => {
|
||||||
|
// TODO
|
||||||
|
})
|
||||||
|
|
||||||
|
it('useWatch', () => {
|
||||||
|
// TODO
|
||||||
|
})
|
||||||
|
|
||||||
|
it('useComputed', () => {
|
||||||
|
// TODO
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
@ -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 {
|
function ensureCurrentInstance() {
|
||||||
let hookState = hooksStateMap.get(instance)
|
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 as ComponentInstance, 'mounted', effect)
|
||||||
injectEffect(currentInstance, 'unmounted', cleanup)
|
injectEffect(currentInstance as ComponentInstance, 'unmounted', cleanup)
|
||||||
injectEffect(currentInstance, 'updated', effect)
|
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
|
||||||
|
@ -21,7 +21,18 @@ export { EventEmitter } from './optional/eventEmitter'
|
|||||||
export { memoize } from './optional/memoize'
|
export { memoize } from './optional/memoize'
|
||||||
|
|
||||||
// Experimental APIs
|
// 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
|
// flags & types
|
||||||
export { ComponentType, ComponentClass, FunctionalComponent } from './component'
|
export { ComponentType, ComponentClass, FunctionalComponent } from './component'
|
||||||
|
Loading…
Reference in New Issue
Block a user