]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat: implement basic hooks
authorEvan You <yyx990803@gmail.com>
Sun, 28 Oct 2018 02:10:25 +0000 (22:10 -0400)
committerEvan You <yyx990803@gmail.com>
Sun, 28 Oct 2018 02:10:25 +0000 (22:10 -0400)
packages/runtime-core/src/componentOptions.ts
packages/runtime-core/src/componentProxy.ts
packages/runtime-core/src/createRenderer.ts
packages/runtime-core/src/index.ts
packages/runtime-core/src/optional/hooks.ts [new file with mode: 0644]

index 77fbb5158309abb79d778a5282526d52c4b51f5d..12af237a4b7a2f50519b6ae00cad8ba67b8b93a5 100644 (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
index a42e812d7ec8ed96206238bc1adea3e12f2dce3a..6fcaa1f71b9846da49bf0dcfe902c2cb42e69833 100644 (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
       }
index db06a64cd6b4c3bdd34e76e501f483de3f11e667..9a46a273352269b787fde6f7ef7c4b23de5612bd 100644 (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)
index 889d0fb4851b2c7290df21fc943126f73d2d5aee..cfb457e35dc510d045e4672209e4f063a1d2ecac 100644 (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'
diff --git a/packages/runtime-core/src/optional/hooks.ts b/packages/runtime-core/src/optional/hooks.ts
new file mode 100644 (file)
index 0000000..50d7f44
--- /dev/null
@@ -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
+}