From 85cd69a988dad2620faf94ceafb4bdb61d7a75a8 Mon Sep 17 00:00:00 2001 From: Evan You Date: Mon, 24 Sep 2018 18:51:58 -0400 Subject: [PATCH] wip: refactor attrs inheritance --- packages/core/src/component.ts | 2 + packages/core/src/componentOptions.ts | 2 +- packages/core/src/componentProps.ts | 91 +++++++++++++++------- packages/core/src/componentUtils.ts | 9 ++- packages/core/src/createRenderer.ts | 27 ++++--- packages/core/src/vdom.ts | 35 +-------- packages/renderer-dom/src/modules/style.ts | 4 +- 7 files changed, 94 insertions(+), 76 deletions(-) diff --git a/packages/core/src/component.ts b/packages/core/src/component.ts index 98fe6c7d..376d28fc 100644 --- a/packages/core/src/component.ts +++ b/packages/core/src/component.ts @@ -30,6 +30,7 @@ export interface MountedComponent 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 diff --git a/packages/core/src/componentOptions.ts b/packages/core/src/componentOptions.ts index c8ce0caa..82c5f0ca 100644 --- a/packages/core/src/componentOptions.ts +++ b/packages/core/src/componentOptions.ts @@ -4,7 +4,7 @@ import { MountedComponent } from './component' export type Data = Record export interface RenderFunction

{ - (props: P, slots: Slots): any + (props: P, slots: Slots, attrs: Data): any } export interface ComponentOptions { diff --git a/packages/core/src/componentProps.ts b/packages/core/src/componentProps.ts index dd2de0ed..bd508b93 100644 --- a/packages/core/src/componentProps.ts +++ b/packages/core/src/componentProps.ts @@ -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, - Component: ComponentClass + Component: ComponentClass | FunctionalComponent ) { // TODO } diff --git a/packages/core/src/componentUtils.ts b/packages/core/src/componentUtils.ts index 4b32e903..4ad27fd7 100644 --- a/packages/core/src/componentUtils.ts +++ b/packages/core/src/componentUtils.ts @@ -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) diff --git a/packages/core/src/createRenderer.ts b/packages/core/src/createRenderer.ts index 3d9af821..1eb230e7 100644 --- a/packages/core/src/createRenderer.ts +++ b/packages/core/src/createRenderer.ts @@ -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) diff --git a/packages/core/src/vdom.ts b/packages/core/src/vdom.ts index 3b632c21..da139d29 100644 --- a/packages/core/src/vdom.ts +++ b/packages/core/src/vdom.ts @@ -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( diff --git a/packages/renderer-dom/src/modules/style.ts b/packages/renderer-dom/src/modules/style.ts index f97df183..bdf876b7 100644 --- a/packages/renderer-dom/src/modules/style.ts +++ b/packages/renderer-dom/src/modules/style.ts @@ -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] = '' } } }