]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat: make hooks usable inside classes
authorEvan You <yyx990803@gmail.com>
Sun, 28 Oct 2018 23:15:18 +0000 (19:15 -0400)
committerEvan You <yyx990803@gmail.com>
Sun, 28 Oct 2018 23:15:18 +0000 (19:15 -0400)
packages/runtime-core/__tests__/hooks.spec.ts
packages/runtime-core/src/component.ts
packages/runtime-core/src/componentOptions.ts
packages/runtime-core/src/componentProxy.ts
packages/runtime-core/src/componentUtils.ts
packages/runtime-core/src/experimental/hooks.ts

index 51ccd39fde8658f83cb3f18fedaa93acc87e22ec..84e447199479feafda8b32200d97aa8f654b7cbb 100644 (file)
@@ -1,4 +1,4 @@
-import { withHooks, useState, h, nextTick, useEffect } from '../src'
+import { withHooks, useState, h, nextTick, useEffect, Component } from '../src'
 import { renderIntsance, serialize, triggerEvent } from '@vue/runtime-test'
 
 describe('hooks', () => {
@@ -50,6 +50,61 @@ describe('hooks', () => {
     expect(effect).toBe(1)
   })
 
+  it('should be usable inside class', async () => {
+    class Counter extends Component {
+      render() {
+        const [count, setCount] = useState(0)
+        return h(
+          'div',
+          {
+            onClick: () => {
+              setCount(count + 1)
+            }
+          },
+          count
+        )
+      }
+    }
+
+    const counter = renderIntsance(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 = renderIntsance(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 with empty keys', async () => {
     // TODO
   })
index e333554c4ed70789b3ab88565d80d3b90f01274b..d57c8f885a69b2cd8780d048afeb5801ae71e9c2 100644 (file)
@@ -45,6 +45,7 @@ interface PublicInstanceMethods {
 
 export interface APIMethods<P = {}, D = {}> {
   data(): Partial<D>
+  hooks(): any
   render(props: Readonly<P>, slots: Slots, attrs: Data, parentVNode: VNode): any
 }
 
@@ -135,6 +136,7 @@ class InternalComponent implements PublicInstanceMethods {
   _queueJob: ((fn: () => void) => void) | null = null
   _isVue: boolean = true
   _inactiveRoot: boolean = false
+  _hookProps: any = null
 
   constructor(props?: object) {
     if (props === void 0) {
index 12af237a4b7a2f50519b6ae00cad8ba67b8b93a5..e1e98136ef1c50498ac894430f68e15672356b93 100644 (file)
@@ -88,6 +88,7 @@ type ReservedKeys = { [K in keyof (APIMethods & LifecycleMethods)]: 1 }
 export const reservedMethods: ReservedKeys = {
   data: 1,
   render: 1,
+  hooks: 1,
   beforeCreate: 1,
   created: 1,
   beforeMount: 1,
index 6fcaa1f71b9846da49bf0dcfe902c2cb42e69833..92525a208851f10531b17707b3604029369c6b9b 100644 (file)
@@ -1,5 +1,7 @@
 import { ComponentInstance } from './component'
 import { isFunction, isReservedKey } from '@vue/shared'
+import { warn } from './warning'
+import { isRendering } from './componentUtils'
 
 const bindCache = new WeakMap()
 
@@ -17,29 +19,31 @@ function getBoundMethod(fn: Function, target: any, receiver: any): Function {
 
 const renderProxyHandlers = {
   get(target: ComponentInstance<any, any>, key: string, receiver: any) {
+    let i: any
     if (key === '_self') {
       return target
-    } else if (
-      target._rawData !== null &&
-      target._rawData.hasOwnProperty(key)
-    ) {
+    } else if ((i = target._rawData) !== null && i.hasOwnProperty(key)) {
       // data
+      // make sure to return from $data to register dependency
       return target.$data[key]
-    } else if (
-      target.$options.props != null &&
-      target.$options.props.hasOwnProperty(key)
-    ) {
+    } else if ((i = target.$options.props) != null && i.hasOwnProperty(key)) {
       // props are only proxied if declared
+      // make sure to return from $props to register dependency
       return target.$props[key]
     } else if (
-      target._computedGetters !== null &&
-      target._computedGetters.hasOwnProperty(key)
+      (i = target._computedGetters) !== null &&
+      i.hasOwnProperty(key)
     ) {
       // computed
-      return target._computedGetters[key]()
+      return i[key]()
+    } else if ((i = target._hookProps) !== null && i.hasOwnProperty(key)) {
+      // hooks injections
+      return i[key]
     } else if (key[0] !== '_') {
-      if (__DEV__ && !(key in target)) {
-        // TODO warn non-present property
+      if (__DEV__ && isRendering && !(key in target)) {
+        warn(
+          `property "${key}" was accessed during render but does not exist on instance.`
+        )
       }
       const value = Reflect.get(target, key, receiver)
       if (key !== 'constructor' && isFunction(value)) {
@@ -56,20 +60,18 @@ const renderProxyHandlers = {
     value: any,
     receiver: any
   ): boolean {
+    let i: any
     if (__DEV__) {
       if (isReservedKey(key) && key in target) {
-        // TODO warn setting immutable properties
+        warn(`failed setting property "${key}": reserved fields are immutable.`)
         return false
       }
-      if (
-        target.$options.props != null &&
-        target.$options.props.hasOwnProperty(key)
-      ) {
-        // TODO warn props are immutable
+      if ((i = target.$options.props) != null && i.hasOwnProperty(key)) {
+        warn(`failed setting property "${key}": props are immutable.`)
         return false
       }
     }
-    if (target._rawData !== null && target._rawData.hasOwnProperty(key)) {
+    if ((i = target._rawData) !== null && i.hasOwnProperty(key)) {
       target.$data[key] = value
       return true
     } else {
index a3e218b29cc12bdf2bd8eee9052b77096072915d..783fd6ed019f07dbe72f2279dc76b6deceabb1ba 100644 (file)
@@ -20,6 +20,7 @@ import {
 import { createRenderProxy } from './componentProxy'
 import { handleError, ErrorTypes } from './errorHandling'
 import { warn } from './warning'
+import { setCurrentInstance, unsetCurrentInstance } from './experimental/hooks'
 
 let currentVNode: VNode | null = null
 let currentContextVNode: VNode | null = null
@@ -100,9 +101,19 @@ export function initializeComponentInstance(instance: ComponentInstance) {
   initializeProps(instance, props, (currentVNode as VNode).data)
 }
 
+export let isRendering = false
+
 export function renderInstanceRoot(instance: ComponentInstance): VNode {
   let vnode
   try {
+    setCurrentInstance(instance)
+    if (instance.hooks) {
+      instance._hookProps =
+        instance.hooks.call(instance.$proxy, instance.$props) || null
+    }
+    if (__DEV__) {
+      isRendering = true
+    }
     vnode = instance.render.call(
       instance.$proxy,
       instance.$props,
@@ -110,6 +121,10 @@ export function renderInstanceRoot(instance: ComponentInstance): VNode {
       instance.$attrs,
       instance.$parentVNode
     )
+    if (__DEV__) {
+      isRendering = false
+    }
+    unsetCurrentInstance()
   } catch (err) {
     handleError(err, instance, ErrorTypes.RENDER)
   }
index c195c2c765d484a44c3256f544e1fd701c31e111..241adfe1786e31f73540f1062532ff9d602e81ee 100644 (file)
@@ -24,7 +24,7 @@ let currentInstance: ComponentInstance | null = null
 let isMounting: boolean = false
 let callIndex: number = 0
 
-const hooksState = new WeakMap<ComponentInstance, HookState>()
+const hooksStateMap = new WeakMap<ComponentInstance, HookState>()
 
 export function setCurrentInstance(instance: ComponentInstance) {
   currentInstance = instance
@@ -36,6 +36,18 @@ export function unsetCurrentInstance() {
   currentInstance = null
 }
 
+function getHookStateForInstance(instance: ComponentInstance): HookState {
+  let hookState = hooksStateMap.get(instance)
+  if (!hookState) {
+    hookState = {
+      state: observable({}),
+      effects: []
+    }
+    hooksStateMap.set(instance, hookState)
+  }
+  return hookState
+}
+
 export function useState<T>(initial: T): [T, (newValue: T) => void] {
   if (!currentInstance) {
     throw new Error(
@@ -43,7 +55,7 @@ export function useState<T>(initial: T): [T, (newValue: T) => void] {
     )
   }
   const id = ++callIndex
-  const { state } = hooksState.get(currentInstance) as HookState
+  const { state } = getHookStateForInstance(currentInstance)
   const set = (newValue: any) => {
     state[id] = newValue
   }
@@ -76,7 +88,7 @@ export function useEffect(rawEffect: Effect, deps?: any[]) {
       }
     }
     effect.current = rawEffect
-    ;(hooksState.get(currentInstance) as HookState).effects[id] = {
+    getHookStateForInstance(currentInstance).effects[id] = {
       effect,
       cleanup,
       deps
@@ -86,7 +98,7 @@ export function useEffect(rawEffect: Effect, deps?: any[]) {
     injectEffect(currentInstance, 'unmounted', cleanup)
     injectEffect(currentInstance, 'updated', effect)
   } else {
-    const record = (hooksState.get(currentInstance) as HookState).effects[id]
+    const record = getHookStateForInstance(currentInstance).effects[id]
     const { effect, cleanup, deps: prevDeps = [] } = record
     record.deps = deps
     if (!deps || deps.some((d, i) => d !== prevDeps[i])) {
@@ -110,12 +122,6 @@ function injectEffect(
 export function withHooks(render: FunctionalComponent): new () => Component {
   return class ComponentWithHooks extends Component {
     static displayName = render.name
-    created() {
-      hooksState.set((this as any)._self, {
-        state: observable({}),
-        effects: []
-      })
-    }
     render(props: Data, slots: Slots, attrs: Data, parentVNode: VNode) {
       setCurrentInstance((this as any)._self)
       const ret = render(props, slots, attrs, parentVNode)