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 $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

View File

@ -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> {

View File

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

View File

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

View File

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

View File

@ -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(

View File

@ -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] = ''
} }
} }
} }