wip: refactor attrs inheritance

This commit is contained in:
Evan You 2018-09-24 18:51:58 -04:00
parent ef1d621162
commit 85cd69a988
7 changed files with 94 additions and 76 deletions

View File

@ -30,6 +30,7 @@ export interface MountedComponent<D = Data, P = Data> extends Component {
$vnode: VNode
$data: D
$props: P
$attrs: Data
$computed: Data
$slots: Slots
$root: MountedComponent
@ -70,6 +71,7 @@ export class Component {
public $parentVNode: VNode | null = null
public $data: Data | null = null
public $props: Data | null = null
public $attrs: Data | null = null
public $computed: Data | null = null
public $slots: Slots | null = null
public $root: MountedComponent | null = null

View File

@ -4,7 +4,7 @@ import { MountedComponent } from './component'
export type Data = Record<string, any>
export interface RenderFunction<P = Data> {
(props: P, slots: Slots): any
(props: P, slots: Slots, attrs: Data): any
}
export interface ComponentOptions<D = Data, P = Data> {

View File

@ -1,5 +1,10 @@
import { EMPTY_OBJ } from './utils'
import { Component, ComponentClass, MountedComponent } from './component'
import {
Component,
ComponentClass,
MountedComponent,
FunctionalComponent
} from './component'
import { immutable, unwrap, lock, unlock } from '@vue/observer'
import {
Data,
@ -9,19 +14,30 @@ import {
PropOptions
} from './componentOptions'
export function initializeProps(instance: Component, props: Data | null) {
export function initializeProps(instance: Component, data: Data | null) {
const { props, attrs } = resolveProps(
data,
instance.$options.props,
instance.constructor as ComponentClass
)
instance.$props = immutable(props || {})
instance.$attrs = immutable(attrs || {})
}
export function updateProps(instance: MountedComponent, nextProps: Data) {
// instance.$props is an observable that should not be replaced.
// instead, we mutate it to match latest props, which will trigger updates
// if any value has changed.
if (nextProps != null) {
const props = instance.$props
const rawProps = unwrap(props)
export function updateProps(instance: MountedComponent, nextData: Data) {
// 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.
if (nextData != null) {
const { props: nextProps, attrs: nextAttrs } = resolveProps(
nextData,
instance.$options.props,
instance.constructor as ComponentClass
)
// unlock to temporarily allow mutatiing props
unlock()
const props = instance.$props
const rawProps = unwrap(props)
for (const key in rawProps) {
if (!nextProps.hasOwnProperty(key)) {
delete props[key]
@ -30,24 +46,46 @@ export function updateProps(instance: MountedComponent, nextProps: Data) {
for (const key in nextProps) {
props[key] = nextProps[key]
}
if (nextAttrs) {
const attrs = instance.$attrs
const rawAttrs = unwrap(attrs)
for (const key in rawAttrs) {
if (!nextAttrs.hasOwnProperty(key)) {
delete attrs[key]
}
}
for (const key in nextAttrs) {
attrs[key] = nextAttrs[key]
}
}
lock()
}
}
// This is called for every component vnode created. This also means the data
// on every component vnode is guarunteed to be a fresh object.
export function normalizeComponentProps(
const EMPTY_PROPS = { props: EMPTY_OBJ }
// resolve raw VNode data.
// - filter out reserved keys (key, ref, slots)
// - extract class, style and nativeOn* into $attrs (to be merged onto child
// component root)
// - for the rest:
// - if has declared props: put declared ones in `props`, the rest in `attrs`
// - else: everything goes in `props`.
export function resolveProps(
raw: any,
rawOptions: ComponentPropsOptions,
Component: ComponentClass
): Data {
rawOptions: ComponentPropsOptions | void,
Component: ComponentClass | FunctionalComponent
): { props: Data; attrs?: Data } {
const hasDeclaredProps = rawOptions !== void 0
const options = (hasDeclaredProps &&
normalizePropsOptions(rawOptions)) as NormalizedPropsOptions
normalizePropsOptions(
rawOptions as ComponentPropsOptions
)) as NormalizedPropsOptions
if (!raw && !hasDeclaredProps) {
return EMPTY_OBJ
return EMPTY_PROPS
}
const res: Data = {}
const props: any = {}
let attrs: any = void 0
if (raw) {
for (const key in raw) {
// key, ref, slots are reserved
@ -66,35 +104,36 @@ export function normalizeComponentProps(
(hasDeclaredProps && !options.hasOwnProperty(key))
) {
const newKey = isNativeOn ? 'on' + key.slice(8) : key
;(res.attrs || (res.attrs = {}))[newKey] = raw[key]
;(attrs || (attrs = {}))[newKey] = raw[key]
} else {
if (__DEV__ && hasDeclaredProps && options.hasOwnProperty(key)) {
validateProp(key, raw[key], options[key], Component)
}
res[key] = raw[key]
props[key] = raw[key]
}
}
}
// set default values
if (hasDeclaredProps) {
for (const key in options) {
if (res[key] === void 0) {
if (props[key] === void 0) {
const opt = options[key]
if (opt != null && opt.hasOwnProperty('default')) {
const defaultValue = opt.default
res[key] =
props[key] =
typeof defaultValue === 'function' ? defaultValue() : defaultValue
}
}
}
}
return res
return { props, attrs }
}
const normalizeCache: WeakMap<
const normalizeCache = new WeakMap<
ComponentPropsOptions,
NormalizedPropsOptions
> = new WeakMap()
>()
function normalizePropsOptions(
raw: ComponentPropsOptions
): NormalizedPropsOptions {
@ -116,7 +155,7 @@ function validateProp(
key: string,
value: any,
validator: PropValidator<any>,
Component: ComponentClass
Component: ComponentClass | FunctionalComponent
) {
// TODO
}

View File

@ -74,6 +74,7 @@ export function renderInstanceRoot(instance: MountedComponent) {
return normalizeComponentRoot(
vnode,
instance.$parentVNode,
instance.$attrs,
instance.$options.inheritAttrs
)
}
@ -93,6 +94,7 @@ export function teardownComponentInstance(instance: MountedComponent) {
export function normalizeComponentRoot(
vnode: any,
componentVNode: VNode | null,
attrs: Data | void,
inheritAttrs: boolean | void
): VNode {
if (vnode == null) {
@ -108,9 +110,10 @@ export function normalizeComponentRoot(
componentVNode &&
(flags & VNodeFlags.COMPONENT || flags & VNodeFlags.ELEMENT)
) {
const parentData = componentVNode.data
if (inheritAttrs !== false && parentData && parentData.attrs) {
vnode = cloneVNode(vnode, parentData.attrs)
if (inheritAttrs !== false && attrs !== void 0) {
// TODO should merge
console.log(attrs)
vnode = cloneVNode(vnode, attrs)
}
if (vnode.el) {
vnode = cloneVNode(vnode)

View File

@ -18,7 +18,7 @@ import {
FunctionalComponent,
ComponentClass
} from './component'
import { updateProps } from './componentProps'
import { updateProps, resolveProps } from './componentProps'
import {
renderInstanceRoot,
createComponentInstance,
@ -272,12 +272,15 @@ export function createRenderer(options: RendererOptions) {
)
} else {
// functional component
const render = tag as FunctionalComponent
const { props, attrs } = resolveProps(data, render.props, render)
const subTree = (vnode.children = normalizeComponentRoot(
(tag as FunctionalComponent)(data || EMPTY_OBJ, slots || EMPTY_OBJ),
render(props, slots || EMPTY_OBJ, attrs || EMPTY_OBJ),
vnode,
(tag as FunctionalComponent).inheritAttrs
attrs,
render.inheritAttrs
))
el = vnode.el = mount(subTree, null, parentComponent, isSVG, null)
el = vnode.el = mount(subTree, null, parentComponent, isSVG, endNode)
}
if (container != null) {
insertOrAppend(container, el, endNode)
@ -508,7 +511,7 @@ export function createRenderer(options: RendererOptions) {
function patchStatefulComponent(prevVNode: VNode, nextVNode: VNode) {
const { childFlags: prevChildFlags } = prevVNode
const {
data: nextProps,
data: nextData,
slots: nextSlots,
childFlags: nextChildFlags
} = nextVNode
@ -519,8 +522,8 @@ export function createRenderer(options: RendererOptions) {
instance.$parentVNode = nextVNode
// Update props. This will trigger child update if necessary.
if (nextProps !== null) {
updateProps(instance, nextProps)
if (nextData !== null) {
updateProps(instance, nextData)
}
// If has different slots content, or has non-compiled slots,
@ -546,20 +549,22 @@ export function createRenderer(options: RendererOptions) {
isSVG: boolean
) {
// functional component tree is stored on the vnode as `children`
const { data: prevProps, slots: prevSlots } = prevVNode
const { data: nextProps, slots: nextSlots } = nextVNode
const { data: prevData, slots: prevSlots } = prevVNode
const { data: nextData, slots: nextSlots } = nextVNode
const render = nextVNode.tag as FunctionalComponent
const prevTree = prevVNode.children as VNode
let shouldUpdate = true
if (render.pure && prevSlots == null && nextSlots == null) {
shouldUpdate = shouldUpdateFunctionalComponent(prevProps, nextProps)
shouldUpdate = shouldUpdateFunctionalComponent(prevData, nextData)
}
if (shouldUpdate) {
const { props, attrs } = resolveProps(nextData, render.props, render)
const nextTree = (nextVNode.children = normalizeComponentRoot(
render(nextProps || EMPTY_OBJ, nextSlots || EMPTY_OBJ),
render(props, nextSlots || EMPTY_OBJ, attrs || EMPTY_OBJ),
nextVNode,
attrs,
render.inheritAttrs
))
patch(prevTree, nextTree, container, parentComponent, isSVG)

View File

@ -4,9 +4,7 @@ import {
FunctionalComponent
} from './component'
import { VNodeFlags, ChildrenFlags } from './flags'
import { normalizeComponentProps } from './componentProps'
import { createComponentClassFromOptions } from './componentUtils'
import { ComponentPropsOptions } from './componentOptions'
// Vue core is platform agnostic, so we are not using Element for "DOM" nodes.
export interface RenderNode {
@ -119,7 +117,6 @@ export function createComponentVNode(
) {
// resolve type
let flags: VNodeFlags
let propsOptions: ComponentPropsOptions
// flags
const compType = typeof comp
@ -134,14 +131,12 @@ export function createComponentVNode(
comp._normalized = true
}
comp = render
propsOptions = comp.props
} else {
// object literal stateful
flags = VNodeFlags.COMPONENT_STATEFUL
comp =
comp._normalized ||
(comp._normalized = createComponentClassFromOptions(comp))
propsOptions = comp.options && comp.options.props
}
} else {
// assumes comp is function here now
@ -150,10 +145,8 @@ export function createComponentVNode(
}
if (comp.prototype && comp.prototype.render) {
flags = VNodeFlags.COMPONENT_STATEFUL
propsOptions = comp.options && comp.options.props
} else {
flags = VNodeFlags.COMPONENT_FUNCTIONAL
propsOptions = comp.props
}
}
@ -161,9 +154,6 @@ export function createComponentVNode(
// TODO warn functional component cannot have ref
}
// props
const props = normalizeComponentProps(data, propsOptions, comp)
// slots
let slots: any
if (childFlags == null) {
@ -188,7 +178,7 @@ export function createComponentVNode(
return createVNode(
flags,
comp,
props,
data,
null, // to be set during mount
childFlags,
key,
@ -258,28 +248,7 @@ export function cloneVNode(vnode: VNode, extraData?: VNodeData): VNode {
}
}
for (const key in extraData) {
const existing = clonedData[key]
const extra = extraData[key]
if (extra === void 0) {
continue
}
// special merge behavior for attrs / class / style / on.
let isOn
if (key === 'attrs') {
clonedData.attrs = existing
? Object.assign({}, existing, extra)
: extra
} else if (
key === 'class' ||
key === 'style' ||
(isOn = key.startsWith('on'))
) {
// all three props can handle array format, so we simply merge them
// by concating.
clonedData[key] = existing ? [].concat(existing, extra) : extra
} else {
clonedData[key] = extra
}
clonedData[key] = extraData[key]
}
}
return createVNode(

View File

@ -22,13 +22,13 @@ export function patchStyle(el: any, prev: any, next: any, data: any) {
if (typeof value === 'number' && !nonNumericRE.test(key)) {
value = value + 'px'
}
style.setProperty(key, value)
style[key] = value
}
if (prev && typeof prev !== 'string') {
prev = normalizeStyle(prev)
for (const key in prev) {
if (!normalizedNext[key]) {
style.setProperty(key, '')
style[key] = ''
}
}
}