Nessun risultato. Prova con un altro termine.
Guide
Notizie
Software
Tutorial

Operazioni con i pattern asincroni in JavaScript

Link copiato negli appunti

Nell'articolo precedente abbiamo fatto conoscenza con le promise, oggetti JavaScript che consentono di schedulare l'esecuzione di blocchi di codice non appena il risultato di una determinata operazione è reso disponibile. In questo articolo approfondiremo ulteriormente questi concetti, e vedremo anche come eseguire operazioni asincrone sfruttando i web worker.

Notificare all'utente lo stato di avanzamento di un'operazione asincrona

Come abbiamo visto nel precedente articolo, una promise accetta tre funzioni:

  • (completed hanlder) una funzione che verrà eseguita al termine dell'operazione asincrona;
  • (error handler), un handler per gestire eventuali errori verificatisi durante l'operazione;
  • (progress hanlder), una funzione che può essere usata per notificare l'avanzamento dell'operazione asincrona all'utente.

Quest'ultimo aspetto non deve essere trascurato, perché informare l'utente sul progresso di un'operazione, soprattutto se si tratta di un'operazione lunga, rappresenta un aspetto fondamentale di una moderna esperienza utente.

Non tutte le API asincrone di WinRT supportano tuttavia questa possibilità. Per comprendere meglio questo punto occorre fare un passo indietro. Nell'articolo precedente abbiamo detto che le API asincrone di WinRT restituiscono delle promise. In realtà, internamente queste API lavorano con oggetti particolari, detti anche "future type". Questi oggetti espongono quattro diversi tipi di interfaccia, a seconda che restituiscano o meno un valore, e che supportino o meno l'aggiornamento sullo stato di avanzamento delle relative operazioni.

Queste quattro interfacce, tutte derivate dalla medesima interfaccia base IAsyncInfo, sono le seguenti:

Interfaccia Descrizione
IAsyncAction rappresenta un'azione asincrona che non prevede un valore di ritorno e non supporta la notifica dello stato di avanzamento dell'operazione.
IAsyncActionWithProgress<TProgress> rappresenta un'azione asincrona che non prevede un valore di ritorno, ma che supporta la notifica dello stato di avanzamento.
IAsyncOperation<TResult> rappresenta un'operazione asincrona che restituisce un valore di tipo TResult, ma non supporta la notifica dello stato di avanzamento.
IAsyncOperationWithProgress<TResult, TProgress> rappresenta un'operazione asincrona che restituisce un valore di tipo TResult e che supporta la notifica dello stato di avanzamento.

Grazie all'uso di queste interfacce, WinRT ha potuto introdurre un ulteriore livello di astrazione nel modello di programmazione asincrona, lasciando a ciascun linguaggio il compito di definirne la relativa implementazione (ad esempio, in C# e VB la relativa implementazione si basa sulle classi della Task Parallel Library, mentre in JavaScript su promise).

Questo significa che la funzione passata come progress handler alla promise restituita da un'API di WinRT verrà utilizzata per notificare il progredire dell'operazione se e solo se l'oggetto restituito internamente da WinRT è di tipo IAsyncActionWithProgress<TProgress> o IAsyncOperationWithProgress<TResult, TProgress>.

Prendiamo ad esempio il seguente snippet (abbiamo già usato un codice simile nell'articolo precedente):

function updateUIfromFile_click() {
    var picker = new Windows.Storage.Pickers.FileOpenPicker();
    picker.fileTypeFilter.replaceAll([".txt"]);
    picker.pickSingleFileAsync()
    .then(function (file) {
        return Windows.Storage.FileIO.readTextAsync(file);
    }, errorHandler, progressHandler)
    .then(function (content) {
        fileContent.innerText = content;
    }, errorHandler, progressHandler);
}

Dal momento che entrambe queste API, PickSingleFileAsync e ReadTextAsync, restituiscono internamente un oggetto che implementa l'interfaccia IAsyncOperation, anche passando un progress handler alle relative promise (assieme a un completed hanlder e un error handler) non sortirà alcun effetto, visto che nessuna delle due API supporta la giusta interfaccia.

Il codice che segue utilizza invece la funzione StartAsync del tipo DownloadOperation per scaricare un file e salvarlo nella libreria Videos dell'utente (in questo caso occorre ricordarsi di dichiarare la relativa "capability" nell'application manifest). Questo metodo restituisce internamente un oggetto di tipo AsyncOperationWithProgress, per cui è possibile passare un progress handler per monitorare il progredire dell'operazione. Questo handler riceverà come parametro un oggetto di tipo DownloadOperation, le cui proprietà ByteReceived e TotalBytesToReceive verranno aggiornate per riflettere lo stato di avanzamento dell'operazione.

