Issue title: Problem with populate and exePopulate
 Issue body: <!-- *Before creating an issue please make sure you are using the latest version of mongoose -->

**Do you want to request a *feature* or report a *bug*?**
bug
**What is the current behavior?**
It seems there is a chain problem with population, I've several populations and sometimes the resulting docs are populated, and sometimes it ain't.


**If the current behavior is a bug, please provide the steps to reproduce.**
<!-- If you can, provide a standalone script / gist to reproduce your issue -->
I've this code:
```javascript
User.virtual("getComponents").get(async function getComponents() {
  //ordenamos los componentes convertidos en Objecto y luego merge de todos en orden.
  function toObj(arr) {
    return arr.reduce((obj, el) => Object.assign(obj, {[el.pos]: el.path}), {});
  }
  await this.populate("profile profiles components").execPopulate();
  await Promise.all([
    this.profile.populate("components").execPopulate(),
    this.populate({
      path: "profiles",
      populate: {path: "components"}
    }).execPopulate()
  ]);
  console.log(this.components);
  console.log(this.profile.components);
  console.log(this.profiles.map(profile => profile.components));
  const obj = Object.assign(
    {},
    ...this.profiles.map(profile => toObj(profile.components)),
    toObj(this.profile.components),
    toObj(this.components)
  );
  console.dir(obj);
  return Object.keys(obj).map(key => ({pos: key, path: obj[key], loaded: false}));
});
```

Sometimes the console.log shows:
```sh
[{"creationUserModel":"User","updateUserModel":"User","active":true,"_id":"5dfcd5facef58e1131ed2f37","pos":"Nav","path":"itdfw/components/navTest.vue","creationDate":"2019-12-20T14:08:58.000Z","creationUser":"5b3b95680becb0696bf949e1","name":"Nav - itdfw/components/navTest.vue","_update":true,"_delete":true}]
["5dfcd659cef58e1131ed2f80"]
[[]]
{ undefined: undefined, Nav: 'itdfw/components/navTest.vue' }
```
the `this.profile.components` are not populated.

and some other times
```sh
[{"creationUserModel":"User","updateUserModel":"User","active":true,"_id":"5dfcd5facef58e1131ed2f37","pos":"Nav","path":"itdfw/components/navTest.vue","creationDate":"2019-12-20T14:08:58.000Z","creationUser":"5b3b95680becb0696bf949e1","name":"Nav - itdfw/components/navTest.vue","_update":true,"_delete":true}]
[{"creationUserModel":"User","updateUserModel":"User","active":true,"_id":"5dfcd659cef58e1131ed2f80","pos":"Top","path":"@/components/navTest2.vue","creationDate":"2019-12-20T14:10:33.000Z","creationUser":"5b3b95680becb0696bf949e1","name":"Top - @/components/navTest2.vue","_update":true,"_delete":true}]
[[]]
{
   Top: '@/components/navTest2.vue',
   Nav: 'itdfw/components/navTest.vue'
 }

```

I realized this because I'm testing atlas but my dev server is in Chile, so there is lag between the queries.

