Просмотр исходного кода

feat: make hooks usable inside classes

Evan You 7 лет назад
Родитель
Сommit
894bead914

+ 56 - 1
packages/runtime-core/__tests__/hooks.spec.ts

@@ -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'
 import { renderIntsance, serialize, triggerEvent } from '@vue/runtime-test'
 
 
 describe('hooks', () => {
 describe('hooks', () => {
@@ -50,6 +50,61 @@ describe('hooks', () => {
     expect(effect).toBe(1)
     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 () => {
   it('useEffect with empty keys', async () => {
     // TODO
     // TODO
   })
   })

+ 2 - 0
packages/runtime-core/src/component.ts

@@ -45,6 +45,7 @@ interface PublicInstanceMethods {
 
 
 export interface APIMethods<P = {}, D = {}> {
 export interface APIMethods<P = {}, D = {}> {
   data(): Partial<D>
   data(): Partial<D>
+  hooks(): any
   render(props: Readonly<P>, slots: Slots, attrs: Data, parentVNode: VNode): 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
   _queueJob: ((fn: () => void) => void) | null = null
   _isVue: boolean = true
   _isVue: boolean = true
   _inactiveRoot: boolean = false
   _inactiveRoot: boolean = false
+  _hookProps: any = null
 
 
   constructor(props?: object) {
   constructor(props?: object) {
     if (props === void 0) {
     if (props === void 0) {

+ 1 - 0
packages/runtime-core/src/componentOptions.ts

@@ -88,6 +88,7 @@ type ReservedKeys = { [K in keyof (APIMethods & LifecycleMethods)]: 1 }
 export const reservedMethods: ReservedKeys = {
 export const reservedMethods: ReservedKeys = {
   data: 1,
   data: 1,
   render: 1,
   render: 1,
+  hooks: 1,
   beforeCreate: 1,
   beforeCreate: 1,
   created: 1,
   created: 1,
   beforeMount: 1,
   beforeMount: 1,

+ 22 - 20
packages/runtime-core/src/componentProxy.ts

@@ -1,5 +1,7 @@
 import { ComponentInstance } from './component'
 import { ComponentInstance } from './component'
 import { isFunction, isReservedKey } from '@vue/shared'
 import { isFunction, isReservedKey } from '@vue/shared'
+import { warn } from './warning'
+import { isRendering } from './componentUtils'
 
 
 const bindCache = new WeakMap()
 const bindCache = new WeakMap()
 
 
@@ -17,29 +19,31 @@ function getBoundMethod(fn: Function, target: any, receiver: any): Function {
 
 
 const renderProxyHandlers = {
 const renderProxyHandlers = {
   get(target: ComponentInstance<any, any>, key: string, receiver: any) {
   get(target: ComponentInstance<any, any>, key: string, receiver: any) {
+    let i: any
     if (key === '_self') {
     if (key === '_self') {
       return target
       return target
-    } else if (
-      target._rawData !== null &&
-      target._rawData.hasOwnProperty(key)
-    ) {
+    } else if ((i = target._rawData) !== null && i.hasOwnProperty(key)) {
       // data
       // data
+      // make sure to return from $data to register dependency
       return target.$data[key]
       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
       // props are only proxied if declared
+      // make sure to return from $props to register dependency
       return target.$props[key]
       return target.$props[key]
     } else if (
     } else if (
-      target._computedGetters !== null &&
-      target._computedGetters.hasOwnProperty(key)
+      (i = target._computedGetters) !== null &&
+      i.hasOwnProperty(key)
     ) {
     ) {
       // computed
       // 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] !== '_') {
     } 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)
       const value = Reflect.get(target, key, receiver)
       if (key !== 'constructor' && isFunction(value)) {
       if (key !== 'constructor' && isFunction(value)) {
@@ -56,20 +60,18 @@ const renderProxyHandlers = {
     value: any,
     value: any,
     receiver: any
     receiver: any
   ): boolean {
   ): boolean {
+    let i: any
     if (__DEV__) {
     if (__DEV__) {
       if (isReservedKey(key) && key in target) {
       if (isReservedKey(key) && key in target) {
-        // TODO warn setting immutable properties
+        warn(`failed setting property "${key}": reserved fields are immutable.`)
         return false
         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
         return false
       }
       }
     }
     }
-    if (target._rawData !== null && target._rawData.hasOwnProperty(key)) {
+    if ((i = target._rawData) !== null && i.hasOwnProperty(key)) {
       target.$data[key] = value
       target.$data[key] = value
       return true
       return true
     } else {
     } else {

+ 15 - 0
packages/runtime-core/src/componentUtils.ts

@@ -20,6 +20,7 @@ import {
 import { createRenderProxy } from './componentProxy'
 import { createRenderProxy } from './componentProxy'
 import { handleError, ErrorTypes } from './errorHandling'
 import { handleError, ErrorTypes } from './errorHandling'
 import { warn } from './warning'
 import { warn } from './warning'
+import { setCurrentInstance, unsetCurrentInstance } from './experimental/hooks'
 
 
 let currentVNode: VNode | null = null
 let currentVNode: VNode | null = null
 let currentContextVNode: VNode | null = null
 let currentContextVNode: VNode | null = null
@@ -100,9 +101,19 @@ export function initializeComponentInstance(instance: ComponentInstance) {
   initializeProps(instance, props, (currentVNode as VNode).data)
   initializeProps(instance, props, (currentVNode as VNode).data)
 }
 }
 
 
+export let isRendering = false
+
 export function renderInstanceRoot(instance: ComponentInstance): VNode {
 export function renderInstanceRoot(instance: ComponentInstance): VNode {
   let vnode
   let vnode
   try {
   try {
+    setCurrentInstance(instance)
+    if (instance.hooks) {
+      instance._hookProps =
+        instance.hooks.call(instance.$proxy, instance.$props) || null
+    }
+    if (__DEV__) {
+      isRendering = true
+    }
     vnode = instance.render.call(
     vnode = instance.render.call(
       instance.$proxy,
       instance.$proxy,
       instance.$props,
       instance.$props,
@@ -110,6 +121,10 @@ export function renderInstanceRoot(instance: ComponentInstance): VNode {
       instance.$attrs,
       instance.$attrs,
       instance.$parentVNode
       instance.$parentVNode
     )
     )
+    if (__DEV__) {
+      isRendering = false
+    }
+    unsetCurrentInstance()
   } catch (err) {
   } catch (err) {
     handleError(err, instance, ErrorTypes.RENDER)
     handleError(err, instance, ErrorTypes.RENDER)
   }
   }

+ 16 - 10
packages/runtime-core/src/experimental/hooks.ts

@@ -24,7 +24,7 @@ let currentInstance: ComponentInstance | null = null
 let isMounting: boolean = false
 let isMounting: boolean = false
 let callIndex: number = 0
 let callIndex: number = 0
 
 
-const hooksState = new WeakMap<ComponentInstance, HookState>()
+const hooksStateMap = new WeakMap<ComponentInstance, HookState>()
 
 
 export function setCurrentInstance(instance: ComponentInstance) {
 export function setCurrentInstance(instance: ComponentInstance) {
   currentInstance = instance
   currentInstance = instance
@@ -36,6 +36,18 @@ export function unsetCurrentInstance() {
   currentInstance = null
   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] {
 export function useState<T>(initial: T): [T, (newValue: T) => void] {
   if (!currentInstance) {
   if (!currentInstance) {
     throw new Error(
     throw new Error(
@@ -43,7 +55,7 @@ export function useState<T>(initial: T): [T, (newValue: T) => void] {
     )
     )
   }
   }
   const id = ++callIndex
   const id = ++callIndex
-  const { state } = hooksState.get(currentInstance) as HookState
+  const { state } = getHookStateForInstance(currentInstance)
   const set = (newValue: any) => {
   const set = (newValue: any) => {
     state[id] = newValue
     state[id] = newValue
   }
   }
@@ -76,7 +88,7 @@ export function useEffect(rawEffect: Effect, deps?: any[]) {
       }
       }
     }
     }
     effect.current = rawEffect
     effect.current = rawEffect
-    ;(hooksState.get(currentInstance) as HookState).effects[id] = {
+    getHookStateForInstance(currentInstance).effects[id] = {
       effect,
       effect,
       cleanup,
       cleanup,
       deps
       deps
@@ -86,7 +98,7 @@ export function useEffect(rawEffect: Effect, deps?: any[]) {
     injectEffect(currentInstance, 'unmounted', cleanup)
     injectEffect(currentInstance, 'unmounted', cleanup)
     injectEffect(currentInstance, 'updated', effect)
     injectEffect(currentInstance, 'updated', effect)
   } else {
   } else {
-    const record = (hooksState.get(currentInstance) as HookState).effects[id]
+    const record = getHookStateForInstance(currentInstance).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 || deps.some((d, i) => d !== prevDeps[i])) {
@@ -110,12 +122,6 @@ function injectEffect(
 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
-    created() {
-      hooksState.set((this as any)._self, {
-        state: observable({}),
-        effects: []
-      })
-    }
     render(props: Data, slots: Slots, attrs: Data, parentVNode: VNode) {
     render(props: Data, slots: Slots, attrs: Data, parentVNode: VNode) {
       setCurrentInstance((this as any)._self)
       setCurrentInstance((this as any)._self)
       const ret = render(props, slots, attrs, parentVNode)
       const ret = render(props, slots, attrs, parentVNode)