Browse Source

feat: detailed info in renderTriggered + hint for skipping slot updates

Evan You 7 years ago
parent
commit
64029b4a54

+ 40 - 4
packages/runtime-core/src/componentUtils.ts

@@ -204,9 +204,7 @@ export function shouldUpdateComponent(
   const { data: prevProps, childFlags: prevChildFlags } = prevVNode
   const { data: nextProps, childFlags: nextChildFlags } = nextVNode
   // If has different slots content, or has non-compiled slots,
-  // the child needs to be force updated. It's ok to call $forceUpdate
-  // again even if props update has already queued an update, as the
-  // scheduler will not queue the same update twice.
+  // the child needs to be force updated.
   if (
     prevChildFlags !== nextChildFlags ||
     (nextChildFlags & ChildrenFlags.DYNAMIC_SLOTS) > 0
@@ -235,11 +233,49 @@ export function shouldUpdateComponent(
   return false
 }
 
+// DEV only
 export function getReasonForComponentUpdate(
   prevVNode: VNode,
   nextVNode: VNode
 ): any {
-  // TODO: extract more detailed information on why the component is updating
+  const reasons = []
+  const { childFlags: prevChildFlags } = prevVNode
+  const { childFlags: nextChildFlags } = nextVNode
+  if (
+    prevChildFlags !== nextChildFlags ||
+    (nextChildFlags & ChildrenFlags.DYNAMIC_SLOTS) > 0
+  ) {
+    reasons.push({
+      type: `slots may have changed`,
+      tip: `use function slots + $stable: true to avoid slot-triggered child updates.`
+    })
+  }
+  const prevProps = prevVNode.data || EMPTY_OBJ
+  const nextProps = nextVNode.data || EMPTY_OBJ
+  for (const key in nextProps) {
+    if (nextProps[key] !== prevProps[key]) {
+      reasons.push({
+        type: 'prop changed',
+        key,
+        value: nextProps[key],
+        oldValue: prevProps[key]
+      })
+    }
+  }
+  for (const key in prevProps) {
+    if (!(key in nextProps)) {
+      reasons.push({
+        type: 'prop changed',
+        key,
+        value: undefined,
+        oldValue: prevProps[key]
+      })
+    }
+  }
+  return {
+    type: 'triggered by parent',
+    reasons
+  }
 }
 
 export function createComponentClassFromOptions(

+ 2 - 1
packages/runtime-core/src/h.ts

@@ -22,7 +22,8 @@ export const Portal = Symbol()
 type RawChildType = VNode | string | number | boolean | null | undefined
 
 export type RawSlots = {
-  [name: string]: () => RawChildrenType
+  $stable?: boolean
+  [name: string]: RawChildType | (() => RawChildrenType)
 }
 
 export type RawChildrenType = RawChildType | RawChildType[]

+ 8 - 0
packages/runtime-core/src/vdom.ts

@@ -10,6 +10,7 @@ import { RawChildrenType, RawSlots } from './h'
 import { FunctionalHandle } from './createRenderer'
 
 const handlersRE = /^on|^vnode/
+const STABLE_SLOTS_HINT = '$stable'
 
 // Vue core is platform agnostic, so we are not using Element for "DOM" nodes.
 export interface RenderNode {
@@ -226,6 +227,10 @@ export function createComponentVNode(
       } else if (isObject(children) && !(children as any)._isVNode) {
         // slot object as children
         slots = children
+        // special manual optimization hint for raw render fn users
+        if (slots[STABLE_SLOTS_HINT]) {
+          childFlags = ChildrenFlags.STABLE_SLOTS
+        }
       } else {
         slots = { default: () => children }
       }
@@ -418,6 +423,9 @@ function normalizeSlots(slots: { [name: string]: any }): Slots {
   }
   const normalized = { _normalized: true } as any
   for (const name in slots) {
+    if (name === STABLE_SLOTS_HINT) {
+      continue
+    }
     normalized[name] = (...args: any[]) => normalizeSlot(slots[name](...args))
   }
   return normalized