feat: implement basic hooks

This commit is contained in:
Evan You 2018-10-27 22:10:25 -04:00
parent 6982f755fd
commit 832d715afe
5 changed files with 131 additions and 6 deletions

View File

@ -172,10 +172,7 @@ export function mergeComponentOptions(to: any, from: any): ComponentOptions {
res[key] = value
} else {
// merge lifecycle hooks
res[key] = function(...args: any[]) {
existing.call(this, ...args)
value.call(this, ...args)
}
res[key] = mergeLifecycleHooks(existing, value)
}
} else if (isArray(value) && isArray(existing)) {
res[key] = existing.concat(value)
@ -188,6 +185,13 @@ export function mergeComponentOptions(to: any, from: any): ComponentOptions {
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 {
// TODO: backwards compat requires recursive merge,
// but maybe we should just warn if we detect clashing keys

View File

@ -57,7 +57,7 @@ const renderProxyHandlers = {
receiver: any
): boolean {
if (__DEV__) {
if (isReservedKey(key)) {
if (isReservedKey(key) && key in target) {
// TODO warn setting immutable properties
return false
}

View File

@ -1155,7 +1155,7 @@ export function createRenderer(options: RendererOptions) {
const {
$proxy,
$options: { beforeMount, mounted, renderTracked, renderTriggered }
$options: { beforeMount, renderTracked, renderTriggered }
} = instance
if (beforeMount) {
@ -1194,6 +1194,10 @@ export function createRenderer(options: RendererOptions) {
if (vnode.ref) {
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) {
lifecycleHooks.push(() => {
mounted.call($proxy)

View File

@ -18,6 +18,7 @@ export { createAsyncComponent } from './optional/asyncComponent'
export { KeepAlive } from './optional/keepAlive'
export { mixins } from './optional/mixins'
export { EventEmitter } from './optional/eventEmitter'
export { withHooks, useState, useEffect } from './optional/hooks'
// flags & types
export { ComponentType, ComponentClass, FunctionalComponent } from './component'

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