feat: make hooks usable inside classes

This commit is contained in:
Evan You 2018-10-28 19:15:18 -04:00
parent 98782b326a
commit 894bead914
6 changed files with 112 additions and 31 deletions

View 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
})

View 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) {

View 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,

View 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 {

View 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)
}

View 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)