feat: make hooks usable inside classes
This commit is contained in:
parent
98782b326a
commit
894bead914
@ -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
|
||||||
})
|
})
|
||||||
|
@ -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) {
|
||||||
|
@ -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,
|
||||||
|
@ -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 (
|
} else if ((i = target._rawData) !== null && i.hasOwnProperty(key)) {
|
||||||
target._rawData !== null &&
|
|
||||||
target._rawData.hasOwnProperty(key)
|
|
||||||
) {
|
|
||||||
// data
|
// data
|
||||||
|
// make sure to return from $data to register dependency
|
||||||
return target.$data[key]
|
return target.$data[key]
|
||||||
} else if (
|
} else if ((i = target.$options.props) != null && i.hasOwnProperty(key)) {
|
||||||
target.$options.props != null &&
|
|
||||||
target.$options.props.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 &&
|
(i = target._computedGetters) !== null &&
|
||||||
target._computedGetters.hasOwnProperty(key)
|
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)) {
|
if (__DEV__ && isRendering && !(key in target)) {
|
||||||
// TODO warn non-present property
|
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 (
|
if ((i = target.$options.props) != null && i.hasOwnProperty(key)) {
|
||||||
target.$options.props != null &&
|
warn(`failed setting property "${key}": props are immutable.`)
|
||||||
target.$options.props.hasOwnProperty(key)
|
|
||||||
) {
|
|
||||||
// TODO warn 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 {
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
Loading…
Reference in New Issue
Block a user