+++ /dev/null
-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
- })
-})
import { ComponentInstance } from './component'
import { isFunction, isReservedKey } from '@vue/shared'
-import { warn } from './warning'
import { isRendering } from './componentRenderUtils'
-import { isObservable } from '@vue/observer'
import { reservedMethods } from './componentOptions'
+import { warn } from './warning'
const bindCache = new WeakMap()
) {
// computed
return i[key]()
- } else if ((i = target._hookProps) !== null && i.hasOwnProperty(key)) {
- // hooks injections
- return i[key]
} else if (key[0] !== '_') {
if (
__DEV__ &&
if ((i = target._rawData) !== null && i.hasOwnProperty(key)) {
target.$data[key] = value
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 {
return Reflect.set(target, key, value, receiver)
}
+++ /dev/null
-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]()
-}