]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat: make functional components time-slicable
authorEvan You <yyx990803@gmail.com>
Thu, 1 Nov 2018 08:05:09 +0000 (17:05 +0900)
committerEvan You <yyx990803@gmail.com>
Fri, 2 Nov 2018 21:31:30 +0000 (06:31 +0900)
packages/runtime-core/src/component.ts
packages/runtime-core/src/componentProps.ts
packages/runtime-core/src/componentProxy.ts
packages/runtime-core/src/componentUtils.ts
packages/runtime-core/src/createRenderer.ts
packages/runtime-core/src/vdom.ts
packages/scheduler/src/experimental.ts

index d57c8f885a69b2cd8780d048afeb5801ae71e9c2..e220056bf1febf5a61e602fac64805f8fc6d6e0c 100644 (file)
@@ -77,7 +77,6 @@ export interface ComponentClass extends ComponentClassOptions {
 
 export interface FunctionalComponent<P = {}> {
   (props: P, slots: Slots, attrs: Data, parentVNode: VNode): any
-  pure?: boolean
   props?: ComponentPropsOptions<P>
   displayName?: string
 }
index e8733d68f2ae34c56ad1765befa835ef6eea1ad6..3b7dcfa9a2a58cccb69a35a64465fc8f85a1ea32 100644 (file)
@@ -1,4 +1,4 @@
-import { immutable, unwrap, lock, unlock } from '@vue/observer'
+import { immutable, unwrap } from '@vue/observer'
 import { ComponentInstance } from './component'
 import {
   Data,
@@ -36,10 +36,12 @@ export function initializeProps(
   options: NormalizedPropsOptions | undefined,
   data: Data | null
 ) {
-  const [props, attrs] = resolveProps(data, options)
-  instance.$props = immutable(props === EMPTY_OBJ ? {} : props)
+  const { 0: props, 1: attrs } = resolveProps(data, options)
+  instance.$props = __DEV__ ? immutable(props) : props
   instance.$attrs = options
-    ? immutable(attrs === EMPTY_OBJ ? {} : attrs)
+    ? __DEV__
+      ? immutable(attrs)
+      : attrs
     : instance.$props
 }
 
@@ -115,47 +117,6 @@ export function resolveProps(
   return [props, attrs]
 }
 
-export function updateProps(
-  instance: ComponentInstance,
-  nextData: Data | null
-) {
-  // instance.$props and instance.$attrs are observables that should not be
-  // replaced. Instead, we mutate them to match latest props, which will trigger
-  // updates if any value that's been used in child component has changed.
-  const [nextProps, nextAttrs] = resolveProps(nextData, instance.$options.props)
-  // unlock to temporarily allow mutatiing props
-  unlock()
-  const props = instance.$props
-  const rawProps = unwrap(props)
-  const hasEmptyProps = nextProps === EMPTY_OBJ
-  for (const key in rawProps) {
-    if (hasEmptyProps || !nextProps.hasOwnProperty(key)) {
-      delete (props as any)[key]
-    }
-  }
-  if (!hasEmptyProps) {
-    for (const key in nextProps) {
-      ;(props as any)[key] = nextProps[key]
-    }
-  }
-  const attrs = instance.$attrs
-  if (attrs !== props) {
-    const rawAttrs = unwrap(attrs)
-    const hasEmptyAttrs = nextAttrs === EMPTY_OBJ
-    for (const key in rawAttrs) {
-      if (hasEmptyAttrs || !nextAttrs.hasOwnProperty(key)) {
-        delete attrs[key]
-      }
-    }
-    if (!hasEmptyAttrs) {
-      for (const key in nextAttrs) {
-        attrs[key] = nextAttrs[key]
-      }
-    }
-  }
-  lock()
-}
-
 export function normalizePropsOptions(
   raw: ComponentPropsOptions | void
 ): NormalizedPropsOptions | void {
index e9cc3cadcb46439353f40216f33a77b91d5a12cb..633741ac9d85a3065641403479d038cfbf8cc74d 100644 (file)
@@ -30,7 +30,6 @@ const renderProxyHandlers = {
       return target.$data[key]
     } else if ((i = target.$options.props) != null && i.hasOwnProperty(key)) {
       // props are only proxied if declared
-      // make sure to return from $props to register dependency
       return target.$props[key]
     } else if (
       (i = target._computedGetters) !== null &&
index 88a425006ffe7449a0e22e221d6ae79f313e14e0..59c3ddb52cd3a416406d6dc0dba6851ebb206bea 100644 (file)
@@ -1,4 +1,4 @@
-import { VNodeFlags } from './flags'
+import { VNodeFlags, ChildrenFlags } from './flags'
 import { EMPTY_OBJ, isArray, isObject } from '@vue/shared'
 import { h } from './h'
 import { VNode, MountedVNode, createFragment } from './vdom'
@@ -193,10 +193,22 @@ function normalizeComponentRoot(
   return vnode
 }
 
-export function shouldUpdateFunctionalComponent(
-  prevProps: Record<string, any> | null,
-  nextProps: Record<string, any> | null
+export function shouldUpdateComponent(
+  prevVNode: VNode,
+  nextVNode: VNode
 ): boolean {
+  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.
+  if (
+    prevChildFlags !== nextChildFlags ||
+    (nextChildFlags & ChildrenFlags.DYNAMIC_SLOTS) > 0
+  ) {
+    return true
+  }
   if (prevProps === nextProps) {
     return false
   }
index e8317c398fef0f6f20c9ef7b77ae6809c5eb647b..991092db88b2e9c4070c25ed74496c24a9525b16 100644 (file)
@@ -1,4 +1,4 @@
-import { autorun, stop } from '@vue/observer'
+import { autorun, stop, Autorun, immutable } from '@vue/observer'
 import { queueJob } from '@vue/scheduler'
 import { VNodeFlags, ChildrenFlags } from './flags'
 import { EMPTY_OBJ, reservedPropRE, isString } from '@vue/shared'
@@ -10,18 +10,18 @@ import {
   Ref,
   VNodeChildren
 } from './vdom'
-import { ComponentInstance, FunctionalComponent } from './component'
-import { updateProps } from './componentProps'
+import { ComponentInstance } from './component'
 import {
   renderInstanceRoot,
   renderFunctionalRoot,
   createComponentInstance,
   teardownComponentInstance,
-  shouldUpdateFunctionalComponent
+  shouldUpdateComponent
 } from './componentUtils'
 import { KeepAliveSymbol } from './optional/keepAlive'
-import { pushWarningContext, popWarningContext } from './warning'
+import { pushWarningContext, popWarningContext, warn } from './warning'
 import { handleError, ErrorTypes } from './errorHandling'
+import { resolveProps } from './componentProps'
 
 export interface NodeOps {
   createElement: (tag: string, isSVG?: boolean) => any
@@ -57,6 +57,13 @@ export interface RendererOptions {
   teardownVNode?: (vnode: VNode) => void
 }
 
+export interface FunctionalHandle {
+  current: VNode
+  prevTree: VNode
+  runner: Autorun
+  forceUpdate: () => void
+}
+
 // The whole mounting / patching / unmouting logic is placed inside this
 // single function so that we can create multiple renderes with different
 // platform definitions. This allows for use cases like creating a test
@@ -239,9 +246,64 @@ export function createRenderer(options: RendererOptions) {
     isSVG: boolean,
     endNode: RenderNode | null
   ) {
-    const subTree = (vnode.children = renderFunctionalRoot(vnode))
-    mount(subTree, container, vnode as MountedVNode, isSVG, endNode)
-    vnode.el = subTree.el as RenderNode
+    if (__DEV__ && vnode.ref) {
+      warn(
+        `cannot use ref on a functional component because there is no ` +
+          `instance to reference to.`
+      )
+    }
+
+    const handle: FunctionalHandle = (vnode.handle = {
+      current: vnode,
+      prevTree: null as any,
+      runner: null as any,
+      forceUpdate: null as any
+    })
+
+    const handleSchedulerError = (err: Error) => {
+      handleError(err, handle.current as VNode, ErrorTypes.SCHEDULER)
+    }
+
+    const queueUpdate = (handle.forceUpdate = () => {
+      queueJob(handle.runner, null, handleSchedulerError)
+    })
+
+    // we are using vnode.ref to store the functional component's update job
+    queueJob(
+      () => {
+        handle.runner = autorun(
+          () => {
+            if (handle.prevTree) {
+              // mounted
+              const { prevTree, current } = handle
+              const nextTree = (handle.prevTree = current.children = renderFunctionalRoot(
+                current
+              ))
+              patch(
+                prevTree as MountedVNode,
+                nextTree,
+                platformParentNode(current.el),
+                current as MountedVNode,
+                isSVG
+              )
+              current.el = nextTree.el
+            } else {
+              // initial mount
+              const subTree = (handle.prevTree = vnode.children = renderFunctionalRoot(
+                vnode
+              ))
+              mount(subTree, container, vnode as MountedVNode, isSVG, endNode)
+              vnode.el = subTree.el as RenderNode
+            }
+          },
+          {
+            scheduler: queueUpdate
+          }
+        )
+      },
+      null,
+      handleSchedulerError
+    )
   }
 
   function mountText(
@@ -462,13 +524,7 @@ export function createRenderer(options: RendererOptions) {
     } else if (flags & VNodeFlags.COMPONENT_STATEFUL) {
       patchStatefulComponent(prevVNode, nextVNode)
     } else {
-      patchFunctionalComponent(
-        prevVNode,
-        nextVNode,
-        container,
-        contextVNode,
-        isSVG
-      )
+      patchFunctionalComponent(prevVNode, nextVNode)
     }
     if (__DEV__) {
       popWarningContext()
@@ -476,31 +532,24 @@ export function createRenderer(options: RendererOptions) {
   }
 
   function patchStatefulComponent(prevVNode: MountedVNode, nextVNode: VNode) {
-    const { data: prevData, childFlags: prevChildFlags } = prevVNode
-    const {
-      data: nextData,
-      slots: nextSlots,
-      childFlags: nextChildFlags
-    } = nextVNode
+    const { data: prevData } = prevVNode
+    const { data: nextData, slots: nextSlots } = nextVNode
 
     const instance = (nextVNode.children =
       prevVNode.children) as ComponentInstance
-    instance.$slots = nextSlots || EMPTY_OBJ
-    instance.$parentVNode = nextVNode as MountedVNode
 
-    // Update props. This will trigger child update if necessary.
     if (nextData !== prevData) {
-      updateProps(instance, nextData)
+      const { 0: props, 1: attrs } = resolveProps(
+        nextData,
+        instance.$options.props
+      )
+      instance.$props = __DEV__ ? immutable(props) : props
+      instance.$attrs = __DEV__ ? immutable(attrs) : attrs
     }
+    instance.$slots = nextSlots || EMPTY_OBJ
+    instance.$parentVNode = nextVNode as MountedVNode
 
-    // 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.
-    const shouldForceUpdate =
-      prevChildFlags !== nextChildFlags ||
-      (nextChildFlags & ChildrenFlags.DYNAMIC_SLOTS) > 0
-    if (shouldForceUpdate) {
+    if (shouldUpdateComponent(prevVNode, nextVNode)) {
       instance.$forceUpdate()
     } else if (instance.$vnode.flags & VNodeFlags.COMPONENT) {
       instance.$vnode.contextVNode = nextVNode
@@ -508,28 +557,13 @@ export function createRenderer(options: RendererOptions) {
     nextVNode.el = instance.$vnode.el
   }
 
-  function patchFunctionalComponent(
-    prevVNode: MountedVNode,
-    nextVNode: VNode,
-    container: RenderNode,
-    contextVNode: MountedVNode | null,
-    isSVG: boolean
-  ) {
-    // functional component tree is stored on the vnode as `children`
-    const { data: prevData, slots: prevSlots } = prevVNode
-    const { data: nextData, slots: nextSlots } = nextVNode
-    const render = nextVNode.tag as FunctionalComponent
-    const prevTree = prevVNode.children as MountedVNode
-
-    let shouldUpdate = true
-    if (render.pure && prevSlots == null && nextSlots == null) {
-      shouldUpdate = shouldUpdateFunctionalComponent(prevData, nextData)
-    }
+  function patchFunctionalComponent(prevVNode: MountedVNode, nextVNode: VNode) {
+    const prevTree = prevVNode.children as VNode
+    const handle = (nextVNode.handle = prevVNode.handle as FunctionalHandle)
+    handle.current = nextVNode
 
-    if (shouldUpdate) {
-      const nextTree = (nextVNode.children = renderFunctionalRoot(nextVNode))
-      patch(prevTree, nextTree, container, nextVNode as MountedVNode, isSVG)
-      nextVNode.el = nextTree.el
+    if (shouldUpdateComponent(prevVNode, nextVNode)) {
+      handle.forceUpdate()
     } else if (prevTree.flags & VNodeFlags.COMPONENT) {
       // functional component returned another component
       prevTree.contextVNode = nextVNode
@@ -1025,7 +1059,7 @@ export function createRenderer(options: RendererOptions) {
   // unmounting ----------------------------------------------------------------
 
   function unmount(vnode: MountedVNode) {
-    const { flags, data, children, childFlags, ref } = vnode
+    const { flags, data, children, childFlags, ref, handle } = vnode
     const isElement = flags & VNodeFlags.ELEMENT
     if (isElement || flags & VNodeFlags.FRAGMENT) {
       if (isElement && data != null && data.vnodeBeforeUnmount) {
@@ -1046,6 +1080,8 @@ export function createRenderer(options: RendererOptions) {
           unmountComponentInstance(children as ComponentInstance)
         }
       } else {
+        // functional
+        stop((handle as FunctionalHandle).runner)
         unmount(children as MountedVNode)
       }
     } else if (flags & VNodeFlags.PORTAL) {
@@ -1144,12 +1180,12 @@ export function createRenderer(options: RendererOptions) {
       beforeMount.call($proxy)
     }
 
-    const errorSchedulerHandler = (err: Error) => {
+    const handleSchedulerError = (err: Error) => {
       handleError(err, instance, ErrorTypes.SCHEDULER)
     }
 
     const queueUpdate = (instance.$forceUpdate = () => {
-      queueJob(instance._updateHandle, flushHooks, errorSchedulerHandler)
+      queueJob(instance._updateHandle, flushHooks, handleSchedulerError)
     })
 
     instance._updateHandle = autorun(
@@ -1185,7 +1221,7 @@ export function createRenderer(options: RendererOptions) {
           // to inject effects in first render
           const { mounted } = instance.$options
           if (mounted) {
-            lifecycleHooks.push(() => {
+            lifecycleHooks.unshift(() => {
               mounted.call($proxy)
             })
           }
index 6d7b5525bd65c828205b65e5f27b6abaf0d74ee2..451a67adda4c59bae2bcde40f52c97871b6b28b1 100644 (file)
@@ -7,6 +7,7 @@ import { VNodeFlags, ChildrenFlags } from './flags'
 import { createComponentClassFromOptions } from './componentUtils'
 import { EMPTY_OBJ, isObject, isArray, isFunction, isString } from '@vue/shared'
 import { RawChildrenType, RawSlots } from './h'
+import { FunctionalHandle } from './createRenderer'
 
 const handlersRE = /^on|^vnode/
 
@@ -37,6 +38,10 @@ export interface VNode {
   // only on mounted component nodes
   // points to the parent stateful/functional component's placeholder node
   contextVNode: VNode | null
+  // only on mounted functional component nodes
+  // a consistent handle so that a functional component can be identified
+  // by the scheduler
+  handle: FunctionalHandle | null
 }
 
 export interface MountedVNode extends VNode {
@@ -92,7 +97,8 @@ export function createVNode(
     slots: slots === void 0 ? null : slots,
     el: null,
     parentVNode: null,
-    contextVNode: null
+    contextVNode: null,
+    handle: null
   }
   if (childFlags === ChildrenFlags.UNKNOWN_CHILDREN) {
     normalizeChildren(vnode, children)
index e6aaf634fb03e776e8aa1a5d689bc7ab2e7c8442..ef10d3aeb008af30339f2d55673057145822744d 100644 (file)
@@ -7,8 +7,11 @@ const enum Priorities {
 
 const frameBudget = 1000 / 60
 
+let start: number = 0
 let currentOps: Op[]
 
+const getNow = () => window.performance.now()
+
 const evaluate = (v: any) => {
   return typeof v === 'function' ? v() : v
 }
@@ -21,11 +24,9 @@ Object.keys(nodeOps).forEach((key: keyof NodeOps) => {
   }
   if (/create/.test(key)) {
     nodeOps[key] = (...args: any[]) => {
+      let res: any
       if (currentOps) {
-        let res: any
-        return () => {
-          return res || (res = original(...args))
-        }
+        return () => res || (res = original(...args))
       } else {
         return original(...args)
       }
@@ -45,7 +46,7 @@ type Op = [Function, ...any[]]
 
 interface Job extends Function {
   ops: Op[]
-  post: Function
+  post: Function | null
   expiration: number
 }
 
@@ -65,6 +66,7 @@ window.addEventListener(
     if (event.source !== window || event.data !== key) {
       return
     }
+    start = getNow()
     flush()
   },
   false
@@ -102,11 +104,11 @@ let hasPendingFlush = false
 
 export function queueJob(
   rawJob: Function,
-  postJob: Function,
+  postJob?: Function | null,
   onError?: (reason: any) => void
 ) {
   const job = rawJob as Job
-  job.post = postJob
+  job.post = postJob || null
   job.ops = job.ops || []
   // 1. let's see if this invalidates any work that
   // has already been done.
@@ -126,12 +128,13 @@ export function queueJob(
     }
   } else if (patchQueue.indexOf(job) === -1) {
     // a new job
-    job.expiration = performance.now() + Priorities.NORMAL
+    job.expiration = getNow() + Priorities.NORMAL
     patchQueue.push(job)
   }
 
   if (!hasPendingFlush) {
     hasPendingFlush = true
+    start = getNow()
     const p = nextTick(flush)
     if (onError) p.catch(onError)
   }
@@ -139,7 +142,6 @@ export function queueJob(
 
 function flush() {
   let job
-  let start = window.performance.now()
   while (true) {
     job = patchQueue.shift()
     if (job) {
@@ -147,7 +149,7 @@ function flush() {
     } else {
       break
     }
-    const now = performance.now()
+    const now = getNow()
     if (now - start > frameBudget && job.expiration > now) {
       break
     }