function startBackgroundDownload_click(args) {
    var uri = new Windows.Foundation.Uri("http://www.contoso.com/video.mp4");
    var docFolder = Windows.Storage.KnownFolders.videosLibrary;
    docFolder.createFileAsync("samplevideo.mp4", Windows.Storage.CreationCollisionOption.generateUniqueName)
       .then(function (file) {
           var downloader = new Windows.Networking.BackgroundTransfer.BackgroundDownloader();
           var transfer = downloader.createDownload(uri, file);
           return transfer.startAsync();
       })
       .then(
           function (result) {
               tbMessage.innerText = "Download completato.";
           },
           function (error) {
               tbMessage.innerText = "Errore";
           },
           function (args) {
               tbMessage.innerText = "Byte ricevuti " + args.progress.bytesReceived + " su " + args.progress.totalBytesToReceive;
           });
}

Cancellare un'operazione asincrona

Dal momento che le promise rappresentano operazioni asincrone, quindi potenzialmente di lunga durata, è importante avere la possibilità di annullare queste operazioni quando non siano più necessarie, senza dover necessariamente attenderne il completamento. Per far questo, è necessario mantenere una reference alla promise e, quando l'operazione non è più necessaria, invocare la funzione cancel sull'istanza della promise. Se l'operazione non è stata ancora completata (e la cancellazione è supportata), la promise enterà nello stato di errore con un valore di Error("Canceled").

Dal momento che l'annullamento di un'operazione determina uno stato di errore, è importante ricordarsi che, in questo caso, a essere invocato sarà l'error handler, ove passato alla promise (e non il completed handler).

Il codice che segue invoca la funzione WinJS.xhr per scaricare contenuti dal web e restituire una promise. La reference a questa promise è quindi utilizzata nell'handler dell'evento di click del pulsante Stop per interrompere l'operazione asincrona (se ancora non completata).

app.onloaded = function () {
    btnStartOperation.addEventListener("click", startOperation_click);
    btnStopOperation.addEventListener("click", stopOperation_click);
}
var myPromise = null;
function startOperation_click(args) {
    myPromise = WinJS.xhr({ url: "http://www.html.it" })
      .then(function (result) {
          tbMessage.innerText = "Operazione completata!";
      }, function (error) {
          if (error.name == "Canceled") {
              tbMessage.innerText = "Operazione cancellata!";
          }
          else {
              tbMessage.innerText = "Si è verificato un errore!";
          }
      }, null);
}
function stopOperation_click(args) {
    if (myPromise != null)
        myPromise.cancel();
}

Una volta annullata l'operazione, sarà invocato l'error handler fornito alla funzione then, all'interno del quale possiamo ispezionare lo stato della promise per verificare che l'operazione sia stata effettivamente annullata.

Per testare questo codice, utilizziamo il seguente markup HTML come riferimento per la pagina default.html dell'applicazione.

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>Demo.Html.it.AsyncPatterns.JS</title>
    <!-- WinJS references -->
    <link href="//Microsoft.WinJS.2.0/css/ui-dark.css" rel="stylesheet" />
    <script src="//Microsoft.WinJS.2.0/js/base.js"></script>
    <script src="//Microsoft.WinJS.2.0/js/ui.js"></script>
    <!-- Demo.Html.it.AsyncPatterns.JS references -->
    <link href="/css/default.css" rel="stylesheet" />
    <script src="/js/default.js"></script>
