React: Clarification RFC : pourquoi « setState » est-il asynchrone ?

Créé le 11 nov. 2017  ·  31Commentaires  ·  Source: facebook/react

Pendant un bon moment, j'ai essayé de comprendre pourquoi setState est asynchrone. Et à défaut de trouver une réponse dans le passé, je suis arrivé à la conclusion que c'était pour des raisons historiques et probablement difficile à changer maintenant. Cependant @gaearon a indiqué qu'il y a une raison claire, donc je suis curieux de savoir :)

Quoi qu'il en soit, voici les raisons que j'entends souvent, mais je pense qu'elles ne peuvent pas être tout car elles sont trop faciles à contrer

Async setState est requis pour le rendu asynchrone

Beaucoup pensent initialement que c'est à cause de l'efficacité du rendu. Mais je ne pense pas que ce soit la raison de ce comportement, car garder la synchronisation de setState avec le rendu async semble trivial pour moi, quelque chose comme :

Component.prototype.setState = (nextState) => {
  this.state = nextState
  if (!this.renderScheduled)
     setImmediate(this.forceUpdate)
}

En fait, par exemple mobx-react permet des affectations synchrones à des observables tout en respectant la nature asynchrone du rendu

Async setState est nécessaire pour savoir quel état a été _rendu_

L'autre argument que j'entends parfois est que vous voulez raisonner sur l'état qui a été rendu, pas l'état qui a été demandé. Mais je ne suis pas sûr que ce principe ait beaucoup de mérite non plus. Conceptuellement, cela me semble étrange. Le rendu est un effet secondaire, l'état concerne les faits. Aujourd'hui, j'ai 32 ans, et l'année prochaine j'aurai 33 ans, que le composant propriétaire réussisse à restituer cette année ou non :).

Pour faire un parallèle (probablement pas très bon) : si vous ne pouviez pas _lire_ votre dernière version d'un document Word écrit par vous-même jusqu'à ce que vous l'imprimiez, ce serait assez gênant. Je ne pense pas, par exemple, que les moteurs de jeu vous donnent des informations sur l'état du jeu qui a été rendu exactement et sur les images qui ont été supprimées.

Une observation intéressante : En 2 ans mobx-react personne ne m'a jamais posé la question : Comment puis-je savoir que mes observables sont rendus ? Cette question ne semble tout simplement pas pertinente très souvent.

J'ai rencontré quelques cas où il était pertinent de savoir quelles données étaient rendues. Le cas dont je me souviens était celui où j'avais besoin de connaître les dimensions en pixels de certaines données à des fins de mise en page. Mais cela a été élégamment résolu en utilisant didComponentUpdate et ne dépendait pas non plus du fait que setState était asynchrone. Ces cas semblent si rares qu'il ne justifie guère de concevoir l'API principalement autour d'eux. Si cela peut être fait d'une manière ou d'une autre , cela suffit je pense


Je n'ai aucun doute que l'équipe React est consciente de la confusion que la nature asynchrone de setState introduit souvent, donc je soupçonne qu'il y a une autre très bonne raison pour la sémantique actuelle. Dis m'en plus :)

Discussion

Commentaire le plus utile

Voici donc quelques réflexions. Ce n'est en aucun cas une réponse complète, mais c'est peut-être encore plus utile que de ne rien dire.

Tout d'abord, je pense que nous convenons que retarder la réconciliation afin de regrouper les mises à jour est bénéfique. C'est-à-dire que nous convenons que le re-rendu de setState() synchrone serait inefficace dans de nombreux cas, et il est préférable de mettre à jour par lots si nous savons que nous en aurons probablement plusieurs.

Par exemple, si nous sommes dans un gestionnaire de navigateur click , et que Child et Parent appellent setState , nous ne voulons pas refaire le rendu les Child deux fois, et préfèrent à la place les marquer comme sales et les restituer ensemble avant de quitter l'événement du navigateur.

Vous demandez : pourquoi ne pouvons-nous pas faire exactement la même chose (batching) mais écrire les mises setState jour this.state sans attendre la fin de la réconciliation. Je ne pense pas qu'il y ait une réponse évidente (l'une ou l'autre solution a des compromis) mais voici quelques raisons auxquelles je peux penser.

Garantir la cohérence interne