AFAIK (because it happens sometimes) in 5.9.10 (didn't try all versions) there is no problem, but maybe it's just a reduced business logic (in mongoose) and the response is faster and the population occurs before.

**What is the expected behavior?**
if I wait for population the resulting doc should be populated before return the data.

**What are the versions of Node.js, Mongoose and MongoDB you are using? Note that "latest" is not a version.**
mongoose 5.9.27
node 14.6.0
mongodb atlas 4.2.8

<!-- You can print `mongoose.version` to get your current version of Mongoose: https://mongoosejs.com/docs/api.html#mongoose_Mongoose-version -->

 Comments: 
 Comment 0: What does your schema look like? And what's the purpose of this line?

```
await Promise.all([
    this.profile.populate("components").execPopulate(),
    this.populate({
      path: "profiles",
      populate: {path: "components"}
    }).execPopulate()
  ]);
```

Looks like you're trying to populate the same path two different ways, and depending on how your schema is defined, at least one of those is wrong.
 Comment 1: Hi, I've **profile** and **profiles**, both refs to model **Profile**, first single ref and second array of refs.

User and Profile model have and array of refs to Components model, so when I want to get all the components associated to a User I need to populate the 3 paths.

But my problem is that it worked before, but now it doesn't, same code with older mongoose worked fine.

What would happens if in some place my flow do ```User.populate("profile")```??? Could it repopulate profile depopulating components? I think it shouldn't but it's just my thought.

I've several virtuals who populate paths in User model, so for example the virtual link is:
```javascript
  User.virtual("link").get(async function () {
    await this.populate("profile").execPopulate();
    return (this.profile && this.profile.link) || "/";
  });
``` 
Could it be sometimes virtual link runs after virtual getComponents and depopulate components from profile?

In your examples you said populate will use the last one for the same path:
![image](https://user-images.githubusercontent.com/775491/89714617-fb5c1500-d96d-11ea-86ce-83355f12640d.png)

So maybe I need to "pre populate" before asking for toJSON with virtuals?

But a fact is it works well with 5.9.10.

Regards.
 Comment 2: Can you please post your schemas? That would be very helpful.
 Comment 3: Hi, I changed the way the model is populated so I doesn't happen now, so maybe this code wouldn't be helpful:

```javascript
"use strict";
/* global ,Log,_ */
var verbosity = "NONE INFO VERB DEBUG".split(" ");

function Schema(db) {
  let User = new db.Schema(
    {
      username: {
        type: String,
        lowercase: true,
        trim: true,
        required: "Debe existir el nombre de usuario",
        create: true,
        name: "Usuario",
        filter: true,
        auth: "password",
        _validate: {
          min: 5
        },
        uniqueCaseInsensitive: true,
        above: 1050
      },
      firstname: {
        type: String,
        trim: true,
        required: true,
        filter: true,
        name: "Nombre(s)",
        validate: [
          function corto(value) {
            return value.length > 2;
          },
          "nombre muy corto"
        ]
      },
      lastname: {
        type: String,
        trim: true,
        required: true,
        name: "Apellido(s)",
        filter: true
      },
      email: {
        type: "Email",
        required: "Debe existir el correo",
        double_check: true,
        auth: "password",
        filter: true,
        name: "Correo Electrónico",
        above: 745,
        uniqueCaseInsensitive: true,
        lower: true
      },
      password: {
        type: "Password",
        required: "Debe existir la contraseña",
        double_check: true,
        hidden: "view",
        name: "Contraseña",
        min: 8
      },
      lastlog: {
        name: "Último Ingreso",
        type: Date,
        default: Date.now,
        hidden: true,
        dateType: "datetime"
      },
      profile: {
        type: String,
        ref: "Profile",
        required: "Debe tener el perfil principal",
        readonly: "self",
        filter: true,
        name: "Perfil",
        above: 1165,
        hidden: ["register"],
        disabled: ["profile"],
        populate: true
      },
      profiles: [
        {
          type: String,
          ref: "Profile",
          readonly: "self",
          name: "Perfiles Adicionales",
          above: 1490,
          hidden: ["register"],
          disabled: ["profile"],
          populate: true
        }
      ],
      level: {
        type: Number,
        default: 10000,
        min: 0,
        max: 10000,
        restrict: true,
        filter: "$lte",
        name: "Nivel",
        description: "El nivel del usuario, mientras más bajo mayor nivel",
        above: 1245,
        hidden: ["register"],
        disabled: ["profile"]
      },
      verbosity: {
        type: String,
        default: "INFO",
        enum: verbosity,
        name: "Nivel de Registro",
        above: 1355,
        hidden: ["register"],
        disabled: ["profile"]
      },
      components: [
        {
          type: "ObjectId",
          ref: "Component",
          name: "Componentes del Layout",
          hidden: ["register", "profile"]
        }
      ],
      fb_id: {
        type: Number,
        hidden: true
      },
      access_token: {
        type: Number,
        hidden: true
      }
    },
    {
      caducate: true,
      icon: "user",
      name: "Administración de Usuarios",
      description: "Interfaz de administracion de usuarios, permite administrar a todos los usuarios del sistema",
      methods: ["GET", "POST", "PUT", "DELETE"],
      userData: ["functions", "firstname", "lastname", "link", "fullname", "getComponents"]
    }
  );
  //Extras del modelo de usuario
  User.index({username: 1}, {unique: true, partialFilterExpression: {active: true}});
  User.index({email: 1}, {unique: true, partialFilterExpression: {active: true}});
  User.index({fb_id: 1}, {unique: true, partialFilterExpression: {fb_id: {$type: "number"}}});
  function Capitalize(s) {
    return s
      .split(" ")
      .map(str => str.charAt(0).toUpperCase() + str.slice(1).toLowerCase())
      .join(" ");
  }
  User.pre("save", function (next) {
    this.lastlog = new Date();
    this.firstname = Capitalize(this.firstname);
    this.lastname = Capitalize(this.lastname);
    next();
  });
  User.method({
    async getProfiles() {
      this.populated("profile") || (await this.populate("profile").execPopulate());
      this.populated("profiles") || (await this.populate("profiles").execPopulate());
      let profiles = [];
      if (this.profile) profiles = [this.profile].concat(this.profiles).sort((a, b) => a.level - b.level);
      return profiles;
    }
  });

  User.virtual("functions").get(async function () {
    return _.uniqBy([].concat(...(await this.getProfiles().map(el => el && el.funcs))), "path");
  }).options.hidden = true;

  User.virtual("fullname").get(function () {
    return this.firstname + " " + this.lastname;
  }).options.hidden = true;
  User.virtual("name").get(function userName(value) {
    if (this.constructor.modelName === "User") return this.firstname + " " + this.lastname;
    return value;
  }).options.hidden = true;
  User.virtual("link").get(async function () {
    this.populated("profile") || (await this.populate("profile").execPopulate());
    return (this.profile && this.profile.link) || "/";
  }).options.hidden = true;

  User.static({
    getByProfile(profile) {
      return this.find({$or: [{profile}, {profiles: profile}]}).exec();
    }
  });

  User.method({
    async canSee(req) {
      this.populated("profile") || (await this.populate("profile").execPopulate());
      this.populated("profiles") || (await this.populate("profiles").execPopulate());
      return this.profile.canSee(req) || this.profiles.some(profile => profile.canSee(req));
    }
  });

  Object.assign(
    User.virtual("getComponents").get(async function getComponents() {
      //ordenamos los componentes convertidos en Objecto y luego merge de todos en orden.
      function toObj(arr) {
        return arr.reduce((obj, el) => Object.assign(obj, {[el.pos]: el.path}), {});
      }
      await Promise.all([
        this.populate("components").execPopulate(),
        this.populate({
          path: "profile",
          populate: {path: "components"}
        }).execPopulate(),
        this.populate({
          path: "profiles",
          populate: {path: "components"}
        }).execPopulate()
      ]);
      const obj = Object.assign(
        {},
        ...this.profiles.map(profile => toObj(profile.components)),
        toObj(this.profile.components),
        toObj(this.components)
      );
      return Object.keys(obj).map(key => ({pos: key, path: obj[key], loaded: false}));
    }).options,
    {
      hidden: true
    }
  );
  User.static({
    canSee(req) {
      //retorna si se puede ver una funcion.
      Log.silly("User", "static canSee");
      let {user} = req.session;
      return this.findById(user).then(user => user.canSee(req));
    }
  });
  User.method({
    async userData(req) {
      let userData = await this.autopopulate().toJSON({
        depopulate: true,
        getters: true,
        virtuals: true,
        versionKey: false
      });
      userData.functions = req.db.Profile.getFunctions(req, await userData.functions);
      return _.pick(userData, this.constructor.schema.set("userData"));
    }
  });

  return {User};
}
module.exports = Schema;
```
 Regards.
 Comment 4: I'd say the biggest problem here is that you're asynchronously mutating state in a virtual. First of all, virtuals should almost always be synchronous. Accessing `getComponents` multiple times in parallel could explain the behavior you're seeing where sometimes `execPopulate()` works and sometimes it doesn't.

Can you please explain why `getComponents` is a virtual rather than a method?
 Comment 5: How could I populate the doc to return a virtual if it's async, or just return a async value? I've LOTS of virtuals returning promises...  

But before let's declare some facts first.
* It worked for some time just fine.
* now I populate profile and profiles before and it works well.

I think it's an edge condition, it's normal in the async world, maybe sometimes it populate with the inner populate first and then repopulate without the inner populate, and sometimes it's the other way around.

the fix is in the code, first populate the profile and profiles and then populate the components in the virtual (with the autopopulate function and the populate option in the paths).

AFAIK virtuals are executed every time they are accessed. so there shouldn't be a problem between returning a promise (inmutable after fulfilling) or any other value. If I get a virtual several times in parallel, every access will have it's own promise, just like if I get a sync function.

Please tell me if I'm wrong.

Something nice will be to have statics virtual, if I have something like.

```javascript
Schema.virtual("firstAccess").get(()=>new Date());
```
Every access will have a new Date, for a doc related data I can't have external cache variable, unless I use $locals. So maybe it could be a plugin or an inner development.

It'll reduce a lot the need for recalculating virtual data, could be a solution like computed values un vue.js.

Best regards.  

 Comment 6: Virtuals are executed every time they're accessed, you're right that every access will get its own promise back. That could be a problem with async. If you access the virtual multiple times while `populate()` is in flight, you may trigger multiple populates. 
 Comment 7: Ok thanks... any thoughts or intention to do what I exposed? maybe I could mean a BIG refactor so it could start as plugin...

What do you think? it could reduce lots of processing for a doc in a flow. for example I had a virtual in an array of sub documents depending in the previous sub doc and I had to change it to static data in the db with a pre validation hook to do the calculation because it turns REALLY slow, but it's virtual data, and by definition calculated data should keep calculated.

So for example 10 subdocs, the first took the data from parent, the second took the data from the first and the first from parent, and so on, so finally with the 10 subdocs it evaluates (10+1)*10/2=55 times, and that for just one virtual and I have like 4, and for just one document.

I'd love something like vuejs computed values, but for start I couldn't do a plugin because plugins are for models, not for schemas, in your documentation it's not mentioned the ability to change `Schema.prototype`, is it safe something like:

```javascript
  db.Schema.prototype.staticVirtual = function (name, fn) {
    const schema = this; //disambiguation
    schema.virtual(name).get(function () {
      const doc = this; //disambiguation
      if (!doc.$locals.$virtuals) doc.$locals.$virtuals = {};
      const {$virtuals} = doc.$locals;
      if (!(name in $virtuals)) $virtuals[name] = fn.call(doc);
      return $virtuals[name];
    });
    return schema.virtual(name);
  };
```
???
if is it then the first part is solved, the second part is much harder because it needs a proxy, observable or events emitting on changes and it should be, IMHO, in the core.

Best regards. 
 Comment 8: Virtuals are similar to [computed values in Vue](https://masteringjs.io/tutorials/vue/computed), including the fact that neither of them support async values :)

I still don't understand what you're asking for here. What is the benefit of having a "static virtual" as opposed to just a [static function](https://mongoosejs.com/docs/guide.html#statics)?
 Comment 9: Well, I think we got out of scope, computed values in Vue only changes when a dependency changes, so if I ask for a computed value it'll remain the same unless I change a dependency value. there is a really nice plugin https://www.npmjs.com/package/vue-async-computed that allows to use async functions for computed values.

When I say static virtual I mean to calculate the value only once making it "static", not to have an static function or value.

That way my problem of virtual dependencies across sub documents get solved.

Regards.

PS: Sorry if sometimes I use misspelled words or if my phrases are somehow confusing, this is not my mother language. :D 
 Comment 10: A virtual that only runs once is an interesting idea. However, I still think a method is more idiomatic for this case.

The primary benefit of virtuals is that you can make it so all your virtuals are included in `toJSON()` and `toObject()` output. Since `JSON.stringify()` is synchronous, we can never support async virtuals for that case. So having a getter-only async virtual doesn't provide you much benefit over using a method, beyond saving you 2 characters `()`.

No worries re: language, I appreciate the discussion :)
 Comment 11: Hi, in my comment above (https://github.com/Automattic/mongoose/issues/9305#issuecomment-683444020) there is no Promise in the virtual I used, I usually use promised virtual but in this case I ain't, I haven't studied the way vue.js does it, but it seems they use a self mod to change the structure of a object to make it reactive and then re run the computed functions when a dependency changes, I think that must be a big change in the mongoose core.

I'll study deeper the case to see if it can be done out the core... 

Regards.
 Comment 12: Re: [the comment you just mentioned](https://github.com/Automattic/mongoose/issues/9305#issuecomment-683444020), I'm not sure why you have:

```javascript
db.Schema.prototype.staticVirtual = function (name, fn) {
  schema.virtual(name).get(function () { ... });
}
```

As opposed to just declaring a virtual:

```javascript
schema.virtual(name).get(function() { ... });
```
 Comment 13: Hi, that was a test to make a virtual "static" so if you ask for the virtual the first time it's saved into $locals, so the next time it wouldn't execute the function again, and to do that I asked you if it's save to add functions to the Schema prototype.

Regards.
 Comment 14: Removed `execPopulate()`
Document#populate() now returns a promise and is now no longer chainable.

Replace await doc.populate('path1').populate('path2').execPopulate(); with await doc.populate(['path1', 'path2']);
Replace await doc.populate('path1', 'select1').populate('path2', 'select2').execPopulate(); with
await doc.populate([{path: 'path1', select: 'select1'}, {path: 'path2', select: 'select2'}]);
{'source': 'GithubIssues', 'repo_name_with_owner': 'Automattic/mongoose', 'issue_id': 672983701, 'repo_stars': 24932.0}