From: Evan You Date: Sun, 28 Oct 2018 23:15:18 +0000 (-0400) Subject: feat: make hooks usable inside classes X-Git-Tag: v3.0.0-alpha.0~1062 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=894bead9141af17b8368d04aded010d8f6ec382b;p=thirdparty%2Fvuejs%2Fcore.git feat: make hooks usable inside classes --- diff --git a/packages/runtime-core/__tests__/hooks.spec.ts b/packages/runtime-core/__tests__/hooks.spec.ts index 51ccd39fde..84e4471994 100644 --- a/packages/runtime-core/__tests__/hooks.spec.ts +++ b/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' 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(`
0
`) + + triggerEvent(counter.$el, 'click') + await nextTick() + expect(serialize(counter.$el)).toBe(`
1
`) + }) + + 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(`
0
`) + + triggerEvent(counter.$el, 'click') + await nextTick() + expect(serialize(counter.$el)).toBe(`
1
`) + }) + it('useEffect with empty keys', async () => { // TODO }) diff --git a/packages/runtime-core/src/component.ts b/packages/runtime-core/src/component.ts index e333554c4e..d57c8f885a 100644 --- a/packages/runtime-core/src/component.ts +++ b/packages/runtime-core/src/component.ts @@ -45,6 +45,7 @@ interface PublicInstanceMethods { export interface APIMethods

{ data(): Partial + hooks(): any render(props: Readonly

, 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) { diff --git a/packages/runtime-core/src/componentOptions.ts b/packages/runtime-core/src/componentOptions.ts index 12af237a4b..e1e98136ef 100644 --- a/packages/runtime-core/src/componentOptions.ts +++ b/packages/runtime-core/src/componentOptions.ts @@ -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, diff --git a/packages/runtime-core/src/componentProxy.ts b/packages/runtime-core/src/componentProxy.ts index 6fcaa1f71b..92525a2088 100644 --- a/packages/runtime-core/src/componentProxy.ts +++ b/packages/runtime-core/src/componentProxy.ts @@ -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, 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 { diff --git a/packages/runtime-core/src/componentUtils.ts b/packages/runtime-core/src/componentUtils.ts index a3e218b29c..783fd6ed01 100644 --- a/packages/runtime-core/src/componentUtils.ts +++ b/packages/runtime-core/src/componentUtils.ts @@ -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) } diff --git a/packages/runtime-core/src/experimental/hooks.ts b/packages/runtime-core/src/experimental/hooks.ts index c195c2c765..241adfe178 100644 --- a/packages/runtime-core/src/experimental/hooks.ts +++ b/packages/runtime-core/src/experimental/hooks.ts @@ -24,7 +24,7 @@ let currentInstance: ComponentInstance | null = null let isMounting: boolean = false let callIndex: number = 0 -const hooksState = new WeakMap() +const hooksStateMap = new WeakMap() 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(initial: T): [T, (newValue: T) => void] { if (!currentInstance) { throw new Error( @@ -43,7 +55,7 @@ export function useState(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)