]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat: warning traces & error handling for functional render
authorEvan You <yyx990803@gmail.com>
Thu, 11 Oct 2018 21:14:39 +0000 (17:14 -0400)
committerEvan You <yyx990803@gmail.com>
Thu, 11 Oct 2018 21:14:39 +0000 (17:14 -0400)
packages/core/src/component.ts
packages/core/src/componentUtils.ts
packages/core/src/createRenderer.ts
packages/core/src/errorHandling.ts
packages/core/src/warning.ts

index 41997f369373b87a59cf95971e166a98dff0766e..fcc168699b926e1cfc59149127eab32494a8b831 100644 (file)
@@ -42,7 +42,6 @@ export interface ComponentInstance<P = {}, D = {}> extends InternalComponent {
 
   data?(): Partial<D>
   render(props: Readonly<P>, slots: Slots, attrs: Data): any
-  renderError?(e: Error): any
   renderTracked?(e: DebuggerEvent): void
   renderTriggered?(e: DebuggerEvent): void
   beforeCreate?(): void
@@ -56,7 +55,8 @@ export interface ComponentInstance<P = {}, D = {}> extends InternalComponent {
   errorCaptured?(): (
     err: Error,
     type: ErrorTypes,
-    target: ComponentInstance
+    instance: ComponentInstance | null,
+    vnode: VNode
   ) => boolean | void
   activated?(): void
   deactivated?(): void
index a902ad95908f30806ba22d596b5a6d0a5590b345..d50a0dee1d5b4efa63e7944890f33dbba25c4f35 100644 (file)
@@ -2,10 +2,15 @@ import { VNodeFlags } from './flags'
 import { EMPTY_OBJ } from './utils'
 import { h } from './h'
 import { VNode, MountedVNode, createFragment } from './vdom'
-import { Component, ComponentInstance, ComponentClass } from './component'
+import {
+  Component,
+  ComponentInstance,
+  ComponentClass,
+  FunctionalComponent
+} from './component'
 import { createTextVNode, cloneVNode } from './vdom'
 import { initializeState } from './componentState'
-import { initializeProps } from './componentProps'
+import { initializeProps, resolveProps } from './componentProps'
 import {
   initializeComputed,
   resolveComputedOptions,
@@ -104,19 +109,24 @@ export function renderInstanceRoot(instance: ComponentInstance): VNode {
       instance.$slots,
       instance.$attrs
     )
-  } catch (e1) {
-    handleError(e1, instance, ErrorTypes.RENDER)
-    if (__DEV__ && instance.renderError) {
-      try {
-        vnode = instance.renderError.call(instance.$proxy, e1)
-      } catch (e2) {
-        handleError(e2, instance, ErrorTypes.RENDER_ERROR)
-      }
-    }
+  } catch (err) {
+    handleError(err, instance, ErrorTypes.RENDER)
   }
   return normalizeComponentRoot(vnode, instance.$parentVNode)
 }
 
+export function renderFunctionalRoot(vnode: VNode): VNode {
+  const render = vnode.tag as FunctionalComponent
+  const { props, attrs } = resolveProps(vnode.data, render.props)
+  let subTree
+  try {
+    subTree = render(props, vnode.slots || EMPTY_OBJ, attrs || EMPTY_OBJ)
+  } catch (err) {
+    handleError(err, vnode, ErrorTypes.RENDER)
+  }
+  return normalizeComponentRoot(subTree, vnode)
+}
+
 export function teardownComponentInstance(instance: ComponentInstance) {
   if (instance._unmounted) {
     return
@@ -132,7 +142,7 @@ export function teardownComponentInstance(instance: ComponentInstance) {
   teardownWatch(instance)
 }
 
-export function normalizeComponentRoot(
+function normalizeComponentRoot(
   vnode: any,
   componentVNode: VNode | null
 ): VNode {
index 49cd3d1def188f1a92b50cff587481ae92639eb0..1362402334178b470bc3f1058b9ed8f620abbd4e 100644 (file)
@@ -16,15 +16,16 @@ import {
   FunctionalComponent,
   ComponentClass
 } from './component'
-import { updateProps, resolveProps } from './componentProps'
+import { updateProps } from './componentProps'
 import {
   renderInstanceRoot,
+  renderFunctionalRoot,
   createComponentInstance,
   teardownComponentInstance,
-  normalizeComponentRoot,
   shouldUpdateFunctionalComponent
 } from './componentUtils'
 import { KeepAliveSymbol } from './optional/keepAlive'
+import { pushContext, popContext } from './warning'
 
 interface NodeOps {
   createElement: (tag: string, isSVG?: boolean) => any
@@ -118,10 +119,8 @@ export function createRenderer(options: RendererOptions) {
     const { flags } = vnode
     if (flags & VNodeFlags.ELEMENT) {
       mountElement(vnode, container, contextVNode, isSVG, endNode)
-    } else if (flags & VNodeFlags.COMPONENT_STATEFUL) {
-      mountStatefulComponent(vnode, container, contextVNode, isSVG, endNode)
-    } else if (flags & VNodeFlags.COMPONENT_FUNCTIONAL) {
-      mountFunctionalComponent(vnode, container, contextVNode, isSVG, endNode)
+    } else if (flags & VNodeFlags.COMPONENT) {
+      mountComponent(vnode, container, contextVNode, isSVG, endNode)
     } else if (flags & VNodeFlags.TEXT) {
       mountText(vnode, container, endNode)
     } else if (flags & VNodeFlags.FRAGMENT) {
@@ -198,7 +197,7 @@ export function createRenderer(options: RendererOptions) {
     })
   }
 
-  function mountStatefulComponent(
+  function mountComponent(
     vnode: VNode,
     container: RenderNode | null,
     contextVNode: MountedVNode | null,
@@ -206,6 +205,27 @@ export function createRenderer(options: RendererOptions) {
     endNode: RenderNode | null
   ) {
     vnode.contextVNode = contextVNode
+    if (__DEV__) {
+      pushContext(vnode)
+    }
+    const { flags } = vnode
+    if (flags & VNodeFlags.COMPONENT_STATEFUL) {
+      mountStatefulComponent(vnode, container, contextVNode, isSVG, endNode)
+    } else {
+      mountFunctionalComponent(vnode, container, contextVNode, isSVG, endNode)
+    }
+    if (__DEV__) {
+      popContext()
+    }
+  }
+
+  function mountStatefulComponent(
+    vnode: VNode,
+    container: RenderNode | null,
+    contextVNode: MountedVNode | null,
+    isSVG: boolean,
+    endNode: RenderNode | null
+  ) {
     if (vnode.flags & VNodeFlags.COMPONENT_STATEFUL_KEPT_ALIVE) {
       // kept-alive
       activateComponentInstance(vnode, container, endNode)
@@ -228,14 +248,7 @@ export function createRenderer(options: RendererOptions) {
     isSVG: boolean,
     endNode: RenderNode | null
   ) {
-    vnode.contextVNode = contextVNode
-    const { tag, data, slots } = vnode
-    const render = tag as FunctionalComponent
-    const { props, attrs } = resolveProps(data, render.props)
-    const subTree = (vnode.children = normalizeComponentRoot(
-      render(props, slots || EMPTY_OBJ, attrs || EMPTY_OBJ),
-      vnode
-    ))
+    const subTree = (vnode.children = renderFunctionalRoot(vnode))
     mount(subTree, container, vnode as MountedVNode, isSVG, endNode)
     vnode.el = subTree.el as RenderNode
   }
@@ -443,6 +456,9 @@ export function createRenderer(options: RendererOptions) {
     contextVNode: MountedVNode | null,
     isSVG: boolean
   ) {
+    if (__DEV__) {
+      pushContext(nextVNode)
+    }
     nextVNode.contextVNode = contextVNode
     const { tag, flags } = nextVNode
     if (tag !== prevVNode.tag) {
@@ -458,6 +474,9 @@ export function createRenderer(options: RendererOptions) {
         isSVG
       )
     }
+    if (__DEV__) {
+      popContext()
+    }
   }
 
   function patchStatefulComponent(prevVNode: MountedVNode, nextVNode: VNode) {
@@ -512,11 +531,7 @@ export function createRenderer(options: RendererOptions) {
     }
 
     if (shouldUpdate) {
-      const { props, attrs } = resolveProps(nextData, render.props)
-      const nextTree = (nextVNode.children = normalizeComponentRoot(
-        render(props, nextSlots || EMPTY_OBJ, attrs || EMPTY_OBJ),
-        nextVNode
-      ))
+      const nextTree = (nextVNode.children = renderFunctionalRoot(nextVNode))
       patch(prevTree, nextTree, container, nextVNode as MountedVNode, isSVG)
       nextVNode.el = nextTree.el
     } else if (prevTree.flags & VNodeFlags.COMPONENT) {
@@ -630,7 +645,7 @@ export function createRenderer(options: RendererOptions) {
     isSVG: boolean
   ) {
     const refNode = platformNextSibling(getVNodeLastEl(prevVNode))
-    reinsertVNode(prevVNode, container)
+    removeVNode(prevVNode, container)
     mount(nextVNode, container, contextVNode, isSVG, refNode)
   }
 
@@ -657,10 +672,10 @@ export function createRenderer(options: RendererOptions) {
             )
             break
           case ChildrenFlags.NO_CHILDREN:
-            reinsertVNode(prevChildren as MountedVNode, container)
+            removeVNode(prevChildren as MountedVNode, container)
             break
           default:
-            reinsertVNode(prevChildren as MountedVNode, container)
+            removeVNode(prevChildren as MountedVNode, container)
             mountArrayChildren(
               nextChildren as VNode[],
               container,
@@ -781,7 +796,7 @@ export function createRenderer(options: RendererOptions) {
       }
     } else if (prevLength > nextLength) {
       for (i = commonLength; i < prevLength; i++) {
-        reinsertVNode(prevChildren[i], container)
+        removeVNode(prevChildren[i], container)
       }
     }
   }
@@ -856,7 +871,7 @@ export function createRenderer(options: RendererOptions) {
       }
     } else if (j > nextEnd) {
       while (j <= prevEnd) {
-        reinsertVNode(prevChildren[j++], container)
+        removeVNode(prevChildren[j++], container)
       }
     } else {
       let prevStart = j
@@ -885,7 +900,7 @@ export function createRenderer(options: RendererOptions) {
                 if (canRemoveWholeContent) {
                   canRemoveWholeContent = false
                   while (i > prevStart) {
-                    reinsertVNode(prevChildren[prevStart++], container)
+                    removeVNode(prevChildren[prevStart++], container)
                   }
                 }
                 if (pos > j) {
@@ -902,10 +917,10 @@ export function createRenderer(options: RendererOptions) {
               }
             }
             if (!canRemoveWholeContent && j > nextEnd) {
-              reinsertVNode(prevVNode, container)
+              removeVNode(prevVNode, container)
             }
           } else if (!canRemoveWholeContent) {
-            reinsertVNode(prevVNode, container)
+            removeVNode(prevVNode, container)
           }
         }
       } else {
@@ -927,7 +942,7 @@ export function createRenderer(options: RendererOptions) {
               if (canRemoveWholeContent) {
                 canRemoveWholeContent = false
                 while (i > prevStart) {
-                  reinsertVNode(prevChildren[prevStart++], container)
+                  removeVNode(prevChildren[prevStart++], container)
                 }
               }
               nextVNode = nextChildren[j]
@@ -943,10 +958,10 @@ export function createRenderer(options: RendererOptions) {
               patch(prevVNode, nextVNode, container, contextVNode, isSVG)
               patched++
             } else if (!canRemoveWholeContent) {
-              reinsertVNode(prevVNode, container)
+              removeVNode(prevVNode, container)
             }
           } else if (!canRemoveWholeContent) {
-            reinsertVNode(prevVNode, container)
+            removeVNode(prevVNode, container)
           }
         }
       }
@@ -1074,7 +1089,7 @@ export function createRenderer(options: RendererOptions) {
           null
         )
       } else if (childFlags === ChildrenFlags.SINGLE_VNODE) {
-        reinsertVNode(children as MountedVNode, vnode.tag as RenderNode)
+        removeVNode(children as MountedVNode, vnode.tag as RenderNode)
       }
     }
     if (ref) {
@@ -1096,21 +1111,21 @@ export function createRenderer(options: RendererOptions) {
     }
   }
 
-  function reinsertVNode(vnode: MountedVNode, container: RenderNode) {
+  function removeVNode(vnode: MountedVNode, container: RenderNode) {
     unmount(vnode)
     const { el, flags, children, childFlags } = vnode
     if (container && el) {
       if (flags & VNodeFlags.FRAGMENT) {
         switch (childFlags) {
           case ChildrenFlags.SINGLE_VNODE:
-            reinsertVNode(children as MountedVNode, container)
+            removeVNode(children as MountedVNode, container)
             break
           case ChildrenFlags.NO_CHILDREN:
             platformRemoveChild(container, el)
             break
           default:
             for (let i = 0; i < (children as MountedVNode[]).length; i++) {
-              reinsertVNode((children as MountedVNode[])[i], container)
+              removeVNode((children as MountedVNode[])[i], container)
             }
         }
       } else {
@@ -1130,7 +1145,7 @@ export function createRenderer(options: RendererOptions) {
       platformClearContent(container)
     } else {
       for (let i = 0; i < children.length; i++) {
-        reinsertVNode(children[i], container)
+        removeVNode(children[i], container)
       }
     }
   }
@@ -1220,6 +1235,9 @@ export function createRenderer(options: RendererOptions) {
     instance: ComponentInstance,
     isSVG: boolean
   ) {
+    if (__DEV__ && instance.$parentVNode) {
+      pushContext(instance.$parentVNode as VNode)
+    }
     const prevVNode = instance.$vnode
 
     if (instance.beforeUpdate) {
@@ -1275,6 +1293,10 @@ export function createRenderer(options: RendererOptions) {
         }
       })
     }
+
+    if (__DEV__ && instance.$parentVNode) {
+      popContext()
+    }
   }
 
   function unmountComponentInstance(instance: ComponentInstance) {
@@ -1380,7 +1402,7 @@ export function createRenderer(options: RendererOptions) {
         patch(prevVNode, vnode, container, null, false)
         container.vnode = vnode
       } else {
-        reinsertVNode(prevVNode, container)
+        removeVNode(prevVNode, container)
         container.vnode = null
       }
     }
index c740457f9924db1135dac44d6e475f7f6d44a6f9..863485e48743dea583a4e27e3fce8d4cf5c15f89 100644 (file)
@@ -1,4 +1,7 @@
 import { ComponentInstance } from './component'
+import { warn } from './warning'
+import { VNode } from './vdom'
+import { VNodeFlags } from './flags'
 
 export const enum ErrorTypes {
   BEFORE_CREATE = 1,
@@ -11,7 +14,6 @@ export const enum ErrorTypes {
   DESTROYED,
   ERROR_CAPTURED,
   RENDER,
-  RENDER_ERROR,
   WATCH_CALLBACK,
   NATIVE_EVENT_HANDLER,
   COMPONENT_EVENT_HANDLER
@@ -28,7 +30,6 @@ const ErrorTypeStrings: Record<number, string> = {
   [ErrorTypes.DESTROYED]: 'destroyed lifecycle hook',
   [ErrorTypes.ERROR_CAPTURED]: 'errorCaptured lifecycle hook',
   [ErrorTypes.RENDER]: 'render function',
-  [ErrorTypes.RENDER_ERROR]: 'renderError function',
   [ErrorTypes.WATCH_CALLBACK]: 'watcher callback',
   [ErrorTypes.NATIVE_EVENT_HANDLER]: 'native event handler',
   [ErrorTypes.COMPONENT_EVENT_HANDLER]: 'component event handler'
@@ -36,21 +37,39 @@ const ErrorTypeStrings: Record<number, string> = {
 
 export function handleError(
   err: Error,
-  instance: ComponentInstance,
+  instance: ComponentInstance | VNode,
   type: ErrorTypes
 ) {
-  let cur = instance
-  while (cur.$parent) {
-    cur = cur.$parent
+  const isFunctional = (instance as VNode)._isVNode
+  let cur: ComponentInstance | null = null
+  if (isFunctional) {
+    let vnode = instance as VNode | null
+    while (vnode && !(vnode.flags & VNodeFlags.COMPONENT_STATEFUL)) {
+      vnode = vnode.contextVNode
+    }
+    if (vnode) {
+      cur = vnode.children as ComponentInstance
+    }
+  } else {
+    cur = (instance as ComponentInstance).$parent
+  }
+  while (cur) {
     const handler = cur.errorCaptured
     if (handler) {
       try {
-        const captured = handler.call(cur, err, type, instance)
+        const captured = handler.call(
+          cur,
+          err,
+          type,
+          isFunctional ? null : instance,
+          isFunctional ? instance : (instance as ComponentInstance).$parentVNode
+        )
         if (captured) return
       } catch (err2) {
         logError(err2, ErrorTypes.ERROR_CAPTURED)
       }
     }
+    cur = cur.$parent
   }
   logError(err, type)
 }
@@ -58,7 +77,7 @@ export function handleError(
 function logError(err: Error, type: ErrorTypes) {
   if (__DEV__) {
     const info = ErrorTypeStrings[type]
-    console.warn(`Unhandled error${info ? ` in ${info}` : ``}:`)
+    warn(`Unhandled error${info ? ` in ${info}` : ``}`)
     console.error(err)
   } else {
     throw err
index 4d42ddca15692ac45147566198cc9a73fc811c54..32ed0616ebae27d84ce0c78cb97d209542d29962 100644 (file)
@@ -1,22 +1,14 @@
 import { ComponentType, ComponentClass, FunctionalComponent } from './component'
 import { EMPTY_OBJ } from './utils'
+import { VNode } from './vdom'
 
-// TODO push vnodes instead
-// component vnodes get a new property (contextVNode) which points to the
-// parent component (stateful or functional)
-// this way we can use any component vnode to construct a trace that inludes
-// functional and stateful components.
+let stack: VNode[] = []
 
-// in createRenderer, parentComponent should be replced by ctx
-// $parent logic should also accomodate
-
-let stack: ComponentType[] = []
-
-export function pushComponent(c: ComponentType) {
-  stack.push(c)
+export function pushContext(vnode: VNode) {
+  stack.push(vnode)
 }
 
-export function popComponent() {
+export function popContext() {
   stack.pop()
 }
 
@@ -26,33 +18,42 @@ export function warn(msg: string) {
 }
 
 function getComponentTrace(): string {
-  const current = stack[stack.length - 1]
+  let current: VNode | null | undefined = stack[stack.length - 1]
   if (!current) {
-    return ''
+    return '\nat <Root/>'
   }
-  // we can't just use the stack itself, because it will be incomplete
-  // during updates
-  // check recursive
+
+  // we can't just use the stack because it will be incomplete during updates
+  // that did not start from the root. Re-construct the parent chain using
+  // contextVNode information.
   const normlaizedStack: Array<{
-    type: ComponentType
+    type: VNode
     recurseCount: number
   }> = []
-  stack.forEach(c => {
-    const last = normlaizedStack[normlaizedStack.length - 1]
-    if (last && last.type === c) {
+
+  while (current) {
+    const last = normlaizedStack[0]
+    if (last && last.type === current) {
       last.recurseCount++
     } else {
-      normlaizedStack.push({ type: c, recurseCount: 0 })
+      normlaizedStack.unshift({
+        type: current,
+        recurseCount: 0
+      })
     }
-  })
+    current = current.contextVNode
+  }
+
   return (
-    `\n\nfound in\n\n` +
+    `\nat ` +
     normlaizedStack
       .map(({ type, recurseCount }, i) => {
-        const padding = i === 0 ? '---> ' : ' '.repeat(5 + i * 2)
+        const padding = i === 0 ? '' : '  '.repeat(i + 1)
         const postfix =
           recurseCount > 0 ? `... (${recurseCount} recursive calls)` : ``
-        return padding + formatComponentName(type) + postfix
+        return (
+          padding + formatComponentName(type.tag as ComponentType) + postfix
+        )
       })
       .join('\n')
   )
@@ -66,12 +67,14 @@ function formatComponentName(c: ComponentType, includeFile?: boolean): string {
   let name: string
   let file: string | null = null
 
-  if (c.prototype.render) {
+  if (c.prototype && c.prototype.render) {
+    // stateful
     const cc = c as ComponentClass
     const options = cc.options || EMPTY_OBJ
     name = options.displayName || cc.name
     file = options.__file
   } else {
+    // functional
     const fc = c as FunctionalComponent
     name = fc.displayName || fc.name
   }