React Redux source function ensureCanMutateNextLis

2020-07-18 05:33发布

how to understand function ensureCanMutateNextListeners? https://github.com/reactjs/redux/blob/master/src/createStore.js

标签: reactjs redux
3条回答
Anthone
2楼-- · 2020-07-18 06:03

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.

查看更多
Summer. ? 凉城
3楼-- · 2020-07-18 06:11

It's a good question. Call @gaearon here

查看更多
在下西门庆
4楼-- · 2020-07-18 06:20

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.

查看更多
登录 后发表回答