wip: refactor attrs inheritance
This commit is contained in:
parent
ef1d621162
commit
85cd69a988
@ -30,6 +30,7 @@ export interface MountedComponent<D = Data, P = Data> extends Component {
|
|||||||
$vnode: VNode
|
$vnode: VNode
|
||||||
$data: D
|
$data: D
|
||||||
$props: P
|
$props: P
|
||||||
|
$attrs: Data
|
||||||
$computed: Data
|
$computed: Data
|
||||||
$slots: Slots
|
$slots: Slots
|
||||||
$root: MountedComponent
|
$root: MountedComponent
|
||||||
@ -70,6 +71,7 @@ export class Component {
|
|||||||
public $parentVNode: VNode | null = null
|
public $parentVNode: VNode | null = null
|
||||||
public $data: Data | null = null
|
public $data: Data | null = null
|
||||||
public $props: Data | null = null
|
public $props: Data | null = null
|
||||||
|
public $attrs: Data | null = null
|
||||||
public $computed: Data | null = null
|
public $computed: Data | null = null
|
||||||
public $slots: Slots | null = null
|
public $slots: Slots | null = null
|
||||||
public $root: MountedComponent | null = null
|
public $root: MountedComponent | null = null
|
||||||
|
@ -4,7 +4,7 @@ import { MountedComponent } from './component'
|
|||||||
export type Data = Record<string, any>
|
export type Data = Record<string, any>
|
||||||
|
|
||||||
export interface RenderFunction<P = Data> {
|
export interface RenderFunction<P = Data> {
|
||||||
(props: P, slots: Slots): any
|
(props: P, slots: Slots, attrs: Data): any
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ComponentOptions<D = Data, P = Data> {
|
export interface ComponentOptions<D = Data, P = Data> {
|
||||||
|
@ -1,5 +1,10 @@
|
|||||||
import { EMPTY_OBJ } from './utils'
|
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 { immutable, unwrap, lock, unlock } from '@vue/observer'
|
||||||
import {
|
import {
|
||||||
Data,
|
Data,
|
||||||
@ -9,19 +14,30 @@ import {
|
|||||||
PropOptions
|
PropOptions
|
||||||
} from './componentOptions'
|
} 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.$props = immutable(props || {})
|
||||||
|
instance.$attrs = immutable(attrs || {})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateProps(instance: MountedComponent, nextProps: Data) {
|
export function updateProps(instance: MountedComponent, nextData: Data) {
|
||||||
// instance.$props is an observable that should not be replaced.
|
// instance.$props and instance.$attrs are observables that should not be
|
||||||
// instead, we mutate it to match latest props, which will trigger updates
|
// replaced. Instead, we mutate them to match latest props, which will trigger
|
||||||
// if any value has changed.
|
// updates if any value that's been used in child component has changed.
|
||||||
if (nextProps != null) {
|
if (nextData != null) {
|
||||||
const props = instance.$props
|
const { props: nextProps, attrs: nextAttrs } = resolveProps(
|
||||||
const rawProps = unwrap(props)
|
nextData,
|
||||||
|
instance.$options.props,
|
||||||
|
instance.constructor as ComponentClass
|
||||||
|
)
|
||||||
// unlock to temporarily allow mutatiing props
|
// unlock to temporarily allow mutatiing props
|
||||||
unlock()
|
unlock()
|
||||||
|
const props = instance.$props
|
||||||
|
const rawProps = unwrap(props)
|
||||||
for (const key in rawProps) {
|
for (const key in rawProps) {
|
||||||
if (!nextProps.hasOwnProperty(key)) {
|
if (!nextProps.hasOwnProperty(key)) {
|
||||||
delete props[key]
|
delete props[key]
|
||||||
@ -30,24 +46,46 @@ export function updateProps(instance: MountedComponent, nextProps: Data) {
|
|||||||
for (const key in nextProps) {
|
for (const key in nextProps) {
|
||||||
props[key] = nextProps[key]
|
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()
|
lock()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// This is called for every component vnode created. This also means the data
|
const EMPTY_PROPS = { props: EMPTY_OBJ }
|
||||||
// on every component vnode is guarunteed to be a fresh object.
|
|
||||||
export function normalizeComponentProps(
|
// 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,
|
raw: any,
|
||||||
rawOptions: ComponentPropsOptions,
|
rawOptions: ComponentPropsOptions | void,
|
||||||
Component: ComponentClass
|
Component: ComponentClass | FunctionalComponent
|
||||||
): Data {
|
): { props: Data; attrs?: Data } {
|
||||||
const hasDeclaredProps = rawOptions !== void 0
|
const hasDeclaredProps = rawOptions !== void 0
|
||||||
const options = (hasDeclaredProps &&
|
const options = (hasDeclaredProps &&
|
||||||
normalizePropsOptions(rawOptions)) as NormalizedPropsOptions
|
normalizePropsOptions(
|
||||||
|
rawOptions as ComponentPropsOptions
|
||||||
|
)) as NormalizedPropsOptions
|
||||||
if (!raw && !hasDeclaredProps) {
|
if (!raw && !hasDeclaredProps) {
|
||||||
return EMPTY_OBJ
|
return EMPTY_PROPS
|
||||||
}
|
}
|
||||||
const res: Data = {}
|
const props: any = {}
|
||||||
|
let attrs: any = void 0
|
||||||
if (raw) {
|
if (raw) {
|
||||||
for (const key in raw) {
|
for (const key in raw) {
|
||||||
// key, ref, slots are reserved
|
// key, ref, slots are reserved
|
||||||
@ -66,35 +104,36 @@ export function normalizeComponentProps(
|
|||||||
(hasDeclaredProps && !options.hasOwnProperty(key))
|
(hasDeclaredProps && !options.hasOwnProperty(key))
|
||||||
) {
|
) {
|
||||||
const newKey = isNativeOn ? 'on' + key.slice(8) : key
|
const newKey = isNativeOn ? 'on' + key.slice(8) : key
|
||||||
;(res.attrs || (res.attrs = {}))[newKey] = raw[key]
|
;(attrs || (attrs = {}))[newKey] = raw[key]
|
||||||
} else {
|
} else {
|
||||||
if (__DEV__ && hasDeclaredProps && options.hasOwnProperty(key)) {
|
if (__DEV__ && hasDeclaredProps && options.hasOwnProperty(key)) {
|
||||||
validateProp(key, raw[key], options[key], Component)
|
validateProp(key, raw[key], options[key], Component)
|
||||||
}
|
}
|
||||||
res[key] = raw[key]
|
props[key] = raw[key]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// set default values
|
// set default values
|
||||||
if (hasDeclaredProps) {
|
if (hasDeclaredProps) {
|
||||||
for (const key in options) {
|
for (const key in options) {
|
||||||
if (res[key] === void 0) {
|
if (props[key] === void 0) {
|
||||||
const opt = options[key]
|
const opt = options[key]
|
||||||
if (opt != null && opt.hasOwnProperty('default')) {
|
if (opt != null && opt.hasOwnProperty('default')) {
|
||||||
const defaultValue = opt.default
|
const defaultValue = opt.default
|
||||||
res[key] =
|
props[key] =
|
||||||
typeof defaultValue === 'function' ? defaultValue() : defaultValue
|
typeof defaultValue === 'function' ? defaultValue() : defaultValue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return res
|
return { props, attrs }
|
||||||
}
|
}
|
||||||
|
|
||||||
const normalizeCache: WeakMap<
|
const normalizeCache = new WeakMap<
|
||||||
ComponentPropsOptions,
|
ComponentPropsOptions,
|
||||||
NormalizedPropsOptions
|
NormalizedPropsOptions
|
||||||
> = new WeakMap()
|
>()
|
||||||
|
|
||||||
function normalizePropsOptions(
|
function normalizePropsOptions(
|
||||||
raw: ComponentPropsOptions
|
raw: ComponentPropsOptions
|
||||||
): NormalizedPropsOptions {
|
): NormalizedPropsOptions {
|
||||||
@ -116,7 +155,7 @@ function validateProp(
|
|||||||
key: string,
|
key: string,
|
||||||
value: any,
|
value: any,
|
||||||
validator: PropValidator<any>,
|
validator: PropValidator<any>,
|
||||||
Component: ComponentClass
|
Component: ComponentClass | FunctionalComponent
|
||||||
) {
|
) {
|
||||||
// TODO
|
// TODO
|
||||||
}
|
}
|
||||||
|
@ -74,6 +74,7 @@ export function renderInstanceRoot(instance: MountedComponent) {
|
|||||||
return normalizeComponentRoot(
|
return normalizeComponentRoot(
|
||||||
vnode,
|
vnode,
|
||||||
instance.$parentVNode,
|
instance.$parentVNode,
|
||||||
|
instance.$attrs,
|
||||||
instance.$options.inheritAttrs
|
instance.$options.inheritAttrs
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -93,6 +94,7 @@ export function teardownComponentInstance(instance: MountedComponent) {
|
|||||||
export function normalizeComponentRoot(
|
export function normalizeComponentRoot(
|
||||||
vnode: any,
|
vnode: any,
|
||||||
componentVNode: VNode | null,
|
componentVNode: VNode | null,
|
||||||
|
attrs: Data | void,
|
||||||
inheritAttrs: boolean | void
|
inheritAttrs: boolean | void
|
||||||
): VNode {
|
): VNode {
|
||||||
if (vnode == null) {
|
if (vnode == null) {
|
||||||
@ -108,9 +110,10 @@ export function normalizeComponentRoot(
|
|||||||
componentVNode &&
|
componentVNode &&
|
||||||
(flags & VNodeFlags.COMPONENT || flags & VNodeFlags.ELEMENT)
|
(flags & VNodeFlags.COMPONENT || flags & VNodeFlags.ELEMENT)
|
||||||
) {
|
) {
|
||||||
const parentData = componentVNode.data
|
if (inheritAttrs !== false && attrs !== void 0) {
|
||||||
if (inheritAttrs !== false && parentData && parentData.attrs) {
|
// TODO should merge
|
||||||
vnode = cloneVNode(vnode, parentData.attrs)
|
console.log(attrs)
|
||||||
|
vnode = cloneVNode(vnode, attrs)
|
||||||
}
|
}
|
||||||
if (vnode.el) {
|
if (vnode.el) {
|
||||||
vnode = cloneVNode(vnode)
|
vnode = cloneVNode(vnode)
|
||||||
|
@ -18,7 +18,7 @@ import {
|
|||||||
FunctionalComponent,
|
FunctionalComponent,
|
||||||
ComponentClass
|
ComponentClass
|
||||||
} from './component'
|
} from './component'
|
||||||
import { updateProps } from './componentProps'
|
import { updateProps, resolveProps } from './componentProps'
|
||||||
import {
|
import {
|
||||||
renderInstanceRoot,
|
renderInstanceRoot,
|
||||||
createComponentInstance,
|
createComponentInstance,
|
||||||
@ -272,12 +272,15 @@ export function createRenderer(options: RendererOptions) {
|
|||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
// functional component
|
// functional component
|
||||||
|
const render = tag as FunctionalComponent
|
||||||
|
const { props, attrs } = resolveProps(data, render.props, render)
|
||||||
const subTree = (vnode.children = normalizeComponentRoot(
|
const subTree = (vnode.children = normalizeComponentRoot(
|
||||||
(tag as FunctionalComponent)(data || EMPTY_OBJ, slots || EMPTY_OBJ),
|
render(props, slots || EMPTY_OBJ, attrs || EMPTY_OBJ),
|
||||||
vnode,
|
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) {
|
if (container != null) {
|
||||||
insertOrAppend(container, el, endNode)
|
insertOrAppend(container, el, endNode)
|
||||||
@ -508,7 +511,7 @@ export function createRenderer(options: RendererOptions) {
|
|||||||
function patchStatefulComponent(prevVNode: VNode, nextVNode: VNode) {
|
function patchStatefulComponent(prevVNode: VNode, nextVNode: VNode) {
|
||||||
const { childFlags: prevChildFlags } = prevVNode
|
const { childFlags: prevChildFlags } = prevVNode
|
||||||
const {
|
const {
|
||||||
data: nextProps,
|
data: nextData,
|
||||||
slots: nextSlots,
|
slots: nextSlots,
|
||||||
childFlags: nextChildFlags
|
childFlags: nextChildFlags
|
||||||
} = nextVNode
|
} = nextVNode
|
||||||
@ -519,8 +522,8 @@ export function createRenderer(options: RendererOptions) {
|
|||||||
instance.$parentVNode = nextVNode
|
instance.$parentVNode = nextVNode
|
||||||
|
|
||||||
// Update props. This will trigger child update if necessary.
|
// Update props. This will trigger child update if necessary.
|
||||||
if (nextProps !== null) {
|
if (nextData !== null) {
|
||||||
updateProps(instance, nextProps)
|
updateProps(instance, nextData)
|
||||||
}
|
}
|
||||||
|
|
||||||
// If has different slots content, or has non-compiled slots,
|
// If has different slots content, or has non-compiled slots,
|
||||||
@ -546,20 +549,22 @@ export function createRenderer(options: RendererOptions) {
|
|||||||
isSVG: boolean
|
isSVG: boolean
|
||||||
) {
|
) {
|
||||||
// functional component tree is stored on the vnode as `children`
|
// functional component tree is stored on the vnode as `children`
|
||||||
const { data: prevProps, slots: prevSlots } = prevVNode
|
const { data: prevData, slots: prevSlots } = prevVNode
|
||||||
const { data: nextProps, slots: nextSlots } = nextVNode
|
const { data: nextData, slots: nextSlots } = nextVNode
|
||||||
const render = nextVNode.tag as FunctionalComponent
|
const render = nextVNode.tag as FunctionalComponent
|
||||||
const prevTree = prevVNode.children as VNode
|
const prevTree = prevVNode.children as VNode
|
||||||
|
|
||||||
let shouldUpdate = true
|
let shouldUpdate = true
|
||||||
if (render.pure && prevSlots == null && nextSlots == null) {
|
if (render.pure && prevSlots == null && nextSlots == null) {
|
||||||
shouldUpdate = shouldUpdateFunctionalComponent(prevProps, nextProps)
|
shouldUpdate = shouldUpdateFunctionalComponent(prevData, nextData)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shouldUpdate) {
|
if (shouldUpdate) {
|
||||||
|
const { props, attrs } = resolveProps(nextData, render.props, render)
|
||||||
const nextTree = (nextVNode.children = normalizeComponentRoot(
|
const nextTree = (nextVNode.children = normalizeComponentRoot(
|
||||||
render(nextProps || EMPTY_OBJ, nextSlots || EMPTY_OBJ),
|
render(props, nextSlots || EMPTY_OBJ, attrs || EMPTY_OBJ),
|
||||||
nextVNode,
|
nextVNode,
|
||||||
|
attrs,
|
||||||
render.inheritAttrs
|
render.inheritAttrs
|
||||||
))
|
))
|
||||||
patch(prevTree, nextTree, container, parentComponent, isSVG)
|
patch(prevTree, nextTree, container, parentComponent, isSVG)
|
||||||
|
@ -4,9 +4,7 @@ import {
|
|||||||
FunctionalComponent
|
FunctionalComponent
|
||||||
} from './component'
|
} from './component'
|
||||||
import { VNodeFlags, ChildrenFlags } from './flags'
|
import { VNodeFlags, ChildrenFlags } from './flags'
|
||||||
import { normalizeComponentProps } from './componentProps'
|
|
||||||
import { createComponentClassFromOptions } from './componentUtils'
|
import { createComponentClassFromOptions } from './componentUtils'
|
||||||
import { ComponentPropsOptions } from './componentOptions'
|
|
||||||
|
|
||||||
// Vue core is platform agnostic, so we are not using Element for "DOM" nodes.
|
// Vue core is platform agnostic, so we are not using Element for "DOM" nodes.
|
||||||
export interface RenderNode {
|
export interface RenderNode {
|
||||||
@ -119,7 +117,6 @@ export function createComponentVNode(
|
|||||||
) {
|
) {
|
||||||
// resolve type
|
// resolve type
|
||||||
let flags: VNodeFlags
|
let flags: VNodeFlags
|
||||||
let propsOptions: ComponentPropsOptions
|
|
||||||
|
|
||||||
// flags
|
// flags
|
||||||
const compType = typeof comp
|
const compType = typeof comp
|
||||||
@ -134,14 +131,12 @@ export function createComponentVNode(
|
|||||||
comp._normalized = true
|
comp._normalized = true
|
||||||
}
|
}
|
||||||
comp = render
|
comp = render
|
||||||
propsOptions = comp.props
|
|
||||||
} else {
|
} else {
|
||||||
// object literal stateful
|
// object literal stateful
|
||||||
flags = VNodeFlags.COMPONENT_STATEFUL
|
flags = VNodeFlags.COMPONENT_STATEFUL
|
||||||
comp =
|
comp =
|
||||||
comp._normalized ||
|
comp._normalized ||
|
||||||
(comp._normalized = createComponentClassFromOptions(comp))
|
(comp._normalized = createComponentClassFromOptions(comp))
|
||||||
propsOptions = comp.options && comp.options.props
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// assumes comp is function here now
|
// assumes comp is function here now
|
||||||
@ -150,10 +145,8 @@ export function createComponentVNode(
|
|||||||
}
|
}
|
||||||
if (comp.prototype && comp.prototype.render) {
|
if (comp.prototype && comp.prototype.render) {
|
||||||
flags = VNodeFlags.COMPONENT_STATEFUL
|
flags = VNodeFlags.COMPONENT_STATEFUL
|
||||||
propsOptions = comp.options && comp.options.props
|
|
||||||
} else {
|
} else {
|
||||||
flags = VNodeFlags.COMPONENT_FUNCTIONAL
|
flags = VNodeFlags.COMPONENT_FUNCTIONAL
|
||||||
propsOptions = comp.props
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -161,9 +154,6 @@ export function createComponentVNode(
|
|||||||
// TODO warn functional component cannot have ref
|
// TODO warn functional component cannot have ref
|
||||||
}
|
}
|
||||||
|
|
||||||
// props
|
|
||||||
const props = normalizeComponentProps(data, propsOptions, comp)
|
|
||||||
|
|
||||||
// slots
|
// slots
|
||||||
let slots: any
|
let slots: any
|
||||||
if (childFlags == null) {
|
if (childFlags == null) {
|
||||||
@ -188,7 +178,7 @@ export function createComponentVNode(
|
|||||||
return createVNode(
|
return createVNode(
|
||||||
flags,
|
flags,
|
||||||
comp,
|
comp,
|
||||||
props,
|
data,
|
||||||
null, // to be set during mount
|
null, // to be set during mount
|
||||||
childFlags,
|
childFlags,
|
||||||
key,
|
key,
|
||||||
@ -258,28 +248,7 @@ export function cloneVNode(vnode: VNode, extraData?: VNodeData): VNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (const key in extraData) {
|
for (const key in extraData) {
|
||||||
const existing = clonedData[key]
|
clonedData[key] = extraData[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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return createVNode(
|
return createVNode(
|
||||||
|
@ -22,13 +22,13 @@ export function patchStyle(el: any, prev: any, next: any, data: any) {
|
|||||||
if (typeof value === 'number' && !nonNumericRE.test(key)) {
|
if (typeof value === 'number' && !nonNumericRE.test(key)) {
|
||||||
value = value + 'px'
|
value = value + 'px'
|
||||||
}
|
}
|
||||||
style.setProperty(key, value)
|
style[key] = value
|
||||||
}
|
}
|
||||||
if (prev && typeof prev !== 'string') {
|
if (prev && typeof prev !== 'string') {
|
||||||
prev = normalizeStyle(prev)
|
prev = normalizeStyle(prev)
|
||||||
for (const key in prev) {
|
for (const key in prev) {
|
||||||
if (!normalizedNext[key]) {
|
if (!normalizedNext[key]) {
|
||||||
style.setProperty(key, '')
|
style[key] = ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user