Skip to content

Commit e649fb6

Browse files
authored
Optimize createListenerCollection (#1523)
1 parent 3eb5271 commit e649fb6

File tree

2 files changed

+96
-17
lines changed

2 files changed

+96
-17
lines changed

src/utils/Subscription.js

+39-17
Original file line numberDiff line numberDiff line change
@@ -4,46 +4,68 @@ import { getBatch } from './batch'
44
// well as nesting subscriptions of descendant components, so that we can ensure the
55
// ancestor components re-render before descendants
66

7-
const CLEARED = null
87
const nullListeners = { notify() {} }
98

109
function createListenerCollection() {
1110
const batch = getBatch()
12-
// the current/next pattern is copied from redux's createStore code.
13-
// TODO: refactor+expose that code to be reusable here?
14-
let current = []
15-
let next = []
11+
let first = null
12+
let last = null
1613

1714
return {
1815
clear() {
19-
next = CLEARED
20-
current = CLEARED
16+
first = null
17+
last = null
2118
},
2219

2320
notify() {
24-
const listeners = (current = next)
2521
batch(() => {
26-
for (let i = 0; i < listeners.length; i++) {
27-
listeners[i]()
22+
let listener = first
23+
while (listener) {
24+
listener.callback()
25+
listener = listener.next
2826
}
2927
})
3028
},
3129

3230
get() {
33-
return next
31+
let listeners = []
32+
let listener = first
33+
while (listener) {
34+
listeners.push(listener)
35+
listener = listener.next
36+
}
37+
return listeners
3438
},
3539

36-
subscribe(listener) {
40+
subscribe(callback) {
3741
let isSubscribed = true
38-
if (next === current) next = current.slice()
39-
next.push(listener)
42+
43+
let listener = (last = {
44+
callback,
45+
next: null,
46+
prev: last
47+
})
48+
49+
if (listener.prev) {
50+
listener.prev.next = listener
51+
} else {
52+
first = listener
53+
}
4054

4155
return function unsubscribe() {
42-
if (!isSubscribed || current === CLEARED) return
56+
if (!isSubscribed || first === null) return
4357
isSubscribed = false
4458

45-
if (next === current) next = current.slice()
46-
next.splice(next.indexOf(listener), 1)
59+
if (listener.next) {
60+
listener.next.prev = listener.prev
61+
} else {
62+
last = listener.prev
63+
}
64+
if (listener.prev) {
65+
listener.prev.next = listener.next
66+
} else {
67+
first = listener.next
68+
}
4769
}
4870
}
4971
}

test/utils/Subscription.spec.js

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import Subscription from '../../src/utils/Subscription'
2+
3+
describe('Subscription', () => {
4+
let notifications
5+
let store
6+
let parent
7+
8+
beforeEach(() => {
9+
notifications = []
10+
store = { subscribe: () => jest.fn() }
11+
12+
parent = new Subscription(store)
13+
parent.onStateChange = () => {}
14+
parent.trySubscribe()
15+
})
16+
17+
function subscribeChild(name) {
18+
const child = new Subscription(store, parent)
19+
child.onStateChange = () => notifications.push(name)
20+
child.trySubscribe()
21+
return child
22+
}
23+
24+
it('listeners are notified in order', () => {
25+
subscribeChild('child1')
26+
subscribeChild('child2')
27+
subscribeChild('child3')
28+
subscribeChild('child4')
29+
30+
parent.notifyNestedSubs()
31+
32+
expect(notifications).toEqual(['child1', 'child2', 'child3', 'child4'])
33+
})
34+
35+
it('listeners can be unsubscribed', () => {
36+
const child1 = subscribeChild('child1')
37+
const child2 = subscribeChild('child2')
38+
const child3 = subscribeChild('child3')
39+
40+
child2.tryUnsubscribe()
41+
parent.notifyNestedSubs()
42+
43+
expect(notifications).toEqual(['child1', 'child3'])
44+
notifications.length = 0
45+
46+
child1.tryUnsubscribe()
47+
parent.notifyNestedSubs()
48+
49+
expect(notifications).toEqual(['child3'])
50+
notifications.length = 0
51+
52+
child3.tryUnsubscribe()
53+
parent.notifyNestedSubs()
54+
55+
expect(notifications).toEqual([])
56+
})
57+
})

0 commit comments

Comments
 (0)