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

@ -214,6 +214,51 @@ describe('store', () => {
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', () => {
const count = writable(0);
const values = [];

Loading…
Cancel
Save