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

View File

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

View File

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

View File

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

View File

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

View File

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