</head>
<body>
    <button id="btnStartOperation">Avvia operazione</button>
    <button id="btnStopOperation">Interrompi operazione</button>
    <div id="tbMessage"></div>
</body>
</html>

Un'alternativa è rappresentata dall'uso della funzione WinJS.Promise.timeout, la quale interrompe l'esecuzione dell'operazione asincrona una volta trascorso il tempo assegnato. Il prossimo snippet mostra una versione modificata della funzione startOperation_click che tiene conto di questo meccanismo:

function startOperation_click(args) {
    myPromise = WinJS.xhr({ url: "http://www.html.it" })
    WinJS.Promise.timeout(2, myPromise) // due secondi per completare l'operazione
    .then(function (result) {
        tbMessage.innerText = "Operazione completata!";
    }, function (error) {
        if (error.name == "Canceled") {
            tbMessage.innerText = "Operazione cancellata!";
        }
        else {
            tbMessage.innerText = "Si è verificato un errore!";
        }
    }, null);
}

Join, eseguire più operazioni in parallelo

Come abbiamo visto nell'articolo precedente, è possibile concatenare più promise una dietro all'altra; in questo modo, ciascuna operazione verrà eseguita in asincrono, una dopo l'altra.

operation1().then(function (result1) {
    return operation2(result1)
}).then(function (result2) {
    return operation3(result2);
}).then(function (result3) {
    // ecc.
});

A volte, tuttavia, quello di cui abbiamo bisogno è eseguire più operazioni in parallelo, senza la necessità di attendere il completamento di un'operazione prima di iniziare le altre. WinJS.Promise espone una funzione appositamente pensata per questo scopo, ossia join. Il metodo join accetta come parametro un array di promise da eseguirsi in parallelo, anziché sequenzialmente. Il seguente snippet mostra un esempio di questo metodo:

app.onloaded = function () {
    btnStartMultipleOperations.addEventListener("click", startUpload_click);
}
function startUpload_click(args) {
    var uri = Windows.Foundation.Uri("http://www.contoso.com"); // URL di destinazione
    var picturesLibrary = Windows.Storage.KnownFolders.picturesLibrary;
    var uploader = Windows.Networking.BackgroundTransfer.BackgroundUploader();
    var uploading = picturesLibrary.getFilesAsync().then(function (files) {
        return WinJS.Promise.join(
            files.map(function (file) {
                return uploader.createUpload(uri, file).
                startAsync().then(function (operation) {
                    tbMessage.innerHTML += "Operazione completata - ID: " + operation.guid;
                }, function (error) {
                    // errore
                });
            })
        )
    });
    uploading.then(
                function (result) {
                    tbMessage.innerHTML += "Tutti i file sono stati caricati";
                },
                function (error) {
                    // errore
                });
}

Questo codice carica tutte le foto presenti nella libreria Pictures dell'utente e, per ciascuna di esse, avvia una distinta operazione di upload (sfruttando la funzione map, che esegue la callback passata come parametro su tutti gli elementi dell'array). Ciascuna di queste operazioni viene eseguita in parallelo rispetto alle altre. Una volta che tutte le operazioni sono state completate, l'ultimo then aggiorna la UI informando l'utente circa l'esito dell'operazione.

Ecco il markup HTML da usare come default.html, per testare l'esempio:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>Demo.Html.it.JoiningPromises.JS</title>
    <!-- WinJS references -->
    <link href="//Microsoft.WinJS.2.0/css/ui-dark.css" rel="stylesheet" />
    <script src="//Microsoft.WinJS.2.0/js/base.js"></script>
    <script src="//Microsoft.WinJS.2.0/js/ui.js"></script>
    <!-- App3 references -->
    <link href="/css/default.css" rel="stylesheet" />
    <script src="/js/default.js"></script>
</head>
<body>
    <button id="btnStartMultipleOperations">Avvia operazioni multiple</button>
    <div id="tbMessage"></div>
</body>
</html>

Implementare codice asincrono con i web worker