Même si state est mis à jour de manière synchrone, props ne le sont pas. (Vous ne pouvez pas connaître props tant que vous n'avez pas rendu à nouveau le composant parent, et si vous le faites de manière synchrone, le traitement par lots sort de la fenêtre.)

À l'heure actuelle, les objets fournis par React ( state , props , refs ) sont cohérents entre eux. Cela signifie que si vous n'utilisez que ces objets, ils sont garantis de faire référence à un arbre entièrement réconcilié (même s'il s'agit d'une version plus ancienne de cet arbre). Pourquoi est-ce important ?

Lorsque vous utilisez uniquement l'état, s'il est vidé de manière synchrone (comme vous l'avez proposé), ce modèle fonctionnerait :

console.log(this.state.value) // 0
this.setState({ value: this.state.value + 1 });
console.log(this.state.value) // 1
this.setState({ value: this.state.value + 1 });
console.log(this.state.value) // 2

Cependant, disons que cet état doit être levé pour être partagé entre quelques composants afin de le déplacer vers un parent :

-this.setState({ value: this.state.value + 1 });
+this.props.onIncrement(); // Does the same thing in a parent

Je tiens à souligner que dans les applications React typiques qui reposent sur setState() il s'agit du type de refactorisation spécifique à React le plus courant que vous feriez quotidiennement .

Cependant, cela casse notre code !

console.log(this.props.value) // 0
this.props.onIncrement();
console.log(this.props.value) // 0
this.props.onIncrement();
console.log(this.props.value) // 0

En effet, dans le modèle que vous avez proposé, this.state serait immédiatement vidé, mais pas this.props . Et nous ne pouvons pas immédiatement vider this.props sans re-rendre le parent, ce qui signifie que nous devrions renoncer au traitement par lots (qui, selon les cas, peut dégrader les performances de manière très significative).

Il existe également des cas plus subtils de la façon dont cela peut casser, par exemple si vous mélangez des données de props (pas encore vidés) et de state (proposé pour être vidés immédiatement) pour créer un nouvel état : https://github.com/facebook/react/issues/122#issuecomment -81856416. Les références présentent le même problème : https://github.com/facebook/react/issues/122#issuecomment -22659651.

Ces exemples ne sont pas du tout théoriques. En fait, les liaisons React Redux avaient exactement ce genre de problème car elles mélangeaient des accessoires React avec un état non-React : https://github.com/reactjs/react-redux/issues/86 , https://github.com/ reactjs/react-redux/pull/99 , https://github.com/reactjs/react-redux/issues/292 , https://github.com/reactjs/redux/issues/1415 , https://github. com/reactjs/react-redux/issues/525.

Je ne sais pas pourquoi les utilisateurs de MobX ne se sont pas heurtés à cela, mais mon intuition est qu'ils pourraient se heurter à de tels scénarios mais les considèrent comme leur propre faute. Ou peut-être qu'ils ne lisent pas autant à partir de props et qu'ils lisent plutôt directement à partir d'objets mutables MobX.

Alors, comment React résout-il ce problème aujourd'hui ? Dans React, this.state et this.props ne sont mis à jour qu'après la réconciliation et le vidage, vous verriez donc 0 imprimés avant et après la refactorisation. Cela rend l'état de levage sûr.

Oui, cela peut être gênant dans certains cas. Surtout pour les personnes venant d'horizons plus OO qui veulent juste muter l'état plusieurs fois au lieu de penser comment représenter une mise à état en un seul endroit. Je peux comprendre cela, même si je pense que garder les mises à jour d'état concentrées est plus clair du point de vue du débogage : https://github.com/facebook/react/issues/122#issuecomment -19888472.

Néanmoins, vous avez la possibilité de déplacer l'état que vous souhaitez lire immédiatement dans un objet mutable latéralement, en particulier si vous ne l'utilisez pas comme source de vérité pour le rendu. C'est à peu près ce que MobX vous permet de faire .

Vous avez également la possibilité de vider l'ensemble de l'arbre si vous savez ce que vous faites. L'API s'appelle ReactDOM.flushSync(fn) . Je ne pense pas que nous l'ayons encore documenté, mais nous le ferons certainement à un moment donné au cours du cycle de publication de la 16.x. Notez qu'il force en fait un nouveau rendu complet pour les mises à jour qui se produisent à l'intérieur de l'appel, vous devez donc l'utiliser avec parcimonie. De cette façon, cela ne brise pas la garantie de cohérence interne entre props , state , et refs .

Pour résumer, le modèle React ne conduit pas toujours au code le plus concis, mais il est cohérent en interne et garantit que l'état de levage est sûr .

Activation des mises à jour simultanées

Conceptuellement, React se comporte comme s'il avait une seule file d'attente de mise à jour par composant. C'est pourquoi la discussion a du sens : nous discutons de l'opportunité d'appliquer les mises à jour à this.state immédiatement ou non, car nous n'avons aucun doute que les mises à jour seront appliquées dans cet ordre exact. Cependant, cela n'a pas besoin d'être le cas ( haha ).

Récemment, nous avons beaucoup parlé de « rendu asynchrone ». J'admets que nous n'avons pas fait un très bon travail pour communiquer ce que cela signifie, mais c'est la nature de la R&D : vous allez après une idée qui semble conceptuellement prometteuse, mais vous ne comprenez vraiment ses implications qu'après avoir passé suffisamment de temps avec elle.

Une façon dont nous avons expliqué le « rendu asynchrone » est que React pourrait attribuer différentes priorités aux appels setState() fonction de leur provenance : un gestionnaire d'événements, une réponse réseau, une animation, etc.

Par exemple, si vous saisissez un message, les appels setState() dans le composant TextBox doivent être immédiatement vidés. Cependant, si vous recevez un nouveau message pendant que vous tapez , il est probablement préférable de retarder le rendu du nouveau MessageBubble jusqu'à un certain seuil (par exemple une seconde) que de laisser la frappe bégayer en bloquant le fil.

Si nous laissons certaines mises à jour avoir une « priorité inférieure », nous pourrions diviser leur rendu en petits morceaux de quelques millisecondes afin qu'ils ne soient pas perceptibles par l'utilisateur.

Je sais que des optimisations de performances comme celle-ci peuvent ne pas sembler très excitantes ou convaincantes. Vous pourriez dire : « nous n'avons pas besoin de cela avec MobX, notre suivi des mises à jour est suffisamment rapide pour éviter simplement les re-rendus ». Je ne pense pas que ce soit vrai dans tous les cas (par exemple, quelle que soit la vitesse de MobX, vous devez toujours créer des nœuds DOM et effectuer le rendu pour les vues nouvellement montées). Pourtant, si c'était vrai, et si vous décidiez consciemment que vous êtes d'accord pour toujours encapsuler les objets dans une bibliothèque JavaScript spécifique qui suit les lectures et les écritures, vous ne bénéficierez peut-être pas autant de ces optimisations.

Mais le rendu asynchrone ne se limite pas à l'optimisation des performances.

Par exemple, considérons le cas où vous naviguez d'un écran à un autre. En règle générale, vous affichez une flèche pendant le rendu du nouvel écran.

Cependant, si la navigation est suffisamment rapide (en l'espace d'une seconde environ), le fait de clignoter et de masquer immédiatement un spinner entraîne une dégradation de l'expérience utilisateur. Pire encore, si vous avez plusieurs niveaux de composants avec différentes dépendances asynchrones (données, code, images), vous vous retrouvez avec une cascade de spinners qui clignotent brièvement un par un. Ceci est à la fois visuellement désagréable et rend votre application plus lente dans la pratique en raison de tous les reflows du DOM. C'est aussi la source de beaucoup de code passe-partout.

Ne serait-il pas bien si, lorsque vous faites un simple setState() qui rend une vue différente, nous pouvions « commencer » à rendre la vue mise à jour « en arrière-plan » ? Imaginez que sans écrire vous-même de code de coordination, vous puissiez choisir d'afficher un spinner si la mise à jour a pris plus d'un certain seuil (par exemple une seconde), et sinon laisser React effectuer une transition transparente lorsque les dépendances asynchrones du nouveau sous-arbre sont satisfait . De plus, pendant que nous «attendons», l'«ancien écran» reste interactif (par exemple, vous pouvez choisir un élément différent vers lequel effectuer la transition), et React impose que si cela prend trop de temps, vous devez afficher un spinner.

Il s'avère qu'avec le modèle React actuel et quelques ajustements des cycles de vie , nous pouvons réellement implémenter cela ! @acdlite travaille sur cette fonctionnalité depuis quelques semaines et publiera bientôt une RFC pour cela.

Notez que cela n'est possible que parce que this.state n'est pas vidé immédiatement. S'il était vidé immédiatement, nous n'aurions aucun moyen de commencer à afficher une « nouvelle version » de la vue en arrière-plan alors que « l'ancienne version » est toujours visible et interactive. Leurs mises à jour indépendantes de l'État se heurteraient.

Je ne veux pas voler le tonnerre à @acdlite en ce qui concerne l'annonce de tout cela, mais j'espère que cela semble au moins un peu excitant. Je comprends que cela puisse encore ressembler à du vaporware, ou comme si nous ne savions pas vraiment ce que nous faisons. J'espère que nous pourrons vous convaincre du contraire dans les prochains mois, et que vous apprécierez la flexibilité du modèle React. Et pour autant que je sache, au moins en partie, cette flexibilité est possible grâce au non- vidage immédiat des mises à jour d'état.

Tous les 31 commentaires

On attend tous @gaearon .

@Kaybarax Hé, c'est le week-end ;-)

@mweststrate Oh ! ma faute. Cool.

Je vais prendre des risques ici et dire que c'est à cause du regroupement de plusieurs setState s dans la même tique.

Je pars en vacances la semaine prochaine mais j'irai probablement mardi donc j'essaierai de répondre lundi.

fonction enqueueUpdate (composant) {
assurerInjecté();

// Diverses parties de notre code (telles que ReactCompositeComponent's
// _renderValidatedComponent) supposent que les appels au rendu ne sont pas imbriqués ;
// vérifie que c'est le cas. (Ceci est appelé par chaque mise à jour de niveau supérieur
// fonction, comme setState, forceUpdate, etc. ; création et
// la destruction des composants de niveau supérieur est protégée dans ReactMount.)

if (!batchingStrategy.isBatchingUpdates) {
batchingStrategy.batchedUpdates(enqueueUpdate, composant);
retourner;
}

dirtyComponents.push(composant);
if (component._updateBatchNumber == null) {
component._updateBatchNumber = updateBatchNumber + 1 ;
}
}

@mweststrate juste 2 cents : c'est une question très valable.
Je suis sûr que nous sommes tous d'accord qu'il serait beaucoup plus facile de raisonner sur l'état si setState était synchrone.
Quelles que soient les raisons de rendre setState asynchrone, je ne suis pas sûr que l'équipe réagisse bien par rapport aux inconvénients que cela introduirait, par exemple la difficulté de raisonner sur l'état actuel et la confusion que cela apporte aux développeurs.

@mweststrate fait intéressant, j'ai posé la même question ici : https://discuss.reactjs.org/t/historic-reasons-behind-setstate-not-being-immediately-visible/8487

J'ai personnellement eu et vu chez d'autres développeurs une confusion à ce sujet. @gaearon ce serait bien d'avoir une explication à ce sujet quand vous aurez un peu de temps :)

Désolé, c'est la fin de l'année et nous avons pris un peu de retard sur GitHub, etc., essayant de conclure tout ce sur quoi nous avons travaillé avant les vacances.

J'ai l'intention de revenir sur ce fil et d'en discuter. Mais c'est aussi une cible un peu mouvante car nous travaillons actuellement sur des fonctionnalités asynchrones de React qui sont directement liées à comment et quand this.state est mis à jour. Je ne veux pas passer beaucoup de temps à écrire quelque chose, puis devoir le réécrire parce que les hypothèses sous-jacentes ont changé. J'aimerais donc garder cela ouvert, mais je ne sais pas encore quand je pourrai donner une réponse définitive.

Voici donc quelques réflexions. Ce n'est en aucun cas une réponse complète, mais c'est peut-être encore plus utile que de ne rien dire.

Tout d'abord, je pense que nous convenons que retarder la réconciliation afin de regrouper les mises à jour est bénéfique. C'est-à-dire que nous convenons que le re-rendu de setState() synchrone serait inefficace dans de nombreux cas, et il est préférable de mettre à jour par lots si nous savons que nous en aurons probablement plusieurs.

Par exemple, si nous sommes dans un gestionnaire de navigateur click , et que Child et Parent appellent setState , nous ne voulons pas refaire le rendu les Child deux fois, et préfèrent à la place les marquer comme sales et les restituer ensemble avant de quitter l'événement du navigateur.

Vous demandez : pourquoi ne pouvons-nous pas faire exactement la même chose (batching) mais écrire les mises setState jour this.state sans attendre la fin de la réconciliation. Je ne pense pas qu'il y ait une réponse évidente (l'une ou l'autre solution a des compromis) mais voici quelques raisons auxquelles je peux penser.

Garantir la cohérence interne

Même si state est mis à jour de manière synchrone, props ne le sont pas. (Vous ne pouvez pas connaître props tant que vous n'avez pas rendu à nouveau le composant parent, et si vous le faites de manière synchrone, le traitement par lots sort de la fenêtre.)

À l'heure actuelle, les objets fournis par React ( state , props , refs ) sont cohérents entre eux. Cela signifie que si vous n'utilisez que ces objets, ils sont garantis de faire référence à un arbre entièrement réconcilié (même s'il s'agit d'une version plus ancienne de cet arbre). Pourquoi est-ce important ?

Lorsque vous utilisez uniquement l'état, s'il est vidé de manière synchrone (comme vous l'avez proposé), ce modèle fonctionnerait :

console.log(this.state.value) // 0
this.setState({ value: this.state.value + 1 });
console.log(this.state.value) // 1
this.setState({ value: this.state.value + 1 });
console.log(this.state.value) // 2

Cependant, disons que cet état doit être levé pour être partagé entre quelques composants afin de le déplacer vers un parent :

-this.setState({ value: this.state.value + 1 });
+this.props.onIncrement(); // Does the same thing in a parent

Je tiens à souligner que dans les applications React typiques qui reposent sur setState() il s'agit du type de refactorisation spécifique à React le plus courant que vous feriez quotidiennement .

Cependant, cela casse notre code !

console.log(this.props.value) // 0
this.props.onIncrement();
console.log(this.props.value) // 0
this.props.onIncrement();
console.log(this.props.value) // 0

En effet, dans le modèle que vous avez proposé, this.state serait immédiatement vidé, mais pas this.props . Et nous ne pouvons pas immédiatement vider this.props sans re-rendre le parent, ce qui signifie que nous devrions renoncer au traitement par lots (qui, selon les cas, peut dégrader les performances de manière très significative).

Il existe également des cas plus subtils de la façon dont cela peut casser, par exemple si vous mélangez des données de props (pas encore vidés) et de state (proposé pour être vidés immédiatement) pour créer un nouvel état : https://github.com/facebook/react/issues/122#issuecomment -81856416. Les références présentent le même problème : https://github.com/facebook/react/issues/122#issuecomment -22659651.

Ces exemples ne sont pas du tout théoriques. En fait, les liaisons React Redux avaient exactement ce genre de problème car elles mélangeaient des accessoires React avec un état non-React : https://github.com/reactjs/react-redux/issues/86 , https://github.com/ reactjs/react-redux/pull/99 , https://github.com/reactjs/react-redux/issues/292 , https://github.com/reactjs/redux/issues/1415 , https://github. com/reactjs/react-redux/issues/525.

Je ne sais pas pourquoi les utilisateurs de MobX ne se sont pas heurtés à cela, mais mon intuition est qu'ils pourraient se heurter à de tels scénarios mais les considèrent comme leur propre faute. Ou peut-être qu'ils ne lisent pas autant à partir de props et qu'ils lisent plutôt directement à partir d'objets mutables MobX.

Alors, comment React résout-il ce problème aujourd'hui ? Dans React, this.state et this.props ne sont mis à jour qu'après la réconciliation et le vidage, vous verriez donc 0 imprimés avant et après la refactorisation. Cela rend l'état de levage sûr.

Oui, cela peut être gênant dans certains cas. Surtout pour les personnes venant d'horizons plus OO qui veulent juste muter l'état plusieurs fois au lieu de penser comment représenter une mise à état en un seul endroit. Je peux comprendre cela, même si je pense que garder les mises à jour d'état concentrées est plus clair du point de vue du débogage : https://github.com/facebook/react/issues/122#issuecomment -19888472.

Néanmoins, vous avez la possibilité de déplacer l'état que vous souhaitez lire immédiatement dans un objet mutable latéralement, en particulier si vous ne l'utilisez pas comme source de vérité pour le rendu. C'est à peu près ce que MobX vous permet de faire .

Vous avez également la possibilité de vider l'ensemble de l'arbre si vous savez ce que vous faites. L'API s'appelle ReactDOM.flushSync(fn) . Je ne pense pas que nous l'ayons encore documenté, mais nous le ferons certainement à un moment donné au cours du cycle de publication de la 16.x. Notez qu'il force en fait un nouveau rendu complet pour les mises à jour qui se produisent à l'intérieur de l'appel, vous devez donc l'utiliser avec parcimonie. De cette façon, cela ne brise pas la garantie de cohérence interne entre props , state , et refs .

Pour résumer, le modèle React ne conduit pas toujours au code le plus concis, mais il est cohérent en interne et garantit que l'état de levage est sûr .

Activation des mises à jour simultanées

Conceptuellement, React se comporte comme s'il avait une seule file d'attente de mise à jour par composant. C'est pourquoi la discussion a du sens : nous discutons de l'opportunité d'appliquer les mises à jour à this.state immédiatement ou non, car nous n'avons aucun doute que les mises à jour seront appliquées dans cet ordre exact. Cependant, cela n'a pas besoin d'être le cas ( haha ).

Récemment, nous avons beaucoup parlé de « rendu asynchrone ». J'admets que nous n'avons pas fait un très bon travail pour communiquer ce que cela signifie, mais c'est la nature de la R&D : vous allez après une idée qui semble conceptuellement prometteuse, mais vous ne comprenez vraiment ses implications qu'après avoir passé suffisamment de temps avec elle.

Une façon dont nous avons expliqué le « rendu asynchrone » est que React pourrait attribuer différentes priorités aux appels setState() fonction de leur provenance : un gestionnaire d'événements, une réponse réseau, une animation, etc.

Par exemple, si vous saisissez un message, les appels setState() dans le composant TextBox doivent être immédiatement vidés. Cependant, si vous recevez un nouveau message pendant que vous tapez , il est probablement préférable de retarder le rendu du nouveau MessageBubble jusqu'à un certain seuil (par exemple une seconde) que de laisser la frappe bégayer en bloquant le fil.

Si nous laissons certaines mises à jour avoir une « priorité inférieure », nous pourrions diviser leur rendu en petits morceaux de quelques millisecondes afin qu'ils ne soient pas perceptibles par l'utilisateur.

Je sais que des optimisations de performances comme celle-ci peuvent ne pas sembler très excitantes ou convaincantes. Vous pourriez dire : « nous n'avons pas besoin de cela avec MobX, notre suivi des mises à jour est suffisamment rapide pour éviter simplement les re-rendus ». Je ne pense pas que ce soit vrai dans tous les cas (par exemple, quelle que soit la vitesse de MobX, vous devez toujours créer des nœuds DOM et effectuer le rendu pour les vues nouvellement montées). Pourtant, si c'était vrai, et si vous décidiez consciemment que vous êtes d'accord pour toujours encapsuler les objets dans une bibliothèque JavaScript spécifique qui suit les lectures et les écritures, vous ne bénéficierez peut-être pas autant de ces optimisations.

Mais le rendu asynchrone ne se limite pas à l'optimisation des performances.

Par exemple, considérons le cas où vous naviguez d'un écran à un autre. En règle générale, vous affichez une flèche pendant le rendu du nouvel écran.

Cependant, si la navigation est suffisamment rapide (en l'espace d'une seconde environ), le fait de clignoter et de masquer immédiatement un spinner entraîne une dégradation de l'expérience utilisateur. Pire encore, si vous avez plusieurs niveaux de composants avec différentes dépendances asynchrones (données, code, images), vous vous retrouvez avec une cascade de spinners qui clignotent brièvement un par un. Ceci est à la fois visuellement désagréable et rend votre application plus lente dans la pratique en raison de tous les reflows du DOM. C'est aussi la source de beaucoup de code passe-partout.

Ne serait-il pas bien si, lorsque vous faites un simple setState() qui rend une vue différente, nous pouvions « commencer » à rendre la vue mise à jour « en arrière-plan » ? Imaginez que sans écrire vous-même de code de coordination, vous puissiez choisir d'afficher un spinner si la mise à jour a pris plus d'un certain seuil (par exemple une seconde), et sinon laisser React effectuer une transition transparente lorsque les dépendances asynchrones du nouveau sous-arbre sont satisfait . De plus, pendant que nous «attendons», l'«ancien écran» reste interactif (par exemple, vous pouvez choisir un élément différent vers lequel effectuer la transition), et React impose que si cela prend trop de temps, vous devez afficher un spinner.

Il s'avère qu'avec le modèle React actuel et quelques ajustements des cycles de vie , nous pouvons réellement implémenter cela ! @acdlite travaille sur cette fonctionnalité depuis quelques semaines et publiera bientôt une RFC pour cela.

Notez que cela n'est possible que parce que this.state n'est pas vidé immédiatement. S'il était vidé immédiatement, nous n'aurions aucun moyen de commencer à afficher une « nouvelle version » de la vue en arrière-plan alors que « l'ancienne version » est toujours visible et interactive. Leurs mises à jour indépendantes de l'État se heurteraient.

Je ne veux pas voler le tonnerre à @acdlite en ce qui concerne l'annonce de tout cela, mais j'espère que cela semble au moins un peu excitant. Je comprends que cela puisse encore ressembler à du vaporware, ou comme si nous ne savions pas vraiment ce que nous faisons. J'espère que nous pourrons vous convaincre du contraire dans les prochains mois, et que vous apprécierez la flexibilité du modèle React. Et pour autant que je sache, au moins en partie, cette flexibilité est possible grâce au non- vidage immédiat des mises à jour d'état.

Merveilleuse explication en profondeur des décisions derrière l'architecture de React. Merci.

marque

Merci, Dan.

Je ❤️ ce problème. Super question et super réponse. J'ai toujours pensé que c'était une mauvaise décision de conception, maintenant je dois repenser

Merci, Dan.

Je l'appelle asyncAwesome setState :smile:

J'ai tendance à penser que tout doit être implémenté de manière asynchrone en premier, et si vous avez besoin d'une opération de synchronisation, eh bien, enveloppez l'opération asynchrone en attendant la fin. Il est beaucoup plus facile de faire du code de synchronisation à partir de code asynchrone (tout ce dont vous avez besoin est un wrapper) que l'inverse (ce qui nécessite essentiellement une réécriture complète, à moins que vous n'utilisiez le threading, ce qui n'est pas du tout léger).

@gaearon merci pour l'explication détaillée ! Cela me taraude depuis longtemps ("il doit y avoir une bonne raison, mais personne ne sait laquelle"). Mais maintenant, cela prend tout son sens et je vois à quel point c'est une décision vraiment consciente :). Merci beaucoup pour la réponse détaillée, je l'apprécie vraiment!

Ou peut-être qu'ils ne lisent pas autant à partir d'accessoires et qu'ils lisent plutôt directement à partir d'objets mutables MobX.

Je pense que c'est tout à fait vrai, dans MobX, les accessoires sont généralement utilisés comme une simple configuration de composant, et les données de domaine ne sont généralement pas capturées dans les accessoires, mais dans les entités de domaine qui sont transmises entre les composants.

Encore une fois, merci beaucoup !

@gaearon Merci pour l'explication détaillée et excellente.
Bien qu'il manque encore quelque chose ici, je pense que je comprends mais je veux être sûr.

Lorsque l'événement est enregistré "Outside React", cela signifie peut-être via addEventListener sur une ref par exemple. Ensuite, il n'y a pas de lot en cours.
Considérez ce code :

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }

  componentDidMount() {
    this.refBtn.addEventListener("click", this.onClick);
  }

  componentWillUnmount() {
    this.refBtn.removeEventListener("click", this.onClick);
  }

  onClick = () => {
    console.log("before setState", this.state.count);
    this.setState(state => ({ count: state.count + 1 }));
    console.log("after setState", this.state.count);
  };

  render() {
    return (
      <div>
        <button onClick={this.onClick}>React Event</button>
        <button ref={ref => (this.refBtn = ref)}>Direct DOM event</button>
      </div>
    );
  }
}

Lorsque nous cliquerons sur le bouton « React Event », nous verrons dans la console :
"before setState" 0
"after setState" 0
Lorsque l'autre bouton "Direct DOM event" sera cliqué, nous verrons dans la console :
"before setState" 0
"after setState" 1

Après quelques petites recherches et navigation dans le code source, je pense savoir pourquoi cela se produit.
réagir ne peut pas contrôler entièrement le flux de l'événement et ne peut pas être sûr du moment et de la manière dont le prochain événement se déclenchera, donc en tant que "mode panique", il déclenchera simplement le changement d'état immédiatement.

Quelles sont vos pensées à ce sujet? :en pensant:

@sag1v bien qu'un peu lié, il est probablement plus clair d'ouvrir un nouveau numéro pour de nouvelles questions. Utilisez simplement #11527 quelque part dans la description pour le lier à celui-ci.

@sag1v @gaearon m'avait donné une réponse très laconique ici https://twitter.com/dan_abramov/status/949992957180104704 . Je pense que son point de vue là-dessus me répondrait aussi plus concrètement.

@mweststrate J'ai pensé à ouvrir un nouveau numéro, mais j'ai réalisé que cela était directement lié à votre question "pourquoi setState asynchrone ?".
Comme cette discussion porte sur les décisions prises pour rendre setState "async", eh bien, j'ai pensé à ajouter quand et pourquoi le faire "synchroniser".
Cela ne me dérange pas d'ouvrir un nouveau problème si je ne vous ai pas convaincu que mon message est lié à ce problème :wink:

Ce @Kaybarax becuase votre question « est - il sync_ When » et non « _ Pourquoi est - il sync_ » ?.
Comme je l'ai mentionné dans mon message, je pense que je sais pourquoi, mais je veux être sûr et obtenir la réponse officielle hehe. :le sourire:

réagir ne peut pas contrôler complètement le flux de l'événement et ne peut pas être sûr de quand et comment le prochain événement se déclenchera, donc en tant que "mode panique", il déclenchera simplement le changement d'état immédiatement

Sorte de. Bien que cela ne soit pas exactement lié à la question de la mise à jour de this.state .

Ce que vous demandez, c'est dans quels cas React active le traitement par lots. React regroupe actuellement les mises à jour dans les gestionnaires d'événements gérés par React, car React "se trouve" dans le cadre supérieur de la pile d'appels et sait quand tous les gestionnaires d'événements React ont été exécutés. À ce stade, il vide la mise à jour.

Si le gestionnaire d'événements n'est pas configuré par React, il rend actuellement la mise à jour synchrone. Parce qu'il ne sait pas s'il est prudent d'attendre ou non, et si d'autres mises à jour auront lieu bientôt.

Dans les futures versions de React, ce comportement changera. Le plan est de traiter les mises à jour comme étant de faible priorité par défaut afin qu'elles finissent par fusionner et regroupées (par exemple en une seconde), avec une option pour les vider immédiatement. Vous pouvez en savoir plus ici : https://github.com/facebook/react/issues/11171#issuecomment -357945371.

Impressionnant!

Cette question et cette réponse devraient être documentées dans un endroit plus accessible. Merci les gars de nous éclairer.

Appris beaucoup . Merci

J'essaie d'ajouter mon point de vue au fil. Je travaille sur une application basée sur MobX depuis quelques mois, j'explore ClojureScript depuis des années et j'ai créé ma propre alternative React (appelée Respo), j'ai essayé Redux au début bien que très peu de temps, et je parie sur ReasonML.

L'idée de base de la combinaison de React et de la programmation fonctionnelle (FP) est que vous disposez d'une donnée que vous pouvez restituer dans une vue avec toutes les compétences dont vous disposez et qui obéissent aux lois de la FP. Vous n'avez aucun effet secondaire si vous n'utilisez que des fonctions pures.

React n'est pas purement fonctionnel. En intégrant les états locaux à l'intérieur des composants, React a le pouvoir d'interagir avec diverses bibliothèques liées au DOM et à d'autres API de navigateur (également compatibles avec MobX), ce qui rend React impur. Cependant, j'ai essayé dans ClojureScript, si React est pur, cela pourrait être un désastre car il est vraiment difficile d'interagir avec autant de bibliothèques existantes qui ont des effets secondaires.

Donc, dans Respo (ma propre solution), j'avais deux objectifs qui semblent en conflit, 1) view = f(store) donc aucun état local n'est attendu ; 2) Je n'aime pas programmer tous les états de l'interface utilisateur des composants dans les réducteurs globaux car cela pourrait être difficile à maintenir. À la fin, j'ai compris que j'avais besoin d'un sucre de syntaxe, qui m'aide à maintenir les états des composants dans un magasin global avec paths , pendant ce temps j'écris des mises à jour d'état à l'intérieur du composant avec les macros Clojure.

Donc ce que j'ai appris : les états locaux sont une fonctionnalité d'expérience du développeur, en dessous nous voulons des états globaux qui permettent à nos moteurs d'effectuer des optimisations à des niveaux profonds. Donc, les gens de MobX préfèrent la POO, est-ce pour l'expérience des développeurs ou pour les moteurs ?

Au fait, j'ai parlé de l'avenir de React et de ses fonctionnalités asynchrones, au cas où vous l'auriez manqué :
https://reactjs.org/blog/2018/03/01/sneak-peek-beyond-react-16.html

Dan, tu es mon idole... merci beaucoup.

Cette page vous a été utile?
0 / 5 - 0 notes