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 = {}> {
(props: P, slots: Slots, attrs: Data, parentVNode: VNode): any
pure?: boolean
props?: ComponentPropsOptions<P>
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 {
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 {

View 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 &&

View 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
}

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 { 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)
})
}

View 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)

View 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
}