TLDR: To convert your code from 1.x with the preload
function to 2.0 with async setup function, do the following:
-
Move all code that is currently in the
preload
function into thesetup
function at the top.
let img, data; function preload(){ // img = loadImage("./my-image.png"); // data = loadJSON("./my-data.json"); } function setup(){ img = loadImage("./my-image.png"); data = loadJSON("./my-data.json"); createCanvas(400, 400); }
-
Add the
async
keyword beforefunction setup
.
let img, data; async function setup(){ img = loadImage("./my-image.png"); data = loadJSON("./my-data.json"); createCanvas(400, 400); }
-
Add the
await
keyword before the load functions copied from thepreload
function.
let img, data; async function setup(){ img = await loadImage("./my-image.png"); data = await loadJSON("./my-data.json"); createCanvas(400, 400); }
When first learning p5.js, you will likely have learnt that the setup
function is run once at the start to setup initial variable values, create the canvas and other one time setup things, after that the draw
function will run over and over again in a loop.
However, if you have ever tried loading something, say an image with p5.js before, you might have done something like this:
let img;
function preload(){
img = loadImage("./my-image.png");
}
function setup(){
createCanvas(400, 400);
image(img, 0, 0, width, height);
}
What is actually going on here and why do we need the preload function, can’t we just load in setup instead? Isn’t setup
meant to be where we initialize variables? What we are doing here in the preload
function looks to be just setting up the variable value so why can’t we do it in setup
? If you already know the answer (ie. Javascript asynchronicity) you can probably skip the next section, for those who don’t let’s look at Javascript asynchronicity.
Part 1
Javascript as a language is by design single threaded, this means that Javascript can only ever run one task at a time, this can be string concatenation, math operation, assigning variables, updating the DOM, etc. This also means that operations such as loading a file over the internet must also run on this single threaded environment. However, you will have noticed that when browsing the web, you are not constantly waiting for files to be loaded one at a time over and over again, this will make the web unusable! In Javascript as well, if we wait for every instance of a file load to complete one after another before executing the following lines of code, there will be long periods of non-activity where we, and Javascript itself, is waiting around doing basically nothing.
To get around this problem, Javascript utilizes asynchronicity.
Asynchronous
Not simultaneous; not concurrent in time; -- opposed to synchronous.
The dictionary definition may not seem super helpful in our context, however applying to file loading over the internet in Javascript, we say that we load files over the internet using an asynchronous operation. An asynchronous file loading operation - a non-simultaneous/non-concurrent file loading operation, ie. it is non-simultaneous/non-concurrent with the single thread that Javascript uses. This means a file loading operation happens outside of the single threaded context and can finish at any point of the single thread.
What this means in practice is that when you ask for a file to be loaded in Javascript through an asynchronous operation, Javascript will hand that file loading operation off to the browser or OS to do the loading while continuing to run the rest of the code, regardless of whether that file loading operation has finished or not, if you come from other programming languages that does not utilize asynchronicity, this may different from what you may expect. So what happens when the file finishes loading? In that case, whenever that is and wherever Javascript has executed the code to, the browser or OS responsible for loading the file will notify Javascript that it has finished loading the file and Javascript will then decide what to do with the loaded file.
Ok so this is all a bit handwave-y and somewhat simplified. Expressed in pseudo-code we will see the following behavior:
// The next line of code runs `loadImage()` asynchronously
// meaning it will not wait until it finishes before running
// the next line below
let img = loadImage("./my-image.png");
// The next line will run while the image is still loading.
console.log("We are loading an image");
console.log(img);
// It is undefined what `img` will be as it is not loaded yet.
// At some point `img` may have the value loaded by `loadImage`
// but we don't know if it is 1 line of code later or 100
// lines of code later, or even never.
Well this doesn’t seem very useful when we have no way of knowing when the file finishes loading, where has our code executed to since it can take 1 second, 5 second, or even a whole minute for the file to load. To get around this, Javascript initially uses callback functions. In the context of asynchronous file loading, a callback function is a function that we define for Javascript to run when it is notified that the file loading has completed.
loadImage("./my-image.png", function(img){
// This is the callback function that runs when the file is
// loaded. `img` is now populated with the loaded data
console.log("Finished loading image");
});
// The next line will run while the image is still loading.
console.log("We are loading an image");
// The console will print "We are loading an image" then
// "Finished loading image", as the synchronous `console.log()`
// on the outside runs first without waiting for `loadImage()`
// then the `console.log()` inside runs when `loadImage()`
// completes.
This works and we have a point within our code where we know for sure that our file loading operation has completed. However, it also means that our code is now split into two different sections. We may be able to put most of our code within the callback function but that somewhat breaks the linear flow of the code and when we try to do multiple asynchronous operations one after another, we can fall into what’s called “callback hell”.
// Here we are trying to load three images one after another
loadImage("./my-image1.png", function(img1){
console.log("Finished loading image 1");
loadImage("./my-image2.png", function(img2){
console.log("Finished loading image 2");
loadImage("./my-image3.png", function(img3){
console.log("Finished loading image 3");
// The rest of our code ...
});
});
});
For a time that is what people did and I hope you can see why these same people wanted to come up with a better solution. Enter “promises”. Promises is a new way of handling an asynchronous operation finishing that preserves more of the linearity of the code while avoiding callback hell. Using promises, and loading one file after another, we can have the following code:
loadImage("./my-image1.png")
.then(function(img1) {
return loadImage("./my-image2.png");
})
.then(function(img2) {
return loadImage("./my-image3.png");
})
.then(function(img3) {
// The rest of our code ...
});
Now with each asynchronous operation, we can wait for it to finish then continue with what we want to do with the loaded data. No matter how many files we load in sequence, we won’t get callback hell. Promises also provide additional quality of life improvements through concurrent promise resolution with Promise.all()
but we won’t explain that in detail for now. This is definitely an improvement over plain old callbacks, however it still isn’t quite the same as writing linear code, we still need to encapsulate code within the callback function of the .then()
method. It would be nice to be able to have code that looks as linear as possible while preserving the single threaded requirement of Javascript and asynchronicity.
This is where the async/await
keywords come in. A common misconception is that async/await
is a completely new method of dealing with asynchronicity in Javascript, just like how promises and callbacks are two different methods of dealing with asynchronicity, that’s not quite true. async/await
is promises, just in a different packaging. The keywords are best understood in the context of a function:
async function loadImage(){
// Some asynchronous operation that populate `img` with data
return img;
}
In the above, the function loadImage
has the async
keyword before the function
keyword, marking it as an asynchronous function. An asynchronous function in this context simply means a function that returns a promise. The return value of the asynchronous function will evaluate into the resolved value of the returned promise. This means that you can do the following by treating the asynchronous function as a promise:
async function loadImage(){
// Some asynchronous operation that populate `img` with data
return img;
}
loadImage()
.then(function(img){
// The rest of our code ...
});
However, this doesn’t really solve the initial problem we want to solve with promises if we are just using the same syntax again. This is where await
comes in. await
can only be used in an async
function (and in modern Javascript environments in the global scope but we’ll not go into this) and it is used in front of calling a promise:
async function loadImage(){
// Some asynchronous operation that populate `img` with data
return img;
}
async function main(){
console.log("The image will now start to load");
let img = await loadImage("./my-image1.png");
// In the next line `img` will have the expected value
console.log("We have finished loading the image", img);
}
main();
When await
is used this way, it tells Javascript to wait at this point until the promise has resolved before continuing to run the code that comes after await
. There can be multiple await
s in the same async
function. This does not mean Javascript is going to sit around and do nothing however, remember this is the very thing we don’t want Javascript as a single threaded language to do right at the start. Instead, Javascript will continue running code that is not being awaited outside of the scope of the async
function, remember an async
function is just a promise like before.
async function loadImage(){
// Some asynchronous operation that populate `img` with data
return img;
}
async function main(){
console.log("A");
let img = await loadImage("./my-image1.png");
// In the next line `img` will have the expected value
console.log("C");
}
main();
console.log("B");
// The order in which we will see messages are:
// "A"
// "B"
// "C"
// `await` pause the function execution and "jump" out of
// the async function to continue on.
With the use of async/await
, within an asynchronous function (ie. the main
function in the above example), we have managed to preserve the linearity of our code and not even needing any callbacks, all the while preserving the asynchronous single thread nature of Javascript, wins all around! As such, you will see that all modern Javascript code nowadays all default to using async/await
for asynchronous operations and all functions that perform asynchronous operations returns promises in anticipation that it will be used with async/await
.
Part 2
Alright, so now we understand the history and the true nature of async/await being just promises underneath to enable linear code, let’s look at this in the context of p5.js. Back in 2014 when p5.js was first created, promises were not a thing in Javascript. Promises was introduced in 2015 and async/await
in 2016. Javascript development was very different back then because even when promises and async/await
were released in 2015/2016, browser support for these new features were very patchy, meaning that as a developer you will have to provide a patched implementation for promises if you want to use it and not being able to use async/await
at all. This leaves callbacks as the most reliable solution for dealing with asynchronicity.
As we have seen, that’s not ideal, especially not in the context of p5.js. We can still use callbacks for file loading in p5.js, however that’s not a very common use case and we can see why:
let myImg;
function setup(){
createCanvas(400, 400);
loadImage("./my-image.png", function(img){
image(img, 0, 0, width, height);
// OR
// The first few loop of draw will have an
// `undefined` `myImg` because of asynchronicity
myImg = img
);
}
Absent promises and async/await
, is there some other way to avoid callbacks and ensure the loaded data is already loaded? The preload
function that existed up until p5.js 2.0 is such a solution that is essentially a hack made possible by how p5 itself runs. p5.js code typically has a setup function that runs at the start of the sketch, this setup function since it cannot be an async
function, means that it must run its code synchronously and not wait for any asynchronous operations to complete before going onto the next line of code:
let img;
function setup(){
createCanvas(400, 400);
img = loadImage("./my-image.png");
// Since `setup` is synchronous, the next line will run
// and error because `loadImage()` has not finished.
image(img, 0, 0, width, height);
}
That is not ideal. What “preload” provides is a separate function that p5 can run before setup, that p5 artificially waits until registered asynchronous operations within it have completed, before starting setup. This is roughly how it works under the hood:
let preloadCounter = 0;
function loadImage(url){
let img = {};
preloadCounter++;
loadURLAsync(url, function(data){
img.data = data;
preloadCounter--;
});
// This works because Javascript returns object by reference
// and not by copy, meaning the callback function of `loadURLAsync`
// can modify its value after the fact.
return img;
}
// Let's say `loadImage()` is called at this point.
let interval = setInterval(function(){
if(preloadCounter === 0){
clearInterval(interval);
// Loading completed, now we can run `setup()` and
// start the sketch.
}
// Loading has not completed yet so we keep waiting.
}. 200);
This is not exactly how preload
is implemented but the general idea is the same. As you can see in this case, while we don’t have promises and async/await
, we can somewhat simulate similar behavior because we control when the sketch starts. The downsides here are that only functions that were specifically written to utilize this behavior can be used (ie. they must increment and decrement the preload counter), and that the syntax does not fit with the semantics of Javascript (this is a bit more of an advanced and philosophical argument so we won’t expand on here).
As of time of writing, we are about 10 years into promises being released in Javascript, and it is near impossible to find a Javascript environment that does not support async/await
nor a library that does not use promises for asynchronous operations. Well, of course p5.js is that exception. With the Javascript ecosystem having converged on async/await
and modern Javascript learning resources assumes and defaults to async/await
, it makes sense for p5.js to move inline with the standard, for no other reason than to fulfill one key use case for p5.js which is to be a platform for someone new to coding to learn coding in Javascript. From a personal point of view, I have always see p5.js as the starting point for a user’s journey and not the endpoint, if the user move on from p5.js to other more advanced libraries or environments because their needs have evolved, it is not a failure of the project as long as we have equipped them to take on these new challenges. I see async/await
as one of the necessary steps to bridge a user between the friendliness of p5.js and the potential complexity of a different environment. Obscuring the complexity of asynchronous operations with preload
was necessary because of callbacks and its syntax but with async/await
it has resolve the same problem preload
was trying to solve but in a better way, because now any asynchronous functions, whether they were specifically written to be used with p5.js or not, will work with p5.js.
The motivation for this change came not entirely from p5.js itself but rather partly from a community project ml5.js. When ml5.js was being reimplemented for a new major version, discussions around preload
were raised: https://github.com/ml5js/ml5-next-gen/issues/29. In particular, for ml5.js as a library to support both usage within p5.js and as a standalone library, they would need to simultaneously support a loading function both returning a promise and using preload
with returning the loaded value, even if their underlying implementation and library uses promises by default.
p5.js 2.0 is a good opportunity to consider larger changes that we were not able to otherwise and introducing async/await
is one of these changes. Removing preload as a function is also a change that we can only do with a major release such as 2.0 (this is due to semver restrictions) and while it is possible to also leave preload around, it would mean the next opportunity to remove it will be in p5.js 3.0. This question of whether to keep preload around for 2.0 was put to the p5.js 2.0 advisory committee, the consensus is to prioritise async/await
because of its prevalence in the Javascript ecosystem and to make a clean break by not including the preload
function. The point around reducing friction in students learning p5.js and learning about the wider Javascript ecosystem was also brought up as a reason to favor async/await
over preload
, which cannot be used with any other libraries that have asynchronous operations, out of the box.
Part 3
Now we have the context and reasoning behind the change out of the way, let’s see what you will need to do to update your code to work with p5.js 2.0. One point before we get into it though, p5.js 1.x versions already released are not going anywhere, if your sketch is already working you likely won’t need to update it. There will also be a p5.js 2.0 compatibility addon that replicates preload
(and other changes) so that you can still use p5.js 2.0 with preload
while you transition your syntax if you wish.
However, if you are a teacher updating your teaching material to use p5.js 2.0, or a library author updating your library to work with p5.js 2.0 without preload, or other use cases that warrant a conversion, read on!
We will first look at updating user code then we’ll look at updating library code. For user code, the trick is to first copy all the code you had in the preload function into the setup
function before any of the variables are being used, you can put it above createCanvas
if you wish to. Next add the async
keyword before function setup
so that it becomes async function setup
. Finally add the await
keyword before each of the loading functions that you just copied from the preload function, so img = loadImage(“./my-image.png”)
becomes img = await loadImage(“./my-image.png”)
. And that’s it! See below for a comparison:
Before:
let img, data;
function preload(){
img = loadImage("./my-image.png");
data = loadJSON("./my-data.json");
}
function setup(){
createCanvas(400, 400);
}
After:
let img, data;
async function setup(){
img = await loadImage("./my-image.png");
data = await loadJSON("./my-data.json");
createCanvas(400, 400);
}
Now if you plan to use a different library to do some other asynchronous operation before your sketch starts, as long as they are using promises, you can await them in the same way in the async setup function.
For library authors, the change will likely be a simplification of your code. You will no longer need to call registerPreloadMethod
, _incrementPreload
, and _decrementPreload
functions, they can be removed. Next the asynchronous functions previously calling the above functions now need to return a promise, the easiest way to do this is to prefix async
before the function
keyword like what we are doing with the setup
function above. Once these are done, your async functions will work with async setup or any other async functions out there.
Before:
p5.prototype.loadCSV = function (filename){
let result = [];
fetch(filename)
.then((res) => res.text())
.then((data) => {
data.split('\n').forEach((line) => {
result.push(line.split(','));
});
this._decrementPreload();
});
return result;
};
p5.prototype.registerPreloadMethod('loadCSV', p5.prototype);
After:
p5.prototype.loadCSV = async function (filename){
// We can return from `fetch` directly here without
// the `async` keyword since `fetch` uses promises
// but it doesn't change anything to include anyway.
return fetch(filename)
.then((res) => res.text())
.then((data) => {
let result = data.split('\n').map((line) => {
return line.split(',');
});
return result;
});
};
You may wish to preserve compatibility with 1.x for your library and there are a few options. First, you can have two versions of the library one with preload
support and one with promises support. Second, you may choose to only have one version of your library now with promises support, users wishing to use p5.js 1.x can use your latest released version that still supports preload
. Finally it is possible with some extra implementation to support both by still having your function return a promise and call the preload methods after checking for their existence.
p5.prototype.loadCSV = function (filename){
if(this._incrementPreload && this._decrementPreload){
// Here we use the old implementation
this._incrementPreload();
let result = [];
fetch(filename)
.then((res) => res.text())
.then((data) => {
data.split('\n').forEach((line) => {
result.push(line.split(','));
});
this._decrementPreload();
});
return result;
}else{
// Here we return a promise, note that we must return
// a promise and not use the `async` keyword
return fetch(filename)
.then((res) => res.text())
.then((data) => {
let result = data.split('\n').map((line) => {
return line.split(',');
});
return result;
});
}
});
There are still additional ideas that are being explored in terms of asynchronous code in p5.js such as the ability to easily load files in parallel. With changes in p5.js 2.0, we have the headroom to explore these in subsequent updates and we would like to invite your participation especially as users and learners of p5.js in these discussions.
Top comments (0)