chore: use proxies instead of signals for spread/rest props (#9801)

* use proxies instead of signals for spread/rest

* fix some spread attribute stuff

* remove is_signal calls

* simplify some more

* more

* remove some unnecessary unwrapping

* another

* simplify

* simplify

* simplify

* remove another MaybeSignal

* more

* remove more unwraps

* code-golf, docs

---------

Co-authored-by: Rich Harris <rich.harris@vercel.com>
Co-authored-by: Simon Holthausen <simon.holthausen@vercel.com>
pull/9799/head
Rich Harris 3 years ago committed by GitHub
parent 2ae13dfde7
commit fc9776b418
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -456,6 +456,7 @@ function read_attribute(parser) {
expression,
parent: null,
metadata: {
contains_call_expression: false,
dynamic: false
}
};

@ -906,7 +906,10 @@ const common_visitors = {
}
},
CallExpression(node, context) {
if (context.state.expression?.type === 'ExpressionTag' && !is_known_safe_call(node, context)) {
if (
context.state.expression?.type === 'ExpressionTag' ||
(context.state.expression?.type === 'SpreadAttribute' && !is_known_safe_call(node, context))
) {
context.state.expression.metadata.contains_call_expression = true;
}

@ -67,7 +67,7 @@ export function serialize_get_binding(node, state) {
if (binding.kind === 'prop' && binding.node.name === '$$props') {
// Special case for $$props which only exists in the old world
return b.call('$.unwrap', node);
return node;
}
if (
@ -88,7 +88,6 @@ export function serialize_get_binding(node, state) {
(!state.analysis.immutable || state.analysis.accessors || binding.reassigned)) ||
binding.kind === 'derived' ||
binding.kind === 'prop' ||
binding.kind === 'rest_prop' ||
binding.kind === 'legacy_reactive'
) {
return b.call('$.get', node);

@ -7,7 +7,7 @@ export const global_visitors = {
Identifier(node, { path, state }) {
if (is_reference(node, /** @type {import('estree').Node} */ (path.at(-1)))) {
if (node.name === '$$props') {
return b.call('$.get', b.id('$$sanitized_props'));
return b.id('$$sanitized_props');
}
return serialize_get_binding(node, state);
}

@ -756,7 +756,20 @@ function serialize_inline_component(node, component_name, context) {
}
events[attribute.name].push(handler);
} else if (attribute.type === 'SpreadAttribute') {
props_and_spreads.push(/** @type {import('estree').Expression} */ (context.visit(attribute)));
const expression = /** @type {import('estree').Expression} */ (context.visit(attribute));
if (attribute.metadata.dynamic) {
let value = expression;
if (attribute.metadata.contains_call_expression) {
const id = b.id(context.state.scope.generate('spread_element'));
context.state.init.push(b.var(id, b.call('$.derived', b.thunk(value))));
value = b.call('$.get', id);
}
props_and_spreads.push(b.thunk(value));
} else {
props_and_spreads.push(expression);
}
} else if (attribute.type === 'Attribute') {
if (attribute.name.startsWith('--')) {
custom_css_props.push(
@ -895,7 +908,7 @@ function serialize_inline_component(node, component_name, context) {
? b.object(/** @type {import('estree').Property[]} */ (props_and_spreads[0]) || [])
: b.call(
'$.spread_props',
b.thunk(b.array(props_and_spreads.map((p) => (Array.isArray(p) ? b.object(p) : p))))
...props_and_spreads.map((p) => (Array.isArray(p) ? b.object(p) : p))
);
/** @param {import('estree').Identifier} node_id */
let fn = (node_id) =>
@ -2764,8 +2777,8 @@ export const template_visitors = {
}
},
LetDirective(node, { state }) {
// let:x --> const x = $.derived(() => $.unwrap($$slotProps).x);
// let:x={{y, z}} --> const derived_x = $.derived(() => { const { y, z } = $.unwrap($$slotProps).x; return { y, z }));
// let:x --> const x = $.derived(() => $$slotProps.x);
// let:x={{y, z}} --> const derived_x = $.derived(() => { const { y, z } = $$slotProps.x; return { y, z }));
if (node.expression && node.expression.type !== 'Identifier') {
const name = state.scope.generate(node.name);
const bindings = state.scope.get_bindings(node);
@ -2787,7 +2800,7 @@ export const template_visitors = {
b.object_pattern(node.expression.properties)
: // @ts-expect-error types don't match, but it can't contain spread elements and the structure is otherwise fine
b.array_pattern(node.expression.elements),
b.member(b.call('$.unwrap', b.id('$$slotProps')), b.id(node.name))
b.member(b.id('$$slotProps'), b.id(node.name))
),
b.return(b.object(bindings.map((binding) => b.init(binding.node.name, binding.node))))
])
@ -2798,10 +2811,7 @@ export const template_visitors = {
const name = node.expression === null ? node.name : node.expression.name;
return b.const(
name,
b.call(
'$.derived',
b.thunk(b.member(b.call('$.unwrap', b.id('$$slotProps')), b.id(node.name)))
)
b.call('$.derived', b.thunk(b.member(b.id('$$slotProps'), b.id(node.name))))
);
}
},
@ -2854,7 +2864,9 @@ export const template_visitors = {
for (const attribute of node.attributes) {
if (attribute.type === 'SpreadAttribute') {
spreads.push(/** @type {import('estree').Expression} */ (context.visit(attribute)));
spreads.push(
b.thunk(/** @type {import('estree').Expression} */ (context.visit(attribute)))
);
} else if (attribute.type === 'Attribute') {
const [, value] = serialize_attribute_value(attribute.value, context);
if (attribute.name === 'name') {
@ -2873,7 +2885,7 @@ export const template_visitors = {
const props_expression =
spreads.length === 0
? b.object(props)
: b.call('$.spread_props', b.thunk(b.array([b.object(props), ...spreads])));
: b.call('$.spread_props', b.object(props), ...spreads);
const fallback =
node.fragment.nodes.length === 0
? b.literal(null)
@ -2883,8 +2895,8 @@ export const template_visitors = {
);
const expression = is_default
? b.member(b.call('$.unwrap', b.id('$$props')), b.id('children'))
: b.member(b.member(b.call('$.unwrap', b.id('$$props')), b.id('$$slots')), name, true, true);
? b.member(b.id('$$props'), b.id('children'))
: b.member(b.member(b.id('$$props'), b.id('$$slots')), name, true, true);
const slot = b.call('$.slot', context.state.node, expression, props_expression, fallback);
context.state.init.push(b.stmt(slot));

@ -434,6 +434,7 @@ export interface SpreadAttribute extends BaseNode {
type: 'SpreadAttribute';
expression: Expression;
metadata: {
contains_call_expression: boolean;
dynamic: boolean;
};
}

@ -29,7 +29,6 @@ import {
push_destroy_fn,
execute_effect,
UNINITIALIZED,
derived,
untrack,
effect,
flushSync,
@ -38,8 +37,7 @@ import {
managed_effect,
push,
current_component_context,
pop,
unwrap
pop
} from './runtime.js';
import {
current_hydration_fragment,
@ -53,12 +51,13 @@ import {
get_descriptor,
get_descriptors,
is_array,
is_function,
object_assign,
object_keys
} from './utils.js';
import { is_promise } from '../common.js';
import { bind_transition, trigger_transitions } from './transitions.js';
import { observe, proxy } from './proxy/proxy.js';
import { proxy } from './proxy/proxy.js';
/** @type {Set<string>} */
const all_registerd_events = new Set();
@ -224,7 +223,7 @@ export function close_frag(anchor, dom) {
}
/**
* @param {import('./types.js').MaybeSignal<(event: Event, ...args: Array<unknown>) => void>} fn
* @param {(event: Event, ...args: Array<unknown>) => void} fn
* @returns {(event: Event, ...args: unknown[]) => void}
*/
export function trusted(fn) {
@ -232,13 +231,13 @@ export function trusted(fn) {
const event = /** @type {Event} */ (args[0]);
if (event.isTrusted) {
// @ts-ignore
unwrap(fn).apply(this, args);
fn.apply(this, args);
}
};
}
/**
* @param {import('./types.js').MaybeSignal<(event: Event, ...args: Array<unknown>) => void>} fn
* @param {(event: Event, ...args: Array<unknown>) => void} fn
* @returns {(event: Event, ...args: unknown[]) => void}
*/
export function self(fn) {
@ -247,13 +246,13 @@ export function self(fn) {
// @ts-ignore
if (event.target === this) {
// @ts-ignore
unwrap(fn).apply(this, args);
fn.apply(this, args);
}
};
}
/**
* @param {import('./types.js').MaybeSignal<(event: Event, ...args: Array<unknown>) => void>} fn
* @param {(event: Event, ...args: Array<unknown>) => void} fn
* @returns {(event: Event, ...args: unknown[]) => void}
*/
export function stopPropagation(fn) {
@ -261,12 +260,12 @@ export function stopPropagation(fn) {
const event = /** @type {Event} */ (args[0]);
event.stopPropagation();
// @ts-ignore
return unwrap(fn).apply(this, args);
return fn.apply(this, args);
};
}
/**
* @param {import('./types.js').MaybeSignal<(event: Event, ...args: Array<unknown>) => void>} fn
* @param {(event: Event, ...args: Array<unknown>) => void} fn
* @returns {(event: Event, ...args: unknown[]) => void}
*/
export function once(fn) {
@ -277,12 +276,12 @@ export function once(fn) {
}
ran = true;
// @ts-ignore
return unwrap(fn).apply(this, args);
return fn.apply(this, args);
};
}
/**
* @param {import('./types.js').MaybeSignal<(event: Event, ...args: Array<unknown>) => void>} fn
* @param {(event: Event, ...args: Array<unknown>) => void} fn
* @returns {(event: Event, ...args: unknown[]) => void}
*/
export function stopImmediatePropagation(fn) {
@ -290,12 +289,12 @@ export function stopImmediatePropagation(fn) {
const event = /** @type {Event} */ (args[0]);
event.stopImmediatePropagation();
// @ts-ignore
return unwrap(fn).apply(this, args);
return fn.apply(this, args);
};
}
/**
* @param {import('./types.js').MaybeSignal<(event: Event, ...args: Array<unknown>) => void>} fn
* @param {(event: Event, ...args: Array<unknown>) => void} fn
* @returns {(event: Event, ...args: unknown[]) => void}
*/
export function preventDefault(fn) {
@ -303,7 +302,7 @@ export function preventDefault(fn) {
const event = /** @type {Event} */ (args[0]);
event.preventDefault();
// @ts-ignore
return unwrap(fn).apply(this, args);
return fn.apply(this, args);
};
}
@ -1185,27 +1184,25 @@ export function bind_property(property, event_name, type, dom, get_value, update
}
});
}
/**
* Makes an `export`ed (non-prop) variable available on the `$$props` object
* so that consumers can do `bind:x` on the component.
* @template V
* @param {import('./types.js').MaybeSignal<Record<string, unknown>>} props
* @param {Record<string, unknown>} props
* @param {string} prop
* @param {V} value
* @returns {void}
*/
export function bind_prop(props, prop, value) {
/** @param {V | null} value */
const update = (value) => {
const current_props = unwrap(props);
if (get_descriptor(current_props, prop)?.set !== undefined) {
current_props[prop] = value;
}
};
update(value);
render_effect(() => () => {
update(null);
});
const desc = get_descriptor(props, prop);
if (desc && desc.set) {
props[prop] = value;
render_effect(() => () => {
props[prop] = null;
});
}
}
/**
@ -2528,71 +2525,98 @@ export function spread_dynamic_element_attributes(node, prev, attrs, css_hash) {
}
/**
* @param {import('./types.js').Signal<Record<string, unknown>> | Record<string, unknown>} props_signal
* @param {string[]} rest
* @returns {Record<string, unknown>}
* The proxy handler for rest props (i.e. `const { x, ...rest } = $props()`).
* Is passed the full `$$props` object and excludes the named props.
* @type {ProxyHandler<{ props: Record<string | symbol, unknown>, exclude: Array<string | symbol> }>}}
*/
export function rest_props(props_signal, rest) {
return derived(() => {
var props = unwrap(props_signal);
observe(props);
/** @type {Record<string, unknown>} */
var rest_props = {};
for (const key in props) {
if (rest.includes(key)) continue;
const { enumerable } = /** @type {PropertyDescriptor} */ (get_descriptor(props, key));
const rest_props_handler = {
get(target, key) {
if (target.exclude.includes(key)) return;
return target.props[key];
},
getOwnPropertyDescriptor(target, key) {
if (target.exclude.includes(key)) return;
if (key in target.props) {
return {
enumerable: true,
configurable: true,
value: target.props[key]
};
}
},
has(target, key) {
if (target.exclude.includes(key)) return false;
return key in target.props;
},
ownKeys(target) {
/** @type {Array<string | symbol>} */
const keys = [];
define_property(rest_props, key, {
get: () => props[key],
enumerable
});
for (let key in target.props) {
if (!target.exclude.includes(key)) keys.push(key);
}
return rest_props;
});
}
return keys;
}
};
/**
* @param {Record<string, unknown>[] | (() => Record<string, unknown>[])} props
* @returns {any}
* @param {import('./types.js').Signal<Record<string, unknown>> | Record<string, unknown>} props
* @param {string[]} rest
* @returns {Record<string, unknown>}
*/
export function spread_props(props) {
if (typeof props === 'function') {
return derived(() => {
return spread_props(props());
});
}
/** @type {Record<string, unknown>} */
const merged_props = {};
let key;
for (let i = 0; i < props.length; i++) {
const obj = props[i];
for (key in obj) {
const desc = /** @type {PropertyDescriptor} */ (get_descriptor(obj, key));
const getter = desc.get;
if (getter !== undefined) {
define_property(merged_props, key, {
enumerable: true,
configurable: true,
get: getter
});
} else if (desc.get !== undefined) {
merged_props[key] = obj[key];
} else {
define_property(merged_props, key, {
enumerable: true,
configurable: true,
value: obj[key]
});
export function rest_props(props, rest) {
return new Proxy({ props, exclude: rest }, rest_props_handler);
}
/**
* The proxy handler for spread props. Handles the incoming array of props
* that looks like `() => { dynamic: props }, { static: prop }, ..` and wraps
* them so that the whole thing is passed to the component as the `$$props` argument.
* @template {Record<string | symbol, unknown>} T
* @type {ProxyHandler<{ props: Array<T | (() => T)> }>}}
*/
const spread_props_handler = {
get(target, key) {
let i = target.props.length;
while (i--) {
let p = target.props[i];
if (is_function(p)) p = p();
if (typeof p === 'object' && p !== null && key in p) return p[key];
}
},
getOwnPropertyDescriptor() {
return { enumerable: true, configurable: true };
},
has(target, key) {
for (let p of target.props) {
if (is_function(p)) p = p();
if (key in p) return true;
}
return false;
},
ownKeys(target) {
/** @type {Array<string | symbol>} */
const keys = [];
for (let p of target.props) {
if (is_function(p)) p = p();
for (const key in p) {
if (!keys.includes(key)) keys.push(key);
}
}
return keys;
}
return merged_props;
};
/**
* @param {Array<Record<string, unknown> | (() => Record<string, unknown>)>} props
* @returns {any}
*/
export function spread_props(...props) {
return new Proxy({ props }, spread_props_handler);
}
/**
@ -2778,11 +2802,10 @@ export function access_props(props) {
}
/**
* @param {import('./types.js').MaybeSignal<Record<string, any>>} props
* @param {Record<string, any>} props
* @returns {Record<string, any>}
*/
export function sanitize_slots(props) {
props = unwrap(props);
const sanitized = { ...props.$$slots };
if (props.children) sanitized.default = props.children;
return sanitized;

@ -97,32 +97,6 @@ export function set_is_ssr(ssr) {
is_ssr = ssr;
}
/**
* @param {import('./types.js').MaybeSignal<Record<string, unknown>>} props
* @returns {import('./types.js').ComponentContext}
*/
export function create_component_context(props) {
const parent = current_component_context;
return {
// accessors
a: null,
// context
c: null,
// effects
e: null,
// mounted
m: false,
// parent
p: parent,
// props
s: props,
// runes
r: false,
// update_callbacks
u: null
};
}
/**
* @param {null | import('./types.js').ComponentContext} context
* @returns {boolean}
@ -1415,18 +1389,17 @@ export function is_store(val) {
* - otherwise create a signal that updates whenever the value is updated from the parent, and when it's updated
* from within the component itself, call the setter of the parent which will propagate the value change back
* @template V
* @param {import('./types.js').MaybeSignal<Record<string, unknown>>} props_obj
* @param {Record<string, unknown>} props
* @param {string} key
* @param {number} flags
* @param {V | (() => V)} [default_value]
* @returns {import('./types.js').Signal<V> | (() => V)}
*/
export function prop_source(props_obj, key, flags, default_value) {
export function prop_source(props, key, flags, default_value) {
const call_default_value = (flags & PROPS_CALL_DEFAULT_VALUE) !== 0;
const immutable = (flags & PROPS_IS_IMMUTABLE) !== 0;
const runes = (flags & PROPS_IS_RUNES) !== 0;
const props = is_signal(props_obj) ? get(props_obj) : props_obj;
const update_bound_prop = get_descriptor(props, key)?.set;
let value = props[key];
const should_set_default_value = value === undefined && default_value !== undefined;
@ -1457,9 +1430,7 @@ export function prop_source(props_obj, key, flags, default_value) {
let mount = true;
sync_effect(() => {
const props = is_signal(props_obj) ? get(props_obj) : props_obj;
observe(props_obj);
observe(props);
// Before if to ensure signal dependency is registered
const propagating_value = props[key];
@ -1510,12 +1481,13 @@ export function prop_source(props_obj, key, flags, default_value) {
/**
* If the prop is readonly and has no fallback value, we can use this function, else we need to use `prop_source`.
* @param {import('./types.js').MaybeSignal<Record<string, unknown>>} props_obj
* @param {Record<string, unknown>} props
* @param {string} key
* @returns {any}
*/
export function prop(props_obj, key) {
return is_signal(props_obj) ? () => get(props_obj)[key] : () => props_obj[key];
export function prop(props, key) {
// TODO skip this, and rewrite as `$$props.foo`
return () => props[key];
}
/**
@ -1595,23 +1567,18 @@ function get_parent_context(component_context) {
/**
* @this {any}
* @param {import('./types.js').MaybeSignal<Record<string, unknown>>} $$props
* @param {Record<string, unknown>} $$props
* @param {Event} event
* @returns {void}
*/
export function bubble_event($$props, event) {
const events = /** @type {Record<string, Function[] | Function>} */ (unwrap($$props).$$events)?.[
var events = /** @type {Record<string, Function[] | Function>} */ ($$props.$$events)?.[
event.type
];
const callbacks = is_array(events) ? events.slice() : events == null ? [] : [events];
let fn;
for (fn of callbacks) {
var callbacks = is_array(events) ? events.slice() : events == null ? [] : [events];
for (var fn of callbacks) {
// Preserve "this" context
if (is_signal(fn)) {
get(fn).call(this, event);
} else {
fn.call(this, event);
}
fn.call(this, event);
}
}
@ -1756,14 +1723,29 @@ export function onDestroy(fn) {
}
/**
* @param {import('./types.js').MaybeSignal<Record<string, unknown>>} props
* @param {Record<string, unknown>} props
* @param {any} runes
* @returns {void}
*/
export function push(props, runes = false) {
const context_stack_item = create_component_context(props);
context_stack_item.r = runes;
current_component_context = context_stack_item;
current_component_context = {
// accessors
a: null,
// context
c: null,
// effects
e: null,
// mounted
m: false,
// parent
p: current_component_context,
// props
s: props,
// runes
r: runes,
// update_callbacks
u: null
};
}
/**
@ -1819,7 +1801,7 @@ function deep_read(value, visited = new Set()) {
}
/**
* @param {() => import('./types.js').MaybeSignal<>} get_value
* @param {() => any} get_value
* @param {Function} inspect
* @returns {void}
*/

@ -36,7 +36,7 @@ export type Store<V> = {
export type ComponentContext = {
/** props */
s: MaybeSignal<Record<string, unknown>>;
s: Record<string, unknown>;
/** accessors */
a: Record<string, any> | null;
/** effectgs */

@ -8,3 +8,11 @@ export var object_assign = Object.assign;
export var define_property = Object.defineProperty;
export var get_descriptor = Object.getOwnPropertyDescriptor;
export var get_descriptors = Object.getOwnPropertyDescriptors;
/**
* @param {any} thing
* @returns {thing is Function}
*/
export function is_function(thing) {
return typeof thing === 'function';
}

@ -5,13 +5,10 @@ import {
is_ssr,
managed_effect,
untrack,
is_signal,
get,
user_effect,
flush_local_render_effects
} from '../internal/client/runtime.js';
import { is_array } from '../internal/client/utils.js';
import { unwrap } from '../internal/index.js';
/**
* The `onMount` function schedules a callback to run as soon as the component has been mounted to the DOM.
@ -139,10 +136,9 @@ export function createEventDispatcher() {
}
return (type, detail, options) => {
const $$events = /** @type {Record<string, Function | Function[]>} */ (
unwrap(component_context.s).$$events
);
const events = $$events?.[/** @type {any} */ (type)];
const events = /** @type {Record<string, Function | Function[]>} */ (
component_context.s.$$events
)?.[/** @type {any} */ (type)];
if (events) {
const callbacks = is_array(events) ? events.slice() : [events];
@ -150,11 +146,7 @@ export function createEventDispatcher() {
// in a server (non-DOM) environment?
const event = create_custom_event(/** @type {string} */ (type), detail, options);
for (const fn of callbacks) {
if (is_signal(fn)) {
get(fn).call(component_context.a, event);
} else {
fn.call(component_context.a, event);
}
fn.call(component_context.a, event);
}
return !event.defaultPrevented;
}

Loading…
Cancel
Save