Fixing reactivity glitches

pull/6332/head
subtle-byte 5 years ago
parent 0a3be1d041
commit a69c374381

@ -13,7 +13,7 @@ export type Updater<T> = (value: T) => T;
type Invalidator<T> = (value?: T) => void; type Invalidator<T> = (value?: T) => void;
/** Start and stop notification callbacks. */ /** Start and stop notification callbacks. */
export type StartStopNotifier<T> = (set: Subscriber<T>) => Unsubscriber | void; export type StartStopNotifier<T> = (set: Subscriber<T>, invalidate: Invalidator<T>) => Unsubscriber | void;
/** Readable interface for subscribing. */ /** Readable interface for subscribing. */
export interface Readable<T> { export interface Readable<T> {
@ -22,7 +22,7 @@ export interface Readable<T> {
* @param run subscription callback * @param run subscription callback
* @param invalidate cleanup callback * @param invalidate cleanup callback
*/ */
subscribe(this: void, run: Subscriber<T>, invalidate?: Invalidator<T>): Unsubscriber; subscribe(this: void, run: Subscriber<T>, invalidate?: Invalidator<T>, revalidate?: Revalidator): Unsubscriber;
} }
/** Writable interface for both updating and subscribing. */ /** Writable interface for both updating and subscribing. */
@ -40,8 +40,9 @@ export interface Writable<T> extends Readable<T> {
update(this: void, updater: Updater<T>): void; update(this: void, updater: Updater<T>): void;
} }
/** Pair of subscriber and invalidator. */ type Revalidator = () => void;
type SubscribeInvalidateTuple<T> = [Subscriber<T>, Invalidator<T>]; /** Tuple of subscriber, invalidator, and revalidator. */
type Subscription<T> = [Subscriber<T>, Invalidator<T>, Revalidator];
const subscriber_queue = []; const subscriber_queue = [];
@ -63,17 +64,27 @@ export function readable<T>(value: T, start: StartStopNotifier<T>): Readable<T>
*/ */
export function writable<T>(value: T, start: StartStopNotifier<T> = noop): Writable<T> { export function writable<T>(value: T, start: StartStopNotifier<T> = noop): Writable<T> {
let stop: Unsubscriber; let stop: Unsubscriber;
const subscribers: Array<SubscribeInvalidateTuple<T>> = []; const subscriptions: Array<Subscription<T>> = [];
function invalidate() {
for (let i = 0; i < subscriptions.length; i += 1) {
subscriptions[i][1]();
}
}
function revalidate() {
for (let i = 0; i < subscriptions.length; i += 1) {
subscriptions[i][2]();
}
}
function set(new_value: T): void { function set(new_value: T): void {
if (safe_not_equal(value, new_value)) { if (safe_not_equal(value, new_value)) {
value = new_value; value = new_value;
if (stop) { // store is ready if (stop) { // store is ready
const run_queue = !subscriber_queue.length; const run_queue = !subscriber_queue.length;
for (let i = 0; i < subscribers.length; i += 1) { invalidate();
const s = subscribers[i]; for (let i = 0; i < subscriptions.length; i += 1) {
s[1](); subscriber_queue.push(subscriptions[i], value);
subscriber_queue.push(s, value);
} }
if (run_queue) { if (run_queue) {
for (let i = 0; i < subscriber_queue.length; i += 2) { for (let i = 0; i < subscriber_queue.length; i += 2) {
@ -82,6 +93,8 @@ export function writable<T>(value: T, start: StartStopNotifier<T> = noop): Writa
subscriber_queue.length = 0; subscriber_queue.length = 0;
} }
} }
} else {
revalidate();
} }
} }
@ -89,20 +102,20 @@ export function writable<T>(value: T, start: StartStopNotifier<T> = noop): Writa
set(fn(value)); set(fn(value));
} }
function subscribe(run: Subscriber<T>, invalidate: Invalidator<T> = noop): Unsubscriber { function subscribe(run: Subscriber<T>, invalidate: Invalidator<T> = noop, revalidate: Revalidator = noop): Unsubscriber {
const subscriber: SubscribeInvalidateTuple<T> = [run, invalidate]; const subscription: Subscription<T> = [run, invalidate, revalidate];
subscribers.push(subscriber); subscriptions.push(subscription);
if (subscribers.length === 1) { if (subscriptions.length === 1) {
stop = start(set) || noop; stop = start(set, invalidate) || noop;
} }
run(value); run(value);
return () => { return () => {
const index = subscribers.indexOf(subscriber); const index = subscriptions.indexOf(subscription);
if (index !== -1) { if (index !== -1) {
subscribers.splice(index, 1); subscriptions.splice(index, 1);
} }
if (subscribers.length === 0) { if (subscriptions.length === 0) {
stop(); stop();
stop = null; stop = null;
} }
@ -167,7 +180,7 @@ export function derived<T>(stores: Stores, fn: Function, initial_value?: T): Rea
const auto = fn.length < 2; const auto = fn.length < 2;
return readable(initial_value, (set) => { return readable(initial_value, (set, invalidate) => {
let inited = false; let inited = false;
const values = []; const values = [];
@ -197,9 +210,14 @@ export function derived<T>(stores: Stores, fn: Function, initial_value?: T): Rea
} }
}, },
() => { () => {
const invalidated = pending & (1 << i);
pending |= (1 << i); pending |= (1 << i);
}) if (!invalidated) {
); invalidate();
}
},
() => pending &= ~(1 << i)
));
inited = true; inited = true;
sync(); sync();

@ -214,6 +214,51 @@ describe('store', () => {
unsubscribe(); unsubscribe();
}); });
it('prevents glitches 2', () => {
const lastname = writable('Jekyll');
const firstname = derived(lastname, n => n === 'Jekyll' ? 'Henry' : 'Edward');
const fullname = derived([lastname, firstname], names => names.reverse().join(' '));
const values = [];
const unsubscribe = fullname.subscribe(value => {
values.push(value);
});
lastname.set('Hyde');
assert.deepEqual(values, [
'Henry Jekyll',
'Edward Hyde'
]);
unsubscribe();
});
it('prevents glitches 3', () => {
const lastname = writable('Jekyll');
const firstname_first_letter = derived(lastname, n => n === 'Jekyll' ? 'H' : 'E');
const firstname = derived(firstname_first_letter, n => n === 'H' ? 'Henry' : 'Edward');
const fullname = derived([lastname, firstname], names => names.reverse().join(' '));
const values = [];
const unsubscribe = fullname.subscribe(value => {
values.push(value);
});
lastname.set('Hyde');
assert.deepEqual(values, [
'Henry Jekyll',
'Edward Hyde'
]);
unsubscribe();
});
it('prevents diamond dependency problem', () => { it('prevents diamond dependency problem', () => {
const count = writable(0); const count = writable(0);
const values = []; const values = [];

Loading…
Cancel
Save