From d5862d8c51065babd031edaf173a92c552ce0b4c Mon Sep 17 00:00:00 2001 From: Evan You Date: Thu, 1 Nov 2018 17:05:09 +0900 Subject: [PATCH] feat: make functional components time-slicable --- packages/runtime-core/src/component.ts | 1 - packages/runtime-core/src/componentProps.ts | 51 +------ packages/runtime-core/src/componentProxy.ts | 1 - packages/runtime-core/src/componentUtils.ts | 20 ++- packages/runtime-core/src/createRenderer.ts | 154 ++++++++++++-------- packages/runtime-core/src/vdom.ts | 8 +- packages/scheduler/src/experimental.ts | 22 +-- 7 files changed, 136 insertions(+), 121 deletions(-) diff --git a/packages/runtime-core/src/component.ts b/packages/runtime-core/src/component.ts index d57c8f88..e220056b 100644 --- a/packages/runtime-core/src/component.ts +++ b/packages/runtime-core/src/component.ts @@ -77,7 +77,6 @@ export interface ComponentClass extends ComponentClassOptions { export interface FunctionalComponent

{ (props: P, slots: Slots, attrs: Data, parentVNode: VNode): any - pure?: boolean props?: ComponentPropsOptions

displayName?: string } diff --git a/packages/runtime-core/src/componentProps.ts b/packages/runtime-core/src/componentProps.ts index e8733d68..3b7dcfa9 100644 --- a/packages/runtime-core/src/componentProps.ts +++ b/packages/runtime-core/src/componentProps.ts @@ -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 { diff --git a/packages/runtime-core/src/componentProxy.ts b/packages/runtime-core/src/componentProxy.ts index e9cc3cad..633741ac 100644 --- a/packages/runtime-core/src/componentProxy.ts +++ b/packages/runtime-core/src/componentProxy.ts @@ -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 && diff --git a/packages/runtime-core/src/componentUtils.ts b/packages/runtime-core/src/componentUtils.ts index 88a42500..59c3ddb5 100644 --- a/packages/runtime-core/src/componentUtils.ts +++ b/packages/runtime-core/src/componentUtils.ts @@ -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 | null, - nextProps: Record | 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 } diff --git a/packages/runtime-core/src/createRenderer.ts b/packages/runtime-core/src/createRenderer.ts index e8317c39..991092db 100644 --- a/packages/runtime-core/src/createRenderer.ts +++ b/packages/runtime-core/src/createRenderer.ts @@ -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 + + if (nextData !== prevData) { + 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 - // Update props. This will trigger child update if necessary. - if (nextData !== prevData) { - updateProps(instance, nextData) - } - - // 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 + function patchFunctionalComponent(prevVNode: MountedVNode, nextVNode: VNode) { + const prevTree = prevVNode.children as VNode + const handle = (nextVNode.handle = prevVNode.handle as FunctionalHandle) + handle.current = nextVNode - let shouldUpdate = true - if (render.pure && prevSlots == null && nextSlots == null) { - shouldUpdate = shouldUpdateFunctionalComponent(prevData, nextData) - } - - 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) }) } diff --git a/packages/runtime-core/src/vdom.ts b/packages/runtime-core/src/vdom.ts index 6d7b5525..451a67ad 100644 --- a/packages/runtime-core/src/vdom.ts +++ b/packages/runtime-core/src/vdom.ts @@ -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) diff --git a/packages/scheduler/src/experimental.ts b/packages/scheduler/src/experimental.ts index e6aaf634..ef10d3ae 100644 --- a/packages/scheduler/src/experimental.ts +++ b/packages/scheduler/src/experimental.ts @@ -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 }