Un'alternativa all'uso di promise per l'esecuzione di pattern asincroni è rappresentata dall'utilizzo di web worker. Secondo le specifiche del World Wide Web Consortium, un web worker è una modalità che permette di eseguire uno script JavaScript in background, senza dunque essere interrotto da click o altre azioni dell'utente sulla UI. Un web worker permette dunque di eseguire calcoli e operazioni complesse senza pregiudicare la reattività dell'interfaccia utente. Dal momento che sono generalmente dedicati ad eseguire task "pesanti" in termini computazionali e di risorse impiegate, è buona norma non eccedere nell'uso di questi oggetti.

Il prossimo snippet mostra un esempio di creazione di un worker:

var worker = new Worker("js\\myWorker.js");
worker.onmessage = function (event) {
    // codice omesso
}

La prima riga di codice istanzia un nuovo oggetto di tipo Worker, passando al costruttore il percorso del file JavaScript che contiene il codice da eseguire in background. Una volta che l'oggetto è stato creato, è possibile scambiare messaggi da e con il worker grazie all'evento onmessage: sia il thread della pagina principale che quello del worker restano infatti in ascolto di questo evento, che viene sollevato non appena un nuovo messaggio viene accodato tramite la funzione postMessage.

Ad esempio, il codice seguente mostra un web worker mettersi in ascolto per messaggi provenienti dalla pagina per poi girarli nuovamente al thread principale.

self.onmessage = function (e) {
    self.postMessage("Echo:" + e.data);
};

È importante ricordare che un worker non può accedere agli elementi della user interface (come elementi del DOM, gli oggetti window e document, ecc.). Si tratta di una limitazione necessaria per mantenere il modello di programmazione asincrona il più semplice possibile, evitando di doversi preoccupare del contesto di sincronizzazione dei diversi thread.

È possibile recuperare i dati dei messaggi inviati tramite la funzione postMessage accedendo alla proprietà data dell'oggetto ricevuto come parametro dall'event handler. Il seguente codice mostra un web worker usato per eseguire in background una richiesta web all'URL contenuto nel messaggio inviato dalla pagina principale.

// Necessario per usare gli oggetti di WinJS
importScripts('//Microsoft.WinJS.1.0/js/base.js');
(function () {
    "use strict";
    self.onmessage = function (e) {
        WinJS.xhr({ url: e.data.url })
        .done(function complete(result) {
            postMessage("Operazione completata: Http Status Code " + result.status);
        }, function (error) {
            postMessage("Si è verificato un errore!");
        });
    }
})();

Al termine dell'operazione, il worker comunica a sua volta alla pagina principale l'esito dell'operazione.

app.onloaded = function () {
    btnPostWorker.addEventListener("click", postWorker_click);
}
var worker = new Worker("js\\myWorker.js");
worker.onmessage = function (event) {
    tbResult.innerText = event.data;
}
function postWorker_click(args) {
    worker.postMessage({ url: "http://www.devleap.com" });
}

Ecco il markup HTML da usare in default.html, per testare questo esempio:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>Demo.Html.it.WebWorkers.JS</title>
    <!-- WinJS references -->
    <link href="//Microsoft.WinJS.2.0/css/ui-dark.css" rel="stylesheet" />
    <script src="//Microsoft.WinJS.2.0/js/base.js"></script>
    <script src="//Microsoft.WinJS.2.0/js/ui.js"></script>
    <!-- Demo.Html.it.WebWorkers.JS references -->
    <link href="/css/default.css" rel="stylesheet" />
    <script src="/js/default.js"></script>
</head>
<body>
    <button id="btnPostWorker">Post URL</button>
    <div id="tbResult"></div>
</body>
</html>

Vale la pena aggiungere che un web worker rimane in vita fino a quando non viene esplicitamente terminato, il che significa che fino a quel momento continuerà a rimanere in memoria. Esistono due modi per terminare un worker: o dalla pagina, tramite una chiamata alla funzione terminate, oppure dal worker stesso, tramite il metodo close. Il prossimo estratto mostra un esempio del primo caso:

function stopWorker_click(args) {
    if (worker != null) {
        worker.onmessage = null;
        worker.terminate();
    }
}


Ti consigliamo anche