hooks.ts 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126
  1. import { ComponentInstance, FunctionalComponent, Component } from '../component'
  2. import { mergeLifecycleHooks, Data } from '../componentOptions'
  3. import { VNode, Slots } from '../vdom'
  4. import { observable } from '@vue/observer'
  5. type RawEffect = () => (() => void) | void
  6. type Effect = RawEffect & {
  7. current?: RawEffect | null | void
  8. }
  9. type EffectRecord = {
  10. effect: Effect
  11. cleanup: Effect
  12. deps: any[] | void
  13. }
  14. type HookState = {
  15. state: any
  16. effects: EffectRecord[]
  17. }
  18. let currentInstance: ComponentInstance | null = null
  19. let isMounting: boolean = false
  20. let callIndex: number = 0
  21. const hooksState = new WeakMap<ComponentInstance, HookState>()
  22. export function setCurrentInstance(instance: ComponentInstance) {
  23. currentInstance = instance
  24. isMounting = !currentInstance._mounted
  25. callIndex = 0
  26. }
  27. export function unsetCurrentInstance() {
  28. currentInstance = null
  29. }
  30. export function useState<T>(initial: T): [T, (newValue: T) => void] {
  31. if (!currentInstance) {
  32. throw new Error(
  33. `useState must be called in a function passed to withHooks.`
  34. )
  35. }
  36. const id = ++callIndex
  37. const { state } = hooksState.get(currentInstance) as HookState
  38. const set = (newValue: any) => {
  39. state[id] = newValue
  40. }
  41. if (isMounting) {
  42. set(initial)
  43. }
  44. return [state[id], set]
  45. }
  46. export function useEffect(rawEffect: Effect, deps?: any[]) {
  47. if (!currentInstance) {
  48. throw new Error(
  49. `useEffect must be called in a function passed to withHooks.`
  50. )
  51. }
  52. const id = ++callIndex
  53. if (isMounting) {
  54. const cleanup: Effect = () => {
  55. const { current } = cleanup
  56. if (current) {
  57. current()
  58. cleanup.current = null
  59. }
  60. }
  61. const effect: Effect = () => {
  62. const { current } = effect
  63. if (current) {
  64. cleanup.current = current()
  65. effect.current = null
  66. }
  67. }
  68. effect.current = rawEffect
  69. ;(hooksState.get(currentInstance) as HookState).effects[id] = {
  70. effect,
  71. cleanup,
  72. deps
  73. }
  74. injectEffect(currentInstance, 'mounted', effect)
  75. injectEffect(currentInstance, 'unmounted', cleanup)
  76. injectEffect(currentInstance, 'updated', effect)
  77. } else {
  78. const record = (hooksState.get(currentInstance) as HookState).effects[id]
  79. const { effect, cleanup, deps: prevDeps = [] } = record
  80. record.deps = deps
  81. if (!deps || deps.some((d, i) => d !== prevDeps[i])) {
  82. cleanup()
  83. effect.current = rawEffect
  84. }
  85. }
  86. }
  87. function injectEffect(
  88. instance: ComponentInstance,
  89. key: string,
  90. effect: Effect
  91. ) {
  92. const existing = instance.$options[key]
  93. ;(instance.$options as any)[key] = existing
  94. ? mergeLifecycleHooks(existing, effect)
  95. : effect
  96. }
  97. export function withHooks(render: FunctionalComponent): new () => Component {
  98. return class ComponentWithHooks extends Component {
  99. static displayName = render.name
  100. created() {
  101. hooksState.set((this as any)._self, {
  102. state: observable({}),
  103. effects: []
  104. })
  105. }
  106. render(props: Data, slots: Slots, attrs: Data, parentVNode: VNode) {
  107. setCurrentInstance((this as any)._self)
  108. const ret = render(props, slots, attrs, parentVNode)
  109. unsetCurrentInstance()
  110. return ret
  111. }
  112. }
  113. }