feat: hooks that match current API

This commit is contained in:
Evan You 2018-10-29 18:50:07 -04:00
parent 894bead914
commit 6767bf83c4
3 changed files with 160 additions and 51 deletions

View File

@ -24,32 +24,6 @@ describe('hooks', () => {
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 () => {
class Counter extends Component {
render() {
@ -105,6 +79,32 @@ describe('hooks', () => {
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 () => {
// 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
})
})

View File

@ -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<T> = { current: T }
type HookState = {
state: any
effects: EffectRecord[]
effects: Record<number, EffectRecord>
refs: Record<number, Ref<any>>
}
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<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 { state } = getHookStateForInstance(currentInstance)
const { state } = getCurrentHookState()
const set = (newValue: any) => {
state[id] = newValue
}
@ -66,11 +81,6 @@ export function useState<T>(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<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 {
return class ComponentWithHooks extends Component {
static displayName = render.name

View File

@ -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'