feat: make functional components time-slicable
This commit is contained in:
parent
6ba02827b1
commit
d5862d8c51
@ -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
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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 &&
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user