Dachang Technology insiste sur la sélection de bons articles pour Zhou Geng    

| Introduction Le terme  race conditions est traduit de l'anglais "race conditions". Lorsque nous développons le Web frontal, la logique la plus courante est d'obtenir et de traiter les données du serveur principal, puis de les afficher sur la page du navigateur. De nombreux détails du processus doivent être pris en compte. L'un des Cet article sera basé sur React combiné à une petite démo pour expliquer ce qu'est une condition de concurrence, ainsi qu'une introduction étape par étape à la résolution de la condition de concurrence. Différents cadres résolvent le problème de différentes manières, mais cela n'affecte pas la compréhension des conditions de concurrence.

récupérer des données

Ce qui suit est une petite démo : le frontal récupère les données de l'article et les affiche sur la page

App.tsx

import React from 'react';
import { Routes, Route } from 'react-router-dom';
import Article from './Article';

function App() {
return (
<Routes>
<Route path="/articles/:articleId" element={<Article />} />
</Routes>

);
}

export default App;

Article.tsx

import React from 'react';
import useArticleLoading from './useArticleLoading';

const Article = () => {
const { article, isLoading } = useArticleLoading();

if (!article || isLoading) {
return<div>Loading...</div>;
}

return (
<div>
<p>{article.id}</p>
<p>{article.title}</p>
<p>{article.body}</p>
</div>

);
};

export default Article;

Dans le composant Article ci-dessus, nous encapsulons la demande de données pertinente dans le crochet personnalisé "useArticleLoading". Pour l'expérience utilisateur de la page, nous affichons soit les données acquises, soit le chargement. Le jugement d'état de chargement est ajouté ici.

useArticleLoading.tsx

import { useParams } from 'react-router-dom';
import { useEffect, useState } from 'react';

interface Article {
 id: number;
 title: string;
 body: string;
}

function useArticleLoading() {
 const { articleId } = useParams<{ articleId: string }>();
 const [isLoading, setIsLoading] = useState(false);
 const [article, setArticle] = useState<Article | null>(null);

 useEffect(() => {
setIsLoading(true);
fetch(`https://get.a.article.com/articles/${articleId}`)
     .then((response) => {
       if (response.ok) {
         return response.json();
       }
       return Promise.reject();
     })
     .then((fetchedArticle: Article) => {
       setArticle(fetchedArticle);
     })
     .finally(() => {
       setIsLoading(false);
     });
 }, [articleId]);

 return {
   article,
   isLoading,
 };
}

export default useArticleLoading;

Dans ce crochet personnalisé, nous gérons l'état de chargement et la demande de données

Lorsque notre url accède à /articles/1, une requête get sera émise pour obtenir le contenu de l'article dont l'articleId correspondant est 1

La condition de course se produit

Ce qui précède est notre façon très courante d'obtenir des données, mais considérons le cas suivant (ordre chronologique) :

  • Visitez articles/1 pour voir le contenu du premier article

    • Le navigateur commence à demander au serveur d'arrière-plan d'obtenir le contenu de l'article 1

    • Il y a un problème avec la connexion réseau

    • articles/1 requête ne répond pas, les données ne sont pas affichées sur la page

  • N'attendez pas les articles/1, visitez les articles/2

    • Le navigateur commence à demander au serveur d'arrière-plan d'obtenir le contenu de l'article 2

    • Pas de problème de connexion internet

    • La demande articles/2 reçoit une réponse immédiate et les données sont affichées sur la page

  • La demande d'articles/1 a répondu

    • Remplace le contenu de l'article actuel via setArticles (fetchedArticles)

    • L'URL actuelle devrait afficher articles/2, mais elle affiche articles/1

Une chose à comprendre est que le processus de requête réseau est complexe et que le temps de réponse est incertain. Lors de l'accès à la même adresse de destination, les liens réseau par lesquels la requête passe ne sont pas nécessairement le même chemin. Par conséquent, la première demande n'est pas nécessairement la première réponse. Si le frontal est développé avec la règle de la première demande, première réponse, cela peut entraîner une mauvaise utilisation des données, ce qui est un problème de condition de concurrence.

résoudre

La solution est également très simple : lorsque la réponse est reçue, il suffit de juger si les données actuelles sont nécessaires, et sinon, de les ignorer.

Dans React, cela peut être fait proprement et facilement grâce au mécanisme d'exécution de useEffect :

useArticlesLoading.tsx

useEffect(() => {
 let didCancel = false;

 setIsLoading(true);
 fetch(`https://get.a.article.com/articles/${articleId}`)
   .then((response) => {
     if (response.ok) {
       return response.json();
     }
     return Promise.reject();
   })
   .then((fetchedArticle: Article) => {
     if (!didCancel) {
       setArticle(fetchedArticle);
     }
   })
   .finally(() => {
     setIsLoading(false);
   });

 return () => {
   didCancel = true;
 }
}, [articleId]);

Selon le mécanisme d'exécution du hook : chaque fois que vous basculez pour obtenir un nouvel article, exécutez la fonction renvoyée par useEffect, puis ré-exécutez le hook et re-rendez.

Maintenant, le bug n'apparaît plus :

  • Visitez articles/1 pour voir le contenu du premier article

    • Le navigateur commence à demander au serveur d'arrière-plan d'obtenir le contenu de l'article 1

    • Il y a un problème avec la connexion réseau

    • articles/1 requête ne répond pas, les données ne sont pas affichées sur la page

  • N'attendez pas les articles/1, visitez les articles/2

    • useArticleLoading restitue et exécute la dernière fonction de retour useEffect avant de restituer, et définit didCancel sur true

    • Pas de problème de connexion internet

    • La demande articles/2 reçoit une réponse immédiate et les données sont affichées sur la page

  • La demande d'articles/1 a répondu

    • setArticles(fetchedArticles) n'a pas été exécuté en raison de la variable didCancel.

Après le traitement, lorsque nous changeons à nouveau d'article, didCancel est vrai, et les données de l'article précédent et de setArticles ne seront plus traitées.

Solution AbortController

Bien que la solution ci-dessus par variables résolve le problème, elle n'est pas optimale. Le navigateur attend toujours que la requête se termine, mais ignore son résultat. C'est encore un gaspillage de ressources. Pour améliorer cela, nous pouvons utiliser AbortController.

Grâce à AbortController, nous pouvons abandonner une ou plusieurs requêtes. L'utilisation est simple, créez une instance d'AbortController et utilisez-la lors d'une requête :

useEffect(() => {
const abortController = new AbortController();

setIsLoading(true);
fetch(`https://get.a.rticle.com/articles/${articleId}`, {
signal: abortController.signal,
})
.then((response) => {
if (response.ok) {
return response.json();
}
return Promise.reject();
})
.then((fetchedArticle: Article) => {
setArticle(fetchedArticle);
})
.finally(() => {
setIsLoading(false);
});

return () => {
abortController.abort();
};
}, [articleId]);

En passant abortController.signal, nous pouvons facilement utiliser abortController.abort() pour abandonner la requête (ou transmettre le même signal à plusieurs requêtes, ce qui peut mettre fin à plusieurs requêtes)

Après avoir utilisé abortController, examinons l'effet :

  • Consulter les articles/1

    • Demander au serveur d'obtenir les données articles/1

  • Visitez articles/2 sans attendre de réponse

    • Rendre à nouveau le crochet, useEffect exécute la fonction de retour, exécute abortController.abort ()

    • Demander au serveur d'obtenir des articles/2 données

    • Obtenez les données articles/2 et affichez-les sur la page

  • Le premier article n'a jamais fini de se charger car nous avons tué la requête manuellement

Les requêtes interrompues manuellement peuvent être visualisées dans les outils de développement :

image

Un problème avec l'appel abortController.abort() est qu'il provoque le rejet de la promesse, ce qui peut entraîner une erreur non détectée :

image

Pour éviter cela, nous pouvons ajouter un gestionnaire d'erreur catch :

useEffect(() => {
 const abortController = new AbortController();

 setIsLoading(true);
 fetch(`https://get.a.article.com/articles/${articleId}`, {
   signal: abortController.signal,
 })
   .then((response) => {
     if (response.ok) {
       return response.json();
     }
     return Promise.reject();
   })
   .then((fetchedArticle: Article) => {
     setArticle(fetchedArticle);
   })
   .catch(() => {
     if (abortController.signal.aborted) {
       console.log('The user aborted the request');
     } else {
       console.error('The request failed');
     }
   })
   .finally(() => {
     setIsLoading(false);
   });

 return () => {
   abortController.abort();
 };
}, [articleId]);

arrêter les autres promesses

AbortController peut non seulement arrêter les requêtes asynchrones, il peut également être utilisé dans les fonctions :

function wait(time: number) {
return new Promise<void>((resolve) => {
setTimeout(() => {
resolve();
}, time);
});
}
wait(5000).then(() => {
console.log('5 seconds passed');
});
function wait(time: number, signal?: AbortSignal) {
return new Promise<void>((resolve, reject) => {
const timeoutId = setTimeout(() => {
resolve();
}, time);
signal?.addEventListener('abort', () => {
clearTimeout(timeoutId);
reject();
});
});
}
const abortController = new AbortController();

setTimeout(() => {
abortController.abort();
}, 1000);

wait(5000, abortController.signal)
.then(() => {
console.log('5 seconds passed');
})
.catch(() => {
console.log('Waiting was interrupted');
});

Passez le signal d'attendre pour mettre fin à la promesse.

autre

À propos de la compatibilité AbortController :

image

À l'exception d'IE, d'autres peuvent être utilisés en toute confiance.

Résumer

Cet article traite des conditions de concurrence dans React et explique le problème des conditions de concurrence. Pour résoudre ce problème, nous avons appris l'idée derrière AbortController et étendu la solution. En plus de cela, nous avons également appris à utiliser AbortController à d'autres fins. Cela nous oblige à creuser plus profondément et à mieux comprendre le fonctionnement d'AbortController. Pour le front-end, vous pouvez choisir votre propre solution la plus appropriée.

Discussion intéressante sur le front-end

, comme 9