how to understand function ensureCanMutateNextListeners? https://github.com/reactjs/redux/blob/master/src/createStore.js
问题:
回答1:
Posting relevant sections obtained from Aaron Powell's blog post here
Sample implementation of subscribe
function from redux
let subscriptions = [];
const subscribe = function (fn) {
if (typeof fn !== 'function') {
throw Error('The provided listener must be a function');
}
var subscribed = true;
// This line is what we are interested in
subscriptions.push(fn);
return function () {
if (!subscribed) {
return;
}
var index = subscriptions.indexOf(fn);
subscriptions.splice(index, 1);
subscribed = false;
};
};
For every dispatch
performed, we must notify every subscriber
.
From the blog
Now one thing you might want to add to the subscribe function is mutation hold on the subscribers collection. The reason for this that you want the collection of listeners shouldn't change while a dispatch is running.
Continued ...
So here we have a function ensureCanMutateNextListeners that, when invoked, checks if the two arrays are the same array reference, if they are, use slice to clone currentSubscriptions so that when we modify the nextSubscriptions array (adding or removing a listener) it won't impact any currently running dispatch pipeline. The goal here is to ensure that the listeners that are used by dispatch are a point in time, for when the dispatch started.
回答2:
This question has been troubling me for quite some while, while the answer and blog post above offers the core idea, it doesn't give a concrete example.
My thinking process was: javaScript is single threaded, any function has run to completion behavior - so it doesn't really make sense to say subscribe
has to worry whether there's a current dispatch operation going on. If we are inside function subscribe
, it's literally impossible to be also inside dispatch
: as nowhere inside the body of subscribe
is dispatch
being called, directly or indirectly.
With that said I just realized a flaw in the above logic today, and thought I'd share here. The explanation is while inside subscribe
, dispatch
will not be running, but the reverse is not true. subscribe
(and unsubscribe
) could be running inside dispatch
. Here's source of dispatch
:
function dispatch(action) {
// ...
const listeners = currentListeners = nextListeners
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i]
listener()
}
return action
}
Looks innocent enough right? Notice the short line: listener()
. listener
is supplied by user of redux
, and literally can be any function - including subscribe
(or a function calls store.subscribe
). Finally, Here's an actual example:
const store = createStore(someReducer);
function doSubscribe() {
store.subscribe(doSubscribe);
}
doSubscribe();
Now what happens when we dispatch? Suppose ensureCanMutateNextListeners
is not used, and subscribe
actually push listener to currentListeners
mutablly.
store.dispatch(action);
Will loop through all listeners:
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i]
listener()
}
Initially there's only one listener: listeners === [doSubscribe]
, so we call listener[0
], a.k.a doSubscribe
, which calls store.subscribe
and will push function doSubscribe
to listeners
, now listeners
have two items! So the loop will call the second function listeners[1]
, which adds another listener
, now we have three items... Because doSubscribe
subscribes itself, the loop will keeps on going, and run forever! Of course, a fix to prevent this particular infinite loop can be:
const currentListenersCount = listeners.length;
for (let i = 0; i < currentListenersCount; i++) {
const listener = listeners[i]
listener()
}
But there can be other situations that mutates listeners array in unexpected way by some listener
even if using only redux
public api.
Therefore, ensureCanMutateNextListeners
is introduced as a general solution. Inside dispatch, any indirect call to subscribe
and unsubscribe
(only two public functions redux
allows you to modify listeners) will mutate a copy of currentListeners
- producing more predictable behaviors. In the example above, with the use of ensureCanMutateNextListeners
, doSubscribe
will add a add itself to a copy of listeners
when called, the loop in dispatch
remain unaffected and complete without any issues.
回答3:
It's a good question. Call @gaearon here