feat: make functional components time-slicable

This commit is contained in:
Evan You 2018-11-01 17:05:09 +09:00
parent 6ba02827b1
commit d5862d8c51
7 changed files with 136 additions and 121 deletions

View File

@ -77,7 +77,6 @@ export interface ComponentClass extends ComponentClassOptions {
export interface FunctionalComponent<P = {}> { export interface FunctionalComponent<P = {}> {
(props: P, slots: Slots, attrs: Data, parentVNode: VNode): any (props: P, slots: Slots, attrs: Data, parentVNode: VNode): any
pure?: boolean
props?: ComponentPropsOptions<P> props?: ComponentPropsOptions<P>
displayName?: string displayName?: string
} }

View File

@ -1,4 +1,4 @@
import { immutable, unwrap, lock, unlock } from '@vue/observer' import { immutable, unwrap } from '@vue/observer'
import { ComponentInstance } from './component' import { ComponentInstance } from './component'
import { import {
Data, Data,
@ -36,10 +36,12 @@ export function initializeProps(
options: NormalizedPropsOptions | undefined, options: NormalizedPropsOptions | undefined,
data: Data | null data: Data | null
) { ) {
const [props, attrs] = resolveProps(data, options) const { 0: props, 1: attrs } = resolveProps(data, options)
instance.$props = immutable(props === EMPTY_OBJ ? {} : props) instance.$props = __DEV__ ? immutable(props) : props
instance.$attrs = options instance.$attrs = options
? immutable(attrs === EMPTY_OBJ ? {} : attrs) ? __DEV__
? immutable(attrs)
: attrs
: instance.$props : instance.$props
} }
@ -115,47 +117,6 @@ export function resolveProps(
return [props, attrs] 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( export function normalizePropsOptions(
raw: ComponentPropsOptions | void raw: ComponentPropsOptions | void
): NormalizedPropsOptions | void { ): NormalizedPropsOptions | void {

View File

@ -30,7 +30,6 @@ const renderProxyHandlers = {
return target.$data[key] return target.$data[key]
} else if ((i = target.$options.props) != null && i.hasOwnProperty(key)) { } else if ((i = target.$options.props) != null && i.hasOwnProperty(key)) {
// props are only proxied if declared // props are only proxied if declared
// make sure to return from $props to register dependency
return target.$props[key] return target.$props[key]
} else if ( } else if (
(i = target._computedGetters) !== null && (i = target._computedGetters) !== null &&

View File

@ -1,4 +1,4 @@
import { VNodeFlags } from './flags' import { VNodeFlags, ChildrenFlags } from './flags'
import { EMPTY_OBJ, isArray, isObject } from '@vue/shared' import { EMPTY_OBJ, isArray, isObject } from '@vue/shared'
import { h } from './h' import { h } from './h'
import { VNode, MountedVNode, createFragment } from './vdom' import { VNode, MountedVNode, createFragment } from './vdom'
@ -193,10 +193,22 @@ function normalizeComponentRoot(
return vnode return vnode
} }
export function shouldUpdateFunctionalComponent( export function shouldUpdateComponent(
prevProps: Record<string, any> | null, prevVNode: VNode,
nextProps: Record<string, any> | null nextVNode: VNode
): boolean { ): 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) { if (prevProps === nextProps) {
return false return false
} }

View 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 { queueJob } from '@vue/scheduler'
import { VNodeFlags, ChildrenFlags } from './flags' import { VNodeFlags, ChildrenFlags } from './flags'
import { EMPTY_OBJ, reservedPropRE, isString } from '@vue/shared' import { EMPTY_OBJ, reservedPropRE, isString } from '@vue/shared'
@ -10,18 +10,18 @@ import {
Ref, Ref,
VNodeChildren VNodeChildren
} from './vdom' } from './vdom'
import { ComponentInstance, FunctionalComponent } from './component' import { ComponentInstance } from './component'
import { updateProps } from './componentProps'
import { import {
renderInstanceRoot, renderInstanceRoot,
renderFunctionalRoot, renderFunctionalRoot,
createComponentInstance, createComponentInstance,
teardownComponentInstance, teardownComponentInstance,
shouldUpdateFunctionalComponent shouldUpdateComponent
} from './componentUtils' } from './componentUtils'
import { KeepAliveSymbol } from './optional/keepAlive' import { KeepAliveSymbol } from './optional/keepAlive'
import { pushWarningContext, popWarningContext } from './warning' import { pushWarningContext, popWarningContext, warn } from './warning'
import { handleError, ErrorTypes } from './errorHandling' import { handleError, ErrorTypes } from './errorHandling'
import { resolveProps } from './componentProps'
export interface NodeOps { export interface NodeOps {
createElement: (tag: string, isSVG?: boolean) => any createElement: (tag: string, isSVG?: boolean) => any
@ -57,6 +57,13 @@ export interface RendererOptions {
teardownVNode?: (vnode: VNode) => void 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 // The whole mounting / patching / unmouting logic is placed inside this
// single function so that we can create multiple renderes with different // single function so that we can create multiple renderes with different
// platform definitions. This allows for use cases like creating a test // platform definitions. This allows for use cases like creating a test
@ -239,9 +246,64 @@ export function createRenderer(options: RendererOptions) {
isSVG: boolean, isSVG: boolean,
endNode: RenderNode | null endNode: RenderNode | null
) { ) {
const subTree = (vnode.children = renderFunctionalRoot(vnode)) if (__DEV__ && vnode.ref) {
mount(subTree, container, vnode as MountedVNode, isSVG, endNode) warn(
vnode.el = subTree.el as RenderNode `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( function mountText(
@ -462,13 +524,7 @@ export function createRenderer(options: RendererOptions) {
} else if (flags & VNodeFlags.COMPONENT_STATEFUL) { } else if (flags & VNodeFlags.COMPONENT_STATEFUL) {
patchStatefulComponent(prevVNode, nextVNode) patchStatefulComponent(prevVNode, nextVNode)
} else { } else {
patchFunctionalComponent( patchFunctionalComponent(prevVNode, nextVNode)
prevVNode,
nextVNode,
container,
contextVNode,
isSVG
)
} }
if (__DEV__) { if (__DEV__) {
popWarningContext() popWarningContext()
@ -476,31 +532,24 @@ export function createRenderer(options: RendererOptions) {
} }
function patchStatefulComponent(prevVNode: MountedVNode, nextVNode: VNode) { function patchStatefulComponent(prevVNode: MountedVNode, nextVNode: VNode) {
const { data: prevData, childFlags: prevChildFlags } = prevVNode const { data: prevData } = prevVNode
const { const { data: nextData, slots: nextSlots } = nextVNode
data: nextData,
slots: nextSlots,
childFlags: nextChildFlags
} = nextVNode
const instance = (nextVNode.children = const instance = (nextVNode.children =
prevVNode.children) as ComponentInstance 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.$slots = nextSlots || EMPTY_OBJ
instance.$parentVNode = nextVNode as MountedVNode instance.$parentVNode = nextVNode as MountedVNode
// Update props. This will trigger child update if necessary. if (shouldUpdateComponent(prevVNode, nextVNode)) {
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) {
instance.$forceUpdate() instance.$forceUpdate()
} else if (instance.$vnode.flags & VNodeFlags.COMPONENT) { } else if (instance.$vnode.flags & VNodeFlags.COMPONENT) {
instance.$vnode.contextVNode = nextVNode instance.$vnode.contextVNode = nextVNode
@ -508,28 +557,13 @@ export function createRenderer(options: RendererOptions) {
nextVNode.el = instance.$vnode.el nextVNode.el = instance.$vnode.el
} }
function patchFunctionalComponent( function patchFunctionalComponent(prevVNode: MountedVNode, nextVNode: VNode) {
prevVNode: MountedVNode, const prevTree = prevVNode.children as VNode
nextVNode: VNode, const handle = (nextVNode.handle = prevVNode.handle as FunctionalHandle)
container: RenderNode, handle.current = nextVNode
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 (shouldUpdateComponent(prevVNode, nextVNode)) {
if (render.pure && prevSlots == null && nextSlots == null) { handle.forceUpdate()
shouldUpdate = shouldUpdateFunctionalComponent(prevData, nextData)
}
if (shouldUpdate) {
const nextTree = (nextVNode.children = renderFunctionalRoot(nextVNode))
patch(prevTree, nextTree, container, nextVNode as MountedVNode, isSVG)
nextVNode.el = nextTree.el
} else if (prevTree.flags & VNodeFlags.COMPONENT) { } else if (prevTree.flags & VNodeFlags.COMPONENT) {
// functional component returned another component // functional component returned another component
prevTree.contextVNode = nextVNode prevTree.contextVNode = nextVNode
@ -1025,7 +1059,7 @@ export function createRenderer(options: RendererOptions) {
// unmounting ---------------------------------------------------------------- // unmounting ----------------------------------------------------------------
function unmount(vnode: MountedVNode) { function unmount(vnode: MountedVNode) {
const { flags, data, children, childFlags, ref } = vnode const { flags, data, children, childFlags, ref, handle } = vnode
const isElement = flags & VNodeFlags.ELEMENT const isElement = flags & VNodeFlags.ELEMENT
if (isElement || flags & VNodeFlags.FRAGMENT) { if (isElement || flags & VNodeFlags.FRAGMENT) {
if (isElement && data != null && data.vnodeBeforeUnmount) { if (isElement && data != null && data.vnodeBeforeUnmount) {
@ -1046,6 +1080,8 @@ export function createRenderer(options: RendererOptions) {
unmountComponentInstance(children as ComponentInstance) unmountComponentInstance(children as ComponentInstance)
} }
} else { } else {
// functional
stop((handle as FunctionalHandle).runner)
unmount(children as MountedVNode) unmount(children as MountedVNode)
} }
} else if (flags & VNodeFlags.PORTAL) { } else if (flags & VNodeFlags.PORTAL) {
@ -1144,12 +1180,12 @@ export function createRenderer(options: RendererOptions) {
beforeMount.call($proxy) beforeMount.call($proxy)
} }
const errorSchedulerHandler = (err: Error) => { const handleSchedulerError = (err: Error) => {
handleError(err, instance, ErrorTypes.SCHEDULER) handleError(err, instance, ErrorTypes.SCHEDULER)
} }
const queueUpdate = (instance.$forceUpdate = () => { const queueUpdate = (instance.$forceUpdate = () => {
queueJob(instance._updateHandle, flushHooks, errorSchedulerHandler) queueJob(instance._updateHandle, flushHooks, handleSchedulerError)
}) })
instance._updateHandle = autorun( instance._updateHandle = autorun(
@ -1185,7 +1221,7 @@ export function createRenderer(options: RendererOptions) {
// to inject effects in first render // to inject effects in first render
const { mounted } = instance.$options const { mounted } = instance.$options
if (mounted) { if (mounted) {
lifecycleHooks.push(() => { lifecycleHooks.unshift(() => {
mounted.call($proxy) mounted.call($proxy)
}) })
} }

View File

@ -7,6 +7,7 @@ import { VNodeFlags, ChildrenFlags } from './flags'
import { createComponentClassFromOptions } from './componentUtils' import { createComponentClassFromOptions } from './componentUtils'
import { EMPTY_OBJ, isObject, isArray, isFunction, isString } from '@vue/shared' import { EMPTY_OBJ, isObject, isArray, isFunction, isString } from '@vue/shared'
import { RawChildrenType, RawSlots } from './h' import { RawChildrenType, RawSlots } from './h'
import { FunctionalHandle } from './createRenderer'
const handlersRE = /^on|^vnode/ const handlersRE = /^on|^vnode/
@ -37,6 +38,10 @@ export interface VNode {
// only on mounted component nodes // only on mounted component nodes
// points to the parent stateful/functional component's placeholder node // points to the parent stateful/functional component's placeholder node
contextVNode: VNode | null 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 { export interface MountedVNode extends VNode {
@ -92,7 +97,8 @@ export function createVNode(
slots: slots === void 0 ? null : slots, slots: slots === void 0 ? null : slots,
el: null, el: null,
parentVNode: null, parentVNode: null,
contextVNode: null contextVNode: null,
handle: null
} }
if (childFlags === ChildrenFlags.UNKNOWN_CHILDREN) { if (childFlags === ChildrenFlags.UNKNOWN_CHILDREN) {
normalizeChildren(vnode, children) normalizeChildren(vnode, children)

View File

@ -7,8 +7,11 @@ const enum Priorities {
const frameBudget = 1000 / 60 const frameBudget = 1000 / 60
let start: number = 0
let currentOps: Op[] let currentOps: Op[]
const getNow = () => window.performance.now()
const evaluate = (v: any) => { const evaluate = (v: any) => {
return typeof v === 'function' ? v() : v return typeof v === 'function' ? v() : v
} }
@ -21,11 +24,9 @@ Object.keys(nodeOps).forEach((key: keyof NodeOps) => {
} }
if (/create/.test(key)) { if (/create/.test(key)) {
nodeOps[key] = (...args: any[]) => { nodeOps[key] = (...args: any[]) => {
let res: any
if (currentOps) { if (currentOps) {
let res: any return () => res || (res = original(...args))
return () => {
return res || (res = original(...args))
}
} else { } else {
return original(...args) return original(...args)
} }
@ -45,7 +46,7 @@ type Op = [Function, ...any[]]
interface Job extends Function { interface Job extends Function {
ops: Op[] ops: Op[]
post: Function post: Function | null
expiration: number expiration: number
} }
@ -65,6 +66,7 @@ window.addEventListener(
if (event.source !== window || event.data !== key) { if (event.source !== window || event.data !== key) {
return return
} }
start = getNow()
flush() flush()
}, },
false false
@ -102,11 +104,11 @@ let hasPendingFlush = false
export function queueJob( export function queueJob(
rawJob: Function, rawJob: Function,
postJob: Function, postJob?: Function | null,
onError?: (reason: any) => void onError?: (reason: any) => void
) { ) {
const job = rawJob as Job const job = rawJob as Job
job.post = postJob job.post = postJob || null
job.ops = job.ops || [] job.ops = job.ops || []
// 1. let's see if this invalidates any work that // 1. let's see if this invalidates any work that
// has already been done. // has already been done.
@ -126,12 +128,13 @@ export function queueJob(
} }
} else if (patchQueue.indexOf(job) === -1) { } else if (patchQueue.indexOf(job) === -1) {
// a new job // a new job
job.expiration = performance.now() + Priorities.NORMAL job.expiration = getNow() + Priorities.NORMAL
patchQueue.push(job) patchQueue.push(job)
} }
if (!hasPendingFlush) { if (!hasPendingFlush) {
hasPendingFlush = true hasPendingFlush = true
start = getNow()
const p = nextTick(flush) const p = nextTick(flush)
if (onError) p.catch(onError) if (onError) p.catch(onError)
} }
@ -139,7 +142,6 @@ export function queueJob(
function flush() { function flush() {
let job let job
let start = window.performance.now()
while (true) { while (true) {
job = patchQueue.shift() job = patchQueue.shift()
if (job) { if (job) {
@ -147,7 +149,7 @@ function flush() {
} else { } else {
break break
} }
const now = performance.now() const now = getNow()
if (now - start > frameBudget && job.expiration > now) { if (now - start > frameBudget && job.expiration > now) {
break break
} }