feat: implement basic hooks
This commit is contained in:
parent
6982f755fd
commit
832d715afe
@ -172,10 +172,7 @@ export function mergeComponentOptions(to: any, from: any): ComponentOptions {
|
|||||||
res[key] = value
|
res[key] = value
|
||||||
} else {
|
} else {
|
||||||
// merge lifecycle hooks
|
// merge lifecycle hooks
|
||||||
res[key] = function(...args: any[]) {
|
res[key] = mergeLifecycleHooks(existing, value)
|
||||||
existing.call(this, ...args)
|
|
||||||
value.call(this, ...args)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else if (isArray(value) && isArray(existing)) {
|
} else if (isArray(value) && isArray(existing)) {
|
||||||
res[key] = existing.concat(value)
|
res[key] = existing.concat(value)
|
||||||
@ -188,6 +185,13 @@ export function mergeComponentOptions(to: any, from: any): ComponentOptions {
|
|||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function mergeLifecycleHooks(a: Function, b: Function): Function {
|
||||||
|
return function(...args: any[]) {
|
||||||
|
a.call(this, ...args)
|
||||||
|
b.call(this, ...args)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function mergeDataFn(a: Function, b: Function): Function {
|
export function mergeDataFn(a: Function, b: Function): Function {
|
||||||
// TODO: backwards compat requires recursive merge,
|
// TODO: backwards compat requires recursive merge,
|
||||||
// but maybe we should just warn if we detect clashing keys
|
// but maybe we should just warn if we detect clashing keys
|
||||||
|
@ -57,7 +57,7 @@ const renderProxyHandlers = {
|
|||||||
receiver: any
|
receiver: any
|
||||||
): boolean {
|
): boolean {
|
||||||
if (__DEV__) {
|
if (__DEV__) {
|
||||||
if (isReservedKey(key)) {
|
if (isReservedKey(key) && key in target) {
|
||||||
// TODO warn setting immutable properties
|
// TODO warn setting immutable properties
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -1155,7 +1155,7 @@ export function createRenderer(options: RendererOptions) {
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
$proxy,
|
$proxy,
|
||||||
$options: { beforeMount, mounted, renderTracked, renderTriggered }
|
$options: { beforeMount, renderTracked, renderTriggered }
|
||||||
} = instance
|
} = instance
|
||||||
|
|
||||||
if (beforeMount) {
|
if (beforeMount) {
|
||||||
@ -1194,6 +1194,10 @@ export function createRenderer(options: RendererOptions) {
|
|||||||
if (vnode.ref) {
|
if (vnode.ref) {
|
||||||
mountRef(vnode.ref, $proxy)
|
mountRef(vnode.ref, $proxy)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// retrieve mounted value right before calling it so that we get
|
||||||
|
// to inject effects in first render
|
||||||
|
const { mounted } = instance.$options
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
lifecycleHooks.push(() => {
|
lifecycleHooks.push(() => {
|
||||||
mounted.call($proxy)
|
mounted.call($proxy)
|
||||||
|
@ -18,6 +18,7 @@ export { createAsyncComponent } from './optional/asyncComponent'
|
|||||||
export { KeepAlive } from './optional/keepAlive'
|
export { KeepAlive } from './optional/keepAlive'
|
||||||
export { mixins } from './optional/mixins'
|
export { mixins } from './optional/mixins'
|
||||||
export { EventEmitter } from './optional/eventEmitter'
|
export { EventEmitter } from './optional/eventEmitter'
|
||||||
|
export { withHooks, useState, useEffect } from './optional/hooks'
|
||||||
|
|
||||||
// flags & types
|
// flags & types
|
||||||
export { ComponentType, ComponentClass, FunctionalComponent } from './component'
|
export { ComponentType, ComponentClass, FunctionalComponent } from './component'
|
||||||
|
116
packages/runtime-core/src/optional/hooks.ts
Normal file
116
packages/runtime-core/src/optional/hooks.ts
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
import { ComponentInstance, APIMethods } from '../component'
|
||||||
|
import { mergeLifecycleHooks, Data } from '../componentOptions'
|
||||||
|
import { VNode, Slots } from '../vdom'
|
||||||
|
import { observable } from '@vue/observer'
|
||||||
|
|
||||||
|
type RawEffect = () => (() => void) | void
|
||||||
|
|
||||||
|
type Effect = RawEffect & {
|
||||||
|
current?: RawEffect | null | void
|
||||||
|
}
|
||||||
|
|
||||||
|
type EffectRecord = {
|
||||||
|
effect: Effect
|
||||||
|
deps: any[] | void
|
||||||
|
}
|
||||||
|
|
||||||
|
type ComponentInstanceWithHook = ComponentInstance & {
|
||||||
|
_state: Record<number, any>
|
||||||
|
_effects: EffectRecord[]
|
||||||
|
}
|
||||||
|
|
||||||
|
let currentInstance: ComponentInstanceWithHook | null = null
|
||||||
|
let isMounting: boolean = false
|
||||||
|
let callIndex: number = 0
|
||||||
|
|
||||||
|
export function useState(initial: any) {
|
||||||
|
if (!currentInstance) {
|
||||||
|
throw new Error(
|
||||||
|
`useState must be called in a function passed to withHooks.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const id = ++callIndex
|
||||||
|
const state = currentInstance._state
|
||||||
|
const set = (newValue: any) => {
|
||||||
|
state[id] = newValue
|
||||||
|
}
|
||||||
|
if (isMounting) {
|
||||||
|
set(initial)
|
||||||
|
}
|
||||||
|
return [state[id], set]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useEffect(rawEffect: Effect, deps?: any[]) {
|
||||||
|
if (!currentInstance) {
|
||||||
|
throw new Error(
|
||||||
|
`useEffect must be called in a function passed to withHooks.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const id = ++callIndex
|
||||||
|
if (isMounting) {
|
||||||
|
const cleanup: Effect = () => {
|
||||||
|
const { current } = cleanup
|
||||||
|
if (current) {
|
||||||
|
current()
|
||||||
|
cleanup.current = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const effect: Effect = () => {
|
||||||
|
cleanup()
|
||||||
|
const { current } = effect
|
||||||
|
if (current) {
|
||||||
|
effect.current = current()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
effect.current = rawEffect
|
||||||
|
|
||||||
|
currentInstance._effects[id] = {
|
||||||
|
effect,
|
||||||
|
deps
|
||||||
|
}
|
||||||
|
|
||||||
|
injectEffect(currentInstance, 'mounted', effect)
|
||||||
|
injectEffect(currentInstance, 'unmounted', cleanup)
|
||||||
|
if (!deps) {
|
||||||
|
injectEffect(currentInstance, 'updated', effect)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const { effect, deps: prevDeps = [] } = currentInstance._effects[id]
|
||||||
|
if (!deps || deps.some((d, i) => d !== prevDeps[i])) {
|
||||||
|
effect.current = rawEffect
|
||||||
|
} else {
|
||||||
|
effect.current = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function injectEffect(
|
||||||
|
instance: ComponentInstanceWithHook,
|
||||||
|
key: string,
|
||||||
|
effect: Effect
|
||||||
|
) {
|
||||||
|
const existing = instance.$options[key]
|
||||||
|
;(instance.$options as any)[key] = existing
|
||||||
|
? mergeLifecycleHooks(existing, effect)
|
||||||
|
: effect
|
||||||
|
}
|
||||||
|
|
||||||
|
export function withHooks<T extends APIMethods['render']>(render: T): T {
|
||||||
|
return {
|
||||||
|
displayName: render.name,
|
||||||
|
created() {
|
||||||
|
const { _self } = this
|
||||||
|
_self._state = observable({})
|
||||||
|
_self._effects = []
|
||||||
|
},
|
||||||
|
render(props: Data, slots: Slots, attrs: Data, parentVNode: VNode) {
|
||||||
|
const { _self } = this
|
||||||
|
callIndex = 0
|
||||||
|
currentInstance = _self
|
||||||
|
isMounting = !_self._mounted
|
||||||
|
const ret = render(props, slots, attrs, parentVNode)
|
||||||
|
currentInstance = null
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
} as any
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user