binary draft
|
@ -69,8 +69,9 @@ new Promise(function(resolve, reject) {
|
|||
|
||||
The value returned by `.then` is a promise, that's why we are able to add another `.then` at `(2)`. When the value is returned in `(1)`, that promise becomes resolved, so the next handler runs with the value.
|
||||
|
||||
Please note: technically we can also add many `.then` to a single promise. This is not chaining:
|
||||
**A classic newbie error: technically we can also add many `.then` to a single promise. This is not chaining.**
|
||||
|
||||
For example:
|
||||
```js run
|
||||
let promise = new Promise(function(resolve, reject) {
|
||||
setTimeout(() => resolve(1), 1000);
|
||||
|
@ -92,7 +93,7 @@ promise.then(function(result) {
|
|||
});
|
||||
```
|
||||
|
||||
What we did here is just several handlers to one promise. They don't pass the result to each other, instead they process it idependantly.
|
||||
What we did here is just several handlers to one promise. They don't pass the result to each other, instead they process it independently.
|
||||
|
||||
Here's the picture (compare it with the chaining above):
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
Promise handlers `.then`/`.catch`/`.finally` are always asynchronous.
|
||||
|
||||
Even when a Promise is immediately resolved, the code on the lines *below* your `.then`/`.catch`/`.finally` may still execute first.
|
||||
Even when a Promise is immediately resolved, the code on the lines *below* your `.then`/`.catch`/`.finally` will still execute first.
|
||||
|
||||
Here's the code that demonstrates it:
|
||||
|
||||
|
@ -23,11 +23,11 @@ Why `.then` triggered after? What's going on?
|
|||
|
||||
# Microtasks
|
||||
|
||||
Asynchronous tasks need proper management. For that, the standard specifies an internal queue `PromiseJobs`, more often called the "microtask queue" (v8 term).
|
||||
Asynchronous tasks need proper management. For that, the standard specifies an internal queue `PromiseJobs`, more often referred to as "microtask queue" (v8 term).
|
||||
|
||||
As said in the [specification](https://tc39.github.io/ecma262/#sec-jobs-and-job-queues):
|
||||
|
||||
- The queue is first-in-first-out: tasks that get enqueued first are run first.
|
||||
- The queue is first-in-first-out: tasks enqueued first are run first.
|
||||
- Execution of a task is initiated only when nothing else is running.
|
||||
|
||||
Or, to say that simply, when a promise is ready, its `.then/catch/finally` handlers are put into the queue. They are not executed yet. Javascript engine takes a task from the queue and executes it, when it becomes free from the current code.
|
||||
|
@ -58,7 +58,7 @@ Now the order is as intended.
|
|||
|
||||
Browser Javascript, as well as Node.js, is based on an *event loop*.
|
||||
|
||||
"Event loop" is a process when the engine sleeps waits for events.
|
||||
"Event loop" is a process when the engine sleeps and waits for events, then reacts on those and sleeps again.
|
||||
|
||||
Examples of events:
|
||||
- `mousemove`, a user moved their mouse.
|
||||
|
@ -71,9 +71,11 @@ Things happen -- the engine handles them -- and waits for more to happen (while
|
|||
|
||||

|
||||
|
||||
When an event happens, and the engine is busy, it gets into a so-called "macrotask queue" (v8 term).
|
||||
As you can see, there's also a queue here. A so-called "macrotask queue" (v8 term).
|
||||
|
||||
For instance, while the engine is busy processing a network `fetch`, a user may move their mouse causing `mousemove`, and `setTimeout` is due and so on, just as painted on the picture above.
|
||||
When an event happens, and the engine is busy, the event is enqueued.
|
||||
|
||||
For instance, while the engine is busy processing a network `fetch`, a user may move their mouse causing `mousemove`, and `setTimeout` may be due and so on, just as painted on the picture above.
|
||||
|
||||
Events from the macrotask queue are processed on "first came – first served" basis. When the engine browser finishes with `fetch`, it handles `mousemove` event, then `setTimeout` handler, and so on.
|
||||
|
||||
|
@ -120,15 +122,19 @@ Promise.resolved()
|
|||
|
||||
Naturally, `promise` shows up first, because `setTimeout` macrotask awaits in the less-priority macrotask queue.
|
||||
|
||||
**As a side effect, macrotasks are handled only when promises give the engine a "free time".**
|
||||
|
||||
So call have a promise chain that doesn't wait for anything, then things like `setTimeout` or event handlers can never get in the middle.
|
||||
|
||||
## Summary
|
||||
|
||||
Promise handling is always asynchronous, as all promise actions pass through the internal "promise jobs" queue, also called "microtask queue" (v8 term).
|
||||
- Promise handling is always asynchronous, as all promise actions pass through the internal "promise jobs" queue, also called "microtask queue" (v8 term).
|
||||
|
||||
**So, `.then/catch/finally` is called after the current code is finished.**
|
||||
|
||||
If we need to guarantee that a piece of code is executed after `.then/catch/finally`, it's best to add it into a chained `.then` call.
|
||||
|
||||
There's also a "macrotask queue" that keeps various events, network operation results, `setTimeout`-scheduled calls, and so on. These are also called "macrotasks" (v8 term).
|
||||
- There's also a "macrotask queue" that keeps various events, network operation results, `setTimeout`-scheduled calls, and so on. These are also called "macrotasks" (v8 term).
|
||||
|
||||
The engine uses the macrotask queue to handle them in the appearance order.
|
||||
|
||||
|
@ -136,4 +142,4 @@ The engine uses the macrotask queue to handle them in the appearance order.
|
|||
|
||||
In other words, they have lower priority.
|
||||
|
||||
So the order is: regular code, then promise handling, then everything else.
|
||||
So the order is: regular code, then promise handling, then everything else, like events etc.
|
||||
|
|
|
@ -307,7 +307,7 @@ async function f() {
|
|||
})();
|
||||
```
|
||||
|
||||
There's no ambiguity here: `await` always finishes first.
|
||||
There's no ambiguity here: `await` always finishes first, because (as a microtask) it has a higher priority than `setTimeout` handling.
|
||||
|
||||
## Summary
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ libs:
|
|||
|
||||
IndexedDB is a built-in database, much more powerful than `localStorage`.
|
||||
|
||||
- Also key/value storage, but not only strings: value can be (almost) anything, multiple key types.
|
||||
- Key/value storage not only strings: value can be (almost) anything, multiple key types.
|
||||
- Supports transactions for reliability.
|
||||
- Supports key range queries, indexes.
|
||||
- Can store much more data than `localStorage`.
|
||||
|
@ -16,7 +16,7 @@ That power is usually excessive for traditional client-server apps. IndexedDB is
|
|||
|
||||
The native interface to IndexedDB, described in the specification <https://www.w3.org/TR/IndexedDB>, is event-based.
|
||||
|
||||
We can also use `async/await`-based interface with the help of a promise-based wrapper, like <https://github.com/jakearchibald/idb>. That's really convenient, but the wrapper is not perfect, it can't replace events for all cases, so we'll start with events, and then use the wrapper.
|
||||
We can also use `async/await` with the help of a promise-based wrapper, like <https://github.com/jakearchibald/idb>. That's pretty convenient, but the wrapper is not perfect, it can't replace events for all cases, so we'll start with events, and then use the wrapper.
|
||||
|
||||
## Open database
|
||||
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
function concat(arrays) {
|
||||
// sum of individual array lengths
|
||||
let totalLength = arrays.reduce((acc, value) => acc + value.length, 0);
|
||||
|
||||
if (!arrays.length) return null;
|
||||
|
||||
let result = new Uint8Array(totalLength);
|
||||
|
||||
// for each array - copy it over result
|
||||
// next array is copied right after the previous one
|
||||
let length = 0;
|
||||
for(let array of arrays) {
|
||||
result.set(array, length);
|
||||
length += array.length;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
function concat(arrays) {
|
||||
// ...your code...
|
||||
}
|
||||
|
||||
let chunks = [
|
||||
new Uint8Array([0, 1, 2]),
|
||||
new Uint8Array([3, 4, 5]),
|
||||
new Uint8Array([6, 7, 8])
|
||||
];
|
||||
|
||||
console.log(Array.from(concat(chunks))); // 0, 1, 2, 3, 4, 5, 6, 7, 8
|
||||
|
||||
console.log(concat(chunks).constructor.name); // Uint8Array
|
31
6-binary/01-arraybuffer-and-views/01-concat/_js.view/test.js
Normal file
|
@ -0,0 +1,31 @@
|
|||
describe("concat", function() {
|
||||
let chunks = [
|
||||
new Uint8Array([0, 1, 2]),
|
||||
new Uint8Array([3, 4, 5]),
|
||||
new Uint8Array([6, 7, 8])
|
||||
];
|
||||
|
||||
it("result has the same array type", function() {
|
||||
|
||||
let result = concat(chunks);
|
||||
|
||||
assert.equal(result.constructor, Uint8Array);
|
||||
});
|
||||
|
||||
it("concatenates arrays", function() {
|
||||
|
||||
let result = concat(chunks);
|
||||
|
||||
assert.deepEqual(result, new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7, 8]));
|
||||
|
||||
});
|
||||
|
||||
it("returns empty array on empty input", function() {
|
||||
|
||||
let result = concat([]);
|
||||
|
||||
assert.equal(result.length, 0);
|
||||
|
||||
});
|
||||
|
||||
});
|
0
6-binary/01-arraybuffer-and-views/01-concat/solution.md
Normal file
4
6-binary/01-arraybuffer-and-views/01-concat/task.md
Normal file
|
@ -0,0 +1,4 @@
|
|||
|
||||
# Concatenate typed arrays
|
||||
|
||||
Given an array of `Uint8Array`, write a function `concat(arrays)` that returns a concatenation of them into a single array.
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.2 KiB |
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 6.8 KiB |
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.2 KiB |
Before Width: | Height: | Size: 6.7 KiB After Width: | Height: | Size: 6.7 KiB |
After Width: | Height: | Size: 40 KiB |
After Width: | Height: | Size: 99 KiB |
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
261
6-binary/01-arraybuffer-and-views/article.md
Normal file
|
@ -0,0 +1,261 @@
|
|||
# ArrayBuffer and views
|
||||
|
||||
Binary data appears when we work with arbitrary files (uploading, downloading, creation). Or when we want to do image/audio processing.
|
||||
|
||||
That's all possible in Javascript, and binary operations are high-performant.
|
||||
|
||||
Although, there's a bit of confusion, because there are many classes. To name a few:
|
||||
- `ArrayBuffer`, `Uint8Array`, `DataView`, etc.
|
||||
|
||||
It may seem complex, but in fact it's not. Everything's fairly simple.
|
||||
|
||||
Let's sort things out.
|
||||
|
||||
**The basic binary object is `ArrayBuffer` -- a fixed-length raw sequence of bytes.**
|
||||
|
||||
We create it like this:
|
||||
```js run
|
||||
let buffer = new ArrayBuffer(16); // create a buffer of length 16
|
||||
alert(buffer.byteLength); // 16
|
||||
```
|
||||
|
||||
This allocates a contiguous memory area of 16 bytes and pre-fills it with zeroes.
|
||||
|
||||
```warn header="`ArrayBuffer` is not `Array` at all"
|
||||
`ArrayBuffer` has nothing in common with `Array`:
|
||||
- It has a fixed length, we can't increase or decrease it.
|
||||
- It takes exactly that much space in the memory.
|
||||
- It can't store any value: only bytes, the exact number of them.
|
||||
```
|
||||
|
||||
`ArrayBuffer` is a raw sequence of bytes. What's stored in it? It has no clue.
|
||||
|
||||
**To manipulate an `ArrayBuffer`, we need to use a "view" object.**
|
||||
|
||||
A view object does not store anything on it's own. It's just an "eyeglasses" that gives an interpretation of the bytes stored in the `ArrayBuffer`.
|
||||
|
||||
For instance:
|
||||
|
||||
- **`Uint8Array`** -- treats each byte as a separate number, with possible values are from 0 to 255 (a byte is 8-bit, so can hold only that much). That's called a "8-bit unsigned integer".
|
||||
- **`Uint16Array`** -- treats every 2 bytes as an integer, with possible values from 0 to 65535. That's called a "16-bit unsigned integer".
|
||||
- **`Uint32Array`** -- treats every 4 bytes as an integer, with possible values from 0 to 4294967295. That's called a "32-bit unsigned integer".
|
||||
- **`Float64Array`** -- treats every 8 bytes as a floating point number with possible values from <code>5.0x10<sup>-324</sup></code> to <code>1.8x10<sup>308</sup></code>.
|
||||
|
||||
So, the binary data in an `ArrayBuffer` of 16 bytes can be interpreted as 16 "tiny numbers", or 8 bigger numbers, or 4 even bigger, or 2 floating-point values with high precision.
|
||||
|
||||

|
||||
|
||||
`ArrayBuffer` is the core object, the root of everything, the raw binary data. But if we're going to write into it, or iterate over it, basically for almost any operation – we must use a view, e.g:
|
||||
|
||||
```js run
|
||||
let buffer = new ArrayBuffer(16); // create a buffer of length 16
|
||||
|
||||
*!*
|
||||
let view = new Uint32Array(buffer); // treat every 4 bytes as an integer number
|
||||
*/!*
|
||||
|
||||
alert(view.length); // 4, it stores that many numbers
|
||||
alert(view.byteLength); // 16, the length in bytes
|
||||
|
||||
// let's write a value
|
||||
view[0] = 123456;
|
||||
|
||||
// iterate over bytes
|
||||
for(let num of view) {
|
||||
alert(num); // 123456, then 0, 0, 0 (4 numbers total)
|
||||
}
|
||||
|
||||
alert(Uint32Array.BYTES_PER_ELEMENT); // 4 (that many bytes per number)
|
||||
```
|
||||
|
||||
The common term for all these views is [TypedArray](https://tc39.github.io/ecma262/#sec-typedarray-objects). They share the same set of methods and properities.
|
||||
|
||||
They are much more like regular arrays: have indexes and iterable.
|
||||
|
||||
## TypedArray
|
||||
|
||||
A typed array constructor (be it `Int8Array` or `Float64Array`, doesn't matter) behaves differently depending on argument types.
|
||||
|
||||
There are 5 variants:
|
||||
|
||||
```js
|
||||
new TypedArray(buffer, [byteOffset], [length]);
|
||||
new TypedArray(object);
|
||||
new TypedArray(typedArray);
|
||||
new TypedArray(length);
|
||||
new TypedArray();
|
||||
```
|
||||
|
||||
A view cannot exist without an underlying `ArrayBuffer`, so gets created automatically in all these calls except the first one.
|
||||
|
||||
1. If an `ArrayBuffer` argument is supplied, the view is created over it. We used that syntax already.
|
||||
|
||||
Optionally we can provide `byteOffset` to start from (0 by default) and the `length` (till the end of the buffer by default).
|
||||
|
||||
2. If an `Array`, or any array-like object is given, it behaves as `Array.from`: creates a typed array of the same length and copies the content.
|
||||
|
||||
We can use it to pre-fill the array with the data:
|
||||
```js run
|
||||
*!*
|
||||
let arr = new Uint8Array([0, 1, 2, 3]);
|
||||
*/!*
|
||||
alert( arr.length ); // 4
|
||||
alert( arr[1] ); // 1
|
||||
```
|
||||
3. If another `TypedArray` is supplied, it's contents is also copied. Values are converted to the new type in the process. The new array will have the same length.
|
||||
```js run
|
||||
let arr16 = new Uint16Array([1, 1000]);
|
||||
*!*
|
||||
let arr8 = new Uint8Array(arr16);
|
||||
*/!*
|
||||
alert( arr8[0] ); // 1
|
||||
alert( arr8[1] ); // 232 (tried to copy 1000, but can't fit 1000 into 8 bits)
|
||||
```
|
||||
|
||||
4. For a numeric argument `length` -- creates an `ArrayBuffer` to contain that many elements. Its byte length will be `length` multiplied by the number of bytes in a single item `TypedArray.BYTES_PER_ELEMENT`:
|
||||
```js run
|
||||
let arr = new Uint16Array(4);
|
||||
alert( Uint16Array.BYTES_PER_ELEMENT ); // 2 bytes per number
|
||||
alert( arr.byteLength ); // 8 bytes in the buffer, to contain four 2-byte numbers
|
||||
```
|
||||
|
||||
5. Without arguments, creates an zero-length empty `ArrayBuffer`.
|
||||
|
||||
Here's the full list of typed arrays:
|
||||
|
||||
- `Uint8Array`, `Uint16Array`, `Uint32Array` -- for integer numbers of 8, 16 and 32 bits.
|
||||
- `Uint8ClampedArray` -- for 8-bit integers, "clamps" them on assignment (see below).
|
||||
- `Int8Array`, `Int16Array`, `Int32Array` -- for signed integer numbers (can be negative).
|
||||
- `Float32Array`, `Float64Array` -- for signed floating-point numbers of 32 and 64 bits.
|
||||
|
||||
```warn header="No `int8` or similar single-valued types"
|
||||
Please note, despite of the names like `Int8Array`, there's no type like `int8`, or similar in Javascript.
|
||||
|
||||
That's logical, as `Int8Array` is technically not an array, but rather a view on `ArrayBuffer`.
|
||||
```
|
||||
|
||||
### Out-of-bounds behavior
|
||||
|
||||
What if we attempt to write an out-of-bounds value into a typed array? There will be no error. But the result may be not what we want.
|
||||
|
||||
For instance, let's try to put 256 into `Uint8Array`. In binary form, 256 is `100000000` (9 bits), but `Uint8Array` only provides 8 bits per value, that makes the available range from 0 to 255.
|
||||
|
||||
For bigger numbers, only the rightmost (less significant) 8 bits are stored, and the rest is cut off:
|
||||
|
||||

|
||||
|
||||
So we'll get zero.
|
||||
|
||||
For 257, the binary form is `100000001` (9 bits), the rightmost 8 get stored, so we'll have `1` in the array:
|
||||
|
||||

|
||||
|
||||
In other words, the number modulo 2<sup>8</sup> is saved.
|
||||
|
||||
Here's the demo:
|
||||
|
||||
```js run
|
||||
let buffer = new ArrayBuffer(16);
|
||||
let uint8array = new Uint8Array(buffer);
|
||||
|
||||
let num = 256;
|
||||
alert(num.toString(2)); // 100000000 (binary representation)
|
||||
|
||||
uint8array[0] = 256;
|
||||
uint8array[1] = 257;
|
||||
|
||||
alert(uint8array[0]); // 0
|
||||
alert(uint8array[1]); // 1
|
||||
```
|
||||
|
||||
`Uint8ClampedArray` is special in this aspect, its behavior is different. It saves 255 for any number that is greater than 255, and 0 for any negative number. That behavior is sometimes useful for image processing.
|
||||
|
||||
## TypedArray methods
|
||||
|
||||
`TypedArray` has regular `Array` methods, with notable exceptions.
|
||||
|
||||
We can iterate, `map`, `slice`, `find`, `reduce` etc.
|
||||
|
||||
There are few things we can't do though:
|
||||
|
||||
- No `splice` -- we can't "delete" a value, because typed arrays are views on a buffer, and these are fixed, contiguous areas of memory. All we can do is to assign a zero.
|
||||
- No `concat` method.
|
||||
|
||||
There are two additional properties:
|
||||
|
||||
- `arr.buffer` -- the reference to the underlying `ArrayBuffer`.
|
||||
- `arr.byteLength` -- the byte size of the underlying `ArrayBuffer`.
|
||||
|
||||
...And methods:
|
||||
|
||||
- `arr.set(fromArr, [offset])` copies all elements from `fromArr` to the `arr`, starting at position `offset` (0 by default).
|
||||
- `arr.subarray([begin, end])` creates a new view of the same type from `begin` to `end` (exclusive). That's similar to `arr.slice` method, but doesn't copy anything -- just creates a new view.
|
||||
|
||||
These methods allow us to copy typed arrays one onto another, create new arrays from existing ones.
|
||||
|
||||
|
||||
|
||||
## DataView
|
||||
|
||||
[DataView](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView) is a special super-flexible "untyped" view over `ArrayBuffer`.
|
||||
|
||||
It allows to access the data on any offset in any format.
|
||||
|
||||
- For typed arrays, the constructor dictates what the format is, then we access i-th number as `arr[i]`. All array members are assumed to have the same format.
|
||||
- With `DataView` we access the data with methods like `.getUint8(i)`. So we decide what the format is at method call time instead of the construction time.
|
||||
|
||||
The syntax:
|
||||
|
||||
```js
|
||||
new DataView(buffer, [byteOffset], [byteLength])
|
||||
```
|
||||
|
||||
- **`buffer`** -- the underlying `ArrayBuffer`. Unlike typed arrays, `DataView` doesn't create a buffer on its own. We need to have it ready.
|
||||
- **`byteOffset`** -- the starting byte position of the view (by default 0).
|
||||
- **`byteLength`** -- the byte length of the view (by default till the end of `buffer`).
|
||||
|
||||
For instance, here we extract numbers in different formats from the same buffer:
|
||||
|
||||
```js run
|
||||
let buffer = new Uint8Array([255, 255, 255, 255]).buffer;
|
||||
|
||||
let dataView = new DataView(buffer);
|
||||
|
||||
// get 8-bit number at offset 0
|
||||
alert( dataView.getUint8(0) ); // 255
|
||||
|
||||
// now get 16-bit number at offset 0, that's 2 bytes, both with max value
|
||||
alert( dataView.getUint16(0) ); // 65535 (biggest 16-bit unsigned int)
|
||||
|
||||
// get 32-bit number at offset 0
|
||||
alert( dataView.getUint32(0) ); // 4294967295 (biggest 32-bit unsigned int)
|
||||
|
||||
dataView.setUint32(0, 0); // set 4-byte number to zero
|
||||
```
|
||||
|
||||
`DataView` is great when we store mixed-format data in the same buffer. E.g 8-bit integers followed by 32-bit floats, or more complex.
|
||||
|
||||
## Summary
|
||||
|
||||
`ArrayBuffer` is the core object, a raw byte sequence.
|
||||
|
||||
To do almost any operation on `ArrayBuffer`, we need a view.
|
||||
|
||||
- It can be a `TypedArray`:
|
||||
- `Uint8Array`, `Uint16Array`, `Uint32Array` -- for integer numbers of 8, 16 and 32 bits.
|
||||
- `Uint8ClampedArray` -- for 8-bit integers, "clamps" them on assignment.
|
||||
- `Int8Array`, `Int16Array`, `Int32Array` -- for signed integer numbers (can be negative).
|
||||
- `Float32Array`, `Float64Array` -- for signed floating-point numbers of 32 and 64 bits.
|
||||
- Or a `DataView` -- the view that uses methods to specify a format, e.g. `getUint8(offset)`.
|
||||
|
||||
In most cases we create and operate directly on typed arrays, leaving `ArrayBuffer` under cover, as a "common discriminator". We can access it as `.buffer` and make another view if needed.
|
||||
|
||||
There are also two additional terms:
|
||||
- `ArrayBufferView` is an umbrella term for all these kinds of views.
|
||||
- `BufferSource` is an umbrella term for `ArrayBuffer` or `ArrayBufferView`.
|
||||
|
||||
These are used in descriptions of methods that operate on binary data. `BufferSource` is one of the most common words, as it means "`ArrayBuffer` or a view over it"
|
||||
|
||||
Here's a cheatsheet:
|
||||
|
||||

|
|
@ -1,13 +0,0 @@
|
|||
function makeCounter() {
|
||||
let count = 0;
|
||||
|
||||
function counter() {
|
||||
return count++;
|
||||
}
|
||||
|
||||
counter.set = value => count = value;
|
||||
|
||||
counter.decrease = () => count--;
|
||||
|
||||
return counter;
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
function makeCounter() {
|
||||
let count = 0;
|
||||
|
||||
// ... your code ...
|
||||
}
|
||||
|
||||
let counter = makeCounter();
|
||||
|
||||
alert( counter() ); // 0
|
||||
alert( counter() ); // 1
|
||||
|
||||
counter.set(10); // set the new count
|
||||
|
||||
alert( counter() ); // 10
|
||||
|
||||
counter.decrease(); // decrease the count by 1
|
||||
|
||||
alert( counter() ); // 10 (instead of 11)
|
|
@ -1,41 +0,0 @@
|
|||
describe("counter", function() {
|
||||
|
||||
it("increases from call to call", function() {
|
||||
|
||||
let counter = makeCounter();
|
||||
|
||||
assert.equal( counter(), 0 );
|
||||
assert.equal( counter(), 1 );
|
||||
assert.equal( counter(), 2 );
|
||||
});
|
||||
|
||||
|
||||
describe("counter.set", function() {
|
||||
it("sets the count", function() {
|
||||
|
||||
let counter = makeCounter();
|
||||
|
||||
counter.set(10);
|
||||
|
||||
assert.equal( counter(), 10 );
|
||||
assert.equal( counter(), 11 );
|
||||
});
|
||||
});
|
||||
|
||||
describe("counter.decrease", function() {
|
||||
it("decreases the count", function() {
|
||||
|
||||
let counter = makeCounter();
|
||||
|
||||
counter.set(10);
|
||||
|
||||
assert.equal( counter(), 10 );
|
||||
|
||||
counter.decrease();
|
||||
|
||||
assert.equal( counter(), 10 );
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
});
|
|
@ -1,13 +0,0 @@
|
|||
function makeCounter() {
|
||||
let count = 0;
|
||||
|
||||
function counter() {
|
||||
return count++;
|
||||
}
|
||||
|
||||
counter.set = value => count = value;
|
||||
|
||||
counter.decrease = () => count--;
|
||||
|
||||
return counter;
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
function makeCounter() {
|
||||
let count = 0;
|
||||
|
||||
// ... your code ...
|
||||
}
|
||||
|
||||
let counter = makeCounter();
|
||||
|
||||
alert( counter() ); // 0
|
||||
alert( counter() ); // 1
|
||||
|
||||
counter.set(10); // set the new count
|
||||
|
||||
alert( counter() ); // 10
|
||||
|
||||
counter.decrease(); // decrease the count by 1
|
||||
|
||||
alert( counter() ); // 10 (instead of 11)
|
|
@ -1,4 +0,0 @@
|
|||
|
||||
# Concatenate typed arrays
|
||||
|
||||
Given multiple `chunks`, concatenate them into a single typed array.
|
|
@ -1,41 +0,0 @@
|
|||
describe("counter", function() {
|
||||
|
||||
it("increases from call to call", function() {
|
||||
|
||||
let counter = makeCounter();
|
||||
|
||||
assert.equal( counter(), 0 );
|
||||
assert.equal( counter(), 1 );
|
||||
assert.equal( counter(), 2 );
|
||||
});
|
||||
|
||||
|
||||
describe("counter.set", function() {
|
||||
it("sets the count", function() {
|
||||
|
||||
let counter = makeCounter();
|
||||
|
||||
counter.set(10);
|
||||
|
||||
assert.equal( counter(), 10 );
|
||||
assert.equal( counter(), 11 );
|
||||
});
|
||||
});
|
||||
|
||||
describe("counter.decrease", function() {
|
||||
it("decreases the count", function() {
|
||||
|
||||
let counter = makeCounter();
|
||||
|
||||
counter.set(10);
|
||||
|
||||
assert.equal( counter(), 10 );
|
||||
|
||||
counter.decrease();
|
||||
|
||||
assert.equal( counter(), 10 );
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
});
|
|
@ -1,129 +0,0 @@
|
|||
# Binary data
|
||||
|
||||
There's a lot of terms in Javascript, related to binary data: files, images and so on.
|
||||
|
||||
To name a few:
|
||||
- `ArrayBuffer`, `Uint8Array`, `BufferSource`, `DataView`, `Blob`.
|
||||
|
||||
There are many questions how to convert one thing into another, because people just don't spare a minute to understand.
|
||||
|
||||
It may seem complex, but in fact it's not. Everything's fairly simple.
|
||||
|
||||
Let's sort things out.
|
||||
|
||||
**The basic binary entity is `ArrayBuffer` -- a fixed-length raw sequence of bytes.**
|
||||
|
||||
We create it like this:
|
||||
```js run
|
||||
let buffer = new ArrayBuffer(16); // create a buffer of length 16
|
||||
alert(buffer.byteLength); // 16
|
||||
```
|
||||
|
||||
This allocates a contiguous memory area of 16 bytes and pre-fills it with zeroes.
|
||||
|
||||
```warn header="`ArrayBuffer` is not `Array` at all"
|
||||
Please note: technically `ArrayBuffer` has nothing in common with `Array`:
|
||||
- It has a fixed length, we can't increase or decrease it.
|
||||
- It takes exactly that much space in the memory.
|
||||
- It can only store bytes, the exact number of them.
|
||||
```
|
||||
|
||||
**To manipulate an `ArrayBuffer`, we need to use a "view" object.**
|
||||
|
||||
A view object does not store anything on it's own. It's just an "eyeglasses" that give different representation of the bytes.
|
||||
|
||||
For instance:
|
||||
|
||||
- **`Uint8Array`** -- treats each byte as a separate number, with possible values are from 0 to 255 (a byte is 8-bit, so can hold only that much). That's called a "8-bit unsigned integer".
|
||||
- **`Uint16Array`** -- treats every 2 bytes as an integer, with possible values from 0 to 65535. That's called a "16-bit unsigned integer".
|
||||
- **`Uint32Array`** -- treats every 4 bytes as an integer, with possible values from 0 to 4294967295. That's called a "32-bit unsigned integer".
|
||||
- **`Float64Array`** -- treats every 8 bytes as a floating point number with possible values from <code>5.0x10<sup>-324</sup></code> to <code>1.8x10<sup>308</sup></code>.
|
||||
|
||||
So, an `ArrayBuffer` of 16 bytes can be interpreted as 16 "tiny numbers", or 8 bigger numbers, or 4 even bigger, or 2 floating-point values with high precision.
|
||||
|
||||

|
||||
|
||||
`ArrayBuffer` is the core object, the root of everything, the raw binary data. But if we're going to write into it, or iterate over it, basically for almost any operation – we must use a view, e.g:
|
||||
|
||||
```js run
|
||||
let buffer = new ArrayBuffer(16); // create a buffer of length 16
|
||||
|
||||
let view = new Uint32Array(buffer); // treat every 4 bytes as an integer number
|
||||
|
||||
alert(view.length); // 4, it stores that many numbers
|
||||
alert(view.byteLength); // 16, the length in bytes
|
||||
|
||||
// let's write a value
|
||||
view[0] = 123456;
|
||||
|
||||
for(let num of view) {
|
||||
alert(num); // 123456, then 0, 0, 0 (4 numbers total)
|
||||
}
|
||||
```
|
||||
|
||||
These numeric views are called "Typed Arrays".
|
||||
|
||||
## TypedArray
|
||||
|
||||
Here's a full list of typed arrays:
|
||||
|
||||
- `Uint8Array`, `Uint16Array`, `Uint32Array` -- for integer numbers of 8, 16 and 32 bits.
|
||||
- `Uint8ClampedArray` -- for 8-bit integers, "clamps" them on assignment (see below).
|
||||
- `Int8Array`, `Int16Array`, `Int32Array` -- for signed integer numbers (can be negative).
|
||||
- `Float32Array`, `Float64Array` -- for signed floating-point numbers of 32 and 64 bits.
|
||||
|
||||
`Uint8ClampedArray` has a special feature. Usually, when we write a number that doesn't fit, into an integer typed array, then its most significant bytes are cut-off.
|
||||
|
||||
For instance, let's try to write 256 into `Uint8Array`. In binary form, 256 is `100000000` (9 bits), but `Uint8Array` only provides 8 per value.
|
||||
|
||||
So only the rightmost 8 bits are kept, and the rest is cut off:
|
||||
|
||||

|
||||
|
||||
So we get zero. Here's the demo:
|
||||
|
||||
```js run
|
||||
let buffer = new ArrayBuffer(16);
|
||||
let uint8array = new Uint8Array(buffer);
|
||||
|
||||
let num = 256;
|
||||
alert(num.toString(2)); // 100000000 (binary representation)
|
||||
|
||||
uint8array[0] = num;
|
||||
|
||||
alert(uint8array[0]); // 0
|
||||
```
|
||||
|
||||
For 257, the binary form is `100000001` (9 bits), so we'll have `1` in the array:
|
||||
|
||||

|
||||
|
||||
In other words, the number modulo 2<sup>8</sup> is saved.
|
||||
|
||||
For `Uint8ClampedArray` the behavior is different. It saves 255 for any number that is greater than 255, and 0 for any negative number. That behavior is useful in image processing.
|
||||
|
||||
**`TypedArray` methods are similar to regular Javascript arrays, with notable exceptions.**
|
||||
|
||||
We can itereate, `map`, `slice`, `find`, etc.
|
||||
|
||||
There are few things we can't do though:
|
||||
|
||||
- No `splice` -- we can't "delete" a value, because typed arrays are views on a buffer, and these are contiguous areas of memory. All we can do is to assign a zero.
|
||||
- No `concat` method, we can use `set` instead.
|
||||
- The `set(fromArr, offset)` method copies all elements from `fromArr` to the current array, starting at position `offset`.
|
||||
|
||||
To concatenate several typed arrays, create a new one with the combined length and `set` onto it:
|
||||
|
||||
// Step 4: join chunks into result
|
||||
let chunksAll = new Uint8Array(receivedLength); // (4.1)
|
||||
let position = 0;
|
||||
for(let chunk of chunks) {
|
||||
chunksAll.set(chunk, position); // (4.2)
|
||||
position += chunk.length;
|
||||
}
|
||||
|
||||
|
||||
|
||||
The full list of `ArrayBufferViews`:
|
||||
|
||||
- `Int8Array`
|
76
6-binary/02-text-decoder/article.md
Normal file
|
@ -0,0 +1,76 @@
|
|||
# TextDecoder, TextEncoder
|
||||
|
||||
What if the binary data is actually a string?
|
||||
|
||||
The build-in [TextDecoder](https://encoding.spec.whatwg.org/#interface-textdecoder) object allows to read the value into an an actual Javascript string, given the buffer and the encoding.
|
||||
|
||||
We first need to create it:
|
||||
```js
|
||||
let decoder = new TextDecoder([label], [options]);
|
||||
```
|
||||
|
||||
- **`label`** -- the encoding, `utf-8` by default.
|
||||
- **`options`** -- optional object:
|
||||
- **`fatal`** -- boolean, if `true` then throw an exception for invalid string, otherwise (default) replace invalid byte sequences with character `\uFFFD`.
|
||||
- **`ignoreBOM`** -- boolean, if `true` then ignore BOM (an optional byte-order unicode mark), we usually don't set it.
|
||||
|
||||
...And then decode:
|
||||
|
||||
```js
|
||||
let str = decoder.decode([input], [options]);
|
||||
```
|
||||
|
||||
- **`input`** -- `BufferSource` to decode.
|
||||
- **`options`** -- optional object:
|
||||
- **`stream`** -- true for decoding streams, when `decoder` is called repeatedly with incoming chunks of data. In that case a multi-byte character may occasionally split between chunks. This options tells `TextDecoder` to memorize "unfinished" characters and decode them when the next chunk comes.
|
||||
|
||||
For instance:
|
||||
|
||||
```js run
|
||||
let uint8Array = new Uint8Array([72, 101, 108, 108, 111]);
|
||||
|
||||
alert( new TextDecoder().decode(uint8Array) ); // Hello
|
||||
```
|
||||
|
||||
|
||||
```js run
|
||||
let uint8Array = new Uint8Array([228, 189, 160, 229, 165, 189]);
|
||||
|
||||
alert( new TextDecoder().decode(uint8Array) ); // 你好
|
||||
```
|
||||
|
||||
We can decode a part of the buffer by passing it a subarray:
|
||||
|
||||
|
||||
```js run
|
||||
let uint8Array = new Uint8Array([0, 72, 101, 108, 108, 111, 0]);
|
||||
|
||||
// the string is in the middle
|
||||
// create a new view over it, without copying anything
|
||||
let binaryString = uint8Array.subarray(1, -1);
|
||||
|
||||
alert( new TextDecoder().decode(binaryString) ); // Hello
|
||||
```
|
||||
|
||||
## TextEncoder
|
||||
|
||||
[TextEncoder](https://encoding.spec.whatwg.org/#interface-textencoder) does the reverse thing -- writes a string into a buffer.
|
||||
|
||||
The syntax is:
|
||||
|
||||
```js run
|
||||
let encoder = new TextEncoder();
|
||||
```
|
||||
|
||||
The only encoding it supports is "utf-8".
|
||||
|
||||
It has two methods:
|
||||
- **`encode(str)`** -- returns `Uint8Array` from a string.
|
||||
- **`encodeInto(str, destination)`** -- encodes `str` into `destination` that must be `Uint8Array`.
|
||||
|
||||
```js run
|
||||
let encoder = new TextEncoder();
|
||||
|
||||
let uint8Array = encoder.encode("Hello");
|
||||
alert(uint8Array); // 72,101,108,108,111
|
||||
```
|
212
6-binary/03-blob/article.md
Normal file
|
@ -0,0 +1,212 @@
|
|||
# Blob
|
||||
|
||||
`ArrayBuffer` and views are a part of ECMA standard, a part of Javascript.
|
||||
|
||||
In the browser, there are additional higher-level objects, described in [File API](https://www.w3.org/TR/FileAPI/).
|
||||
|
||||
`Blob` consists of an optional string `type` (a MIME-type usually), plus `blobParts` this a sequence of other `Blob` objects, strings and `BufferSources`.
|
||||
|
||||

|
||||
|
||||
Thanks to `type`, we can download/upload blobs, and it naturally becomes `Content-Type` in network requests.
|
||||
|
||||
The constructor syntax is:
|
||||
|
||||
```js
|
||||
new Blob(blobParts, options);
|
||||
```
|
||||
|
||||
- **`blobParts`** is an array of Blob/BufferSource/String values.
|
||||
- **`options`** optional object:
|
||||
- **`type`** -- blob type, usually MIME-type, e.g. `image/png`,
|
||||
- **`endings`** -- whether to transform end-of-line `\r\n` to `\n` end vise versa, to make the blob correspond to current OS newlines. By default `"transparent"` (do nothing), but also can be `"native"` (transform).
|
||||
|
||||
Then we can access blob as a whole, or extract slices with:
|
||||
|
||||
```js
|
||||
blob.slice([byteStart], [byteEnd], [contentType]);
|
||||
```
|
||||
|
||||
- **`byteStart`** -- the starting byte, by default 0.
|
||||
- **`byteEnd`** -- the last byte (exclusive, by default till the end).
|
||||
- **`contentType`** -- the `type` of the new blob, by default the same as the source.
|
||||
|
||||
The arguments are similar to `array.slice`, negative numbers are allowed too.
|
||||
|
||||
We can't change data directly in a blob, but we can copy parts of blobs, create new blobs from them, mix them and so on. This behavior is similar to Javascript strings: we can't change a character in a string, but we can make a new corrected string.
|
||||
|
||||
## Blob as URL
|
||||
|
||||
A Blob can be easily used in an URL for `<a>`, `<img>` or other tags.
|
||||
|
||||
Let's start with a simple example. By clicking on a link you download a dynamically-generated blob with `hello world` contents as a file:
|
||||
|
||||
```html run
|
||||
<!-- download attribute forces the browser to download instead of navigating -->
|
||||
<a download="hello.txt" href='#' id="link">Download</a>
|
||||
|
||||
<script>
|
||||
let binary = new Uint8Array([72, 101, 108, 108, 111]); // "hello" in binary form
|
||||
|
||||
// create blob of multiple parts
|
||||
let blob = new Blob([binary, ' ', 'world'], {type: 'text/plain'});
|
||||
|
||||
link.href = URL.createObjectURL(blob);
|
||||
</script>
|
||||
```
|
||||
|
||||
We can also create a link in Javascript and simulate a click by `link.click()`, then download starts authomatically.
|
||||
|
||||
Here's the similar "on the fly" blob creation and download code, now without HTML:
|
||||
|
||||
```js run
|
||||
let link = document.createElement('a');
|
||||
link.download = 'hello.txt';
|
||||
|
||||
let blob = new Blob(['Hello, world!'], {type: 'text/plain'});
|
||||
|
||||
link.href = URL.createObjectURL(blob);
|
||||
|
||||
link.click();
|
||||
|
||||
URL.revokeObjectURL(link.href);
|
||||
```
|
||||
|
||||
**`URL.createObjectURL` takes a blob and creates an unique URL for it, in the form `blob:<origin>/<uuid>`.**
|
||||
|
||||
That's what the generated url looks like:
|
||||
|
||||
```
|
||||
blob:https://javascript.info/1e67e00e-860d-40a5-89ae-6ab0cbee6273
|
||||
```
|
||||
|
||||
The browser keeps an internal url -> blob mapping. For each url generated by `URL.createObjectURL`, it stores a reference to the corresponding blob.
|
||||
|
||||
A generated url is only valid while the current document is open. And it allows to reference the blob in `<img>`, `<a>`, any other object that expects an URL.
|
||||
|
||||
There's a side-effect though. While there's an mapping for a blob, the blob itself resides in the memory. The browser can't free it.
|
||||
|
||||
The mapping is automatically cleared on document unload, so blobs are freed then. But if an app is long-living, then that doesn't happen. So if we create an URL, that blob will hang in memory, even if not needed any more.
|
||||
|
||||
**`URL.revokeObjectURL(url)` removes the reference from the internal mapping, thus allowing the blob to be deleted (if there are no other references), and the memory to be freed.**
|
||||
|
||||
In the last example, we intend the blob to be used only once, for instant downloading, so we call `URL.revokeObjectURL(link.href)` immediately.
|
||||
|
||||
In the previous example though, with the clickable HTML-link, we dont't call `URL.revokeObjectURL(link.href)`, because that would make the blob url invalid. After the revocation, as the mapping is removed, the url doesn't work any more.
|
||||
|
||||
## Blob to base64
|
||||
|
||||
An alternative to `URL.createObjectURL` is to convert a blob into a base64-encoded string.
|
||||
|
||||
That encoding represents binary data as a string of ultra-safe "readable" characters with ASCII-codes from 0 to 64. And what's more important -- we can use this encoding in "data-urls".
|
||||
|
||||
A [data url](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs) has the form `data:[<mediatype>][;base64],<data>`. We can use such urls everywhere, on par with "regular" urls.
|
||||
|
||||
For instance, here's a smiley:
|
||||
|
||||
```html
|
||||
<img src="data:image/png;base64,R0lGODlhDAAMAKIFAF5LAP/zxAAAANyuAP/gaP///wAAAAAAACH5BAEAAAUALAAAAAAMAAwAAAMlWLPcGjDKFYi9lxKBOaGcF35DhWHamZUW0K4mAbiwWtuf0uxFAgA7">
|
||||
```
|
||||
|
||||
The browser will decode the string and show the image: <img src="data:image/png;base64,R0lGODlhDAAMAKIFAF5LAP/zxAAAANyuAP/gaP///wAAAAAAACH5BAEAAAUALAAAAAAMAAwAAAMlWLPcGjDKFYi9lxKBOaGcF35DhWHamZUW0K4mAbiwWtuf0uxFAgA7">
|
||||
|
||||
|
||||
To transform a blob into base64 and vise-versa, we'll use built-in `FileReader/FileWriter` objects. In the [next chapter](info:file) we'll cover those more in-detail. For now, it's important only that they can read a blob as data url, that we can assign it to `link.href`.
|
||||
|
||||
Here's the demo of downloading a blob, now via base-64:
|
||||
|
||||
```js run
|
||||
let link = document.createElement('a');
|
||||
link.download = 'hello.txt';
|
||||
|
||||
let blob = new Blob(['Hello, world!'], {type: 'text/plain'});
|
||||
|
||||
*!*
|
||||
let reader = new FileReader();
|
||||
*/!*
|
||||
|
||||
reader.onload = function() {
|
||||
link.href = reader.result; // data url
|
||||
link.click();
|
||||
};
|
||||
|
||||
*!*
|
||||
reader.readAsDataURL(blob); // converts the blob to base64 and calls onload
|
||||
*/!*
|
||||
```
|
||||
|
||||
Both ways of making an URL of a blob are usable. But usually `URL.createObjectURL(blob)` is simpler and faster.
|
||||
|
||||
```compare title-plus="URL.createObjectURL(blob)" title-minus="Blob to data url"
|
||||
+ We need to revoke them if care about memory.
|
||||
+ Direct access to blob, no "encoding/decoding"
|
||||
- No need to revoke anything.
|
||||
- Performance and memory losses on big blobs for encoding.
|
||||
```
|
||||
|
||||
## Image or a page to blob
|
||||
|
||||
We can create a blob of an image, or even a page screenshot.
|
||||
|
||||
That's usually done via `<canvas>` element:
|
||||
|
||||
1. We draw an existing image on canvas using [canvas.drawImage](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/drawImage).
|
||||
2. Canvas [.toBlob(callback, format, quality)](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob) creates a blob and calls `callback` with it.
|
||||
|
||||
In the example below, an image is just copied, but we could cut from it, or transform it on canvas prior to making a blob:
|
||||
|
||||
```js run
|
||||
let img = document.querySelector('img'); // just any image
|
||||
|
||||
let canvas = document.createElement('canvas');
|
||||
canvas.width = img.clientWidth;
|
||||
canvas.height = img.clientHeight;
|
||||
|
||||
let context = canvas.getContext('2d');
|
||||
|
||||
context.drawImage(img, 0, 0); // allows to cut image
|
||||
// we can context.rotate() as well, and do many other things
|
||||
|
||||
canvas.toBlob(function(blob) {
|
||||
// download the blob
|
||||
let link = document.createElement('a');
|
||||
link.download = 'example.png';
|
||||
link.href = URL.createObjectURL(blob);
|
||||
link.click();
|
||||
|
||||
URL.revokeObjectURL(link.href);
|
||||
}, 'image/png');
|
||||
```
|
||||
|
||||
For screenshotting a page, we can use a library such as <https://github.com/niklasvh/html2canvas> to draw the screenshot on `<canvas>`, and then download it the same way.
|
||||
|
||||
## From Blob to ArrayBuffer
|
||||
|
||||
The `Blob` constructor allows to create a blob from almost anything.
|
||||
|
||||
But we can also revert back to lowest-level `ArrayBuffer` using `FileReader`:
|
||||
|
||||
```js
|
||||
// get arrayBuffer from blob
|
||||
let fileReader = new FileReader();
|
||||
fileReader.onload = function(event) {
|
||||
let arrayBuffer = fileReader.result;
|
||||
};
|
||||
*!*
|
||||
fileReader.readAsArrayBuffer(blob);
|
||||
*/!*
|
||||
```
|
||||
|
||||
|
||||
## Summary
|
||||
|
||||
While `ArrayBuffer`, `Uint8Array` and other `BufferSource` are "binary data", a [Blob](https://www.w3.org/TR/FileAPI/#dfn-Blob) represents "binary data with type".
|
||||
|
||||
That makes Blobs convenient for upload/download operations, that are so common in the browser.
|
||||
|
||||
Methods that perform web-requests, such as [XMLHttpRequest](info:xmlhttprequest), [fetch](info:fetch) and so on, can work with `Blob` natively, as well as with other binary types.
|
||||
|
||||
That said, it's still possible to convert betweeen `Blob` and low-level binary data types:
|
||||
|
||||
- We can make a Blob from a typed array using `new Blob(...)` constructor.
|
||||
- We can get back `ArrayBuffer` from a Blob using `FileReader`, and then create a view over it for low-level binary processing.
|
BIN
6-binary/03-blob/blob.png
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
6-binary/03-blob/blob@2x.png
Normal file
After Width: | Height: | Size: 23 KiB |
106
6-binary/04-file/article.md
Normal file
|
@ -0,0 +1,106 @@
|
|||
# File and FileReader
|
||||
|
||||
A [File](https://www.w3.org/TR/FileAPI/#dfn-file) object inhereits from `Blob`, but is extended with filesystem-related capabilities.
|
||||
|
||||
There are two ways to obtain it.
|
||||
|
||||
First, there's a constructor, similar to `Blob`:
|
||||
|
||||
```js
|
||||
new File(fileParts, fileName, [options])
|
||||
```
|
||||
|
||||
- **`fileParts`** -- is an array of Blob/BufferSource/String value, same as `Blob`.
|
||||
- **`fileName`** -- file name string.
|
||||
- **`options`** -- optional object:
|
||||
- **`lastModified`** -- a timestamp (integer date) of last modification.
|
||||
|
||||
Second, more often we get a file from `<input type="file">` or drag'n'drop or other browser interfaces. Then the file gets these from OS.
|
||||
|
||||
For instance:
|
||||
|
||||
```html run
|
||||
<input type="file" onchange="showFile(this)">
|
||||
|
||||
<script>
|
||||
function showFile(input) {
|
||||
let file = input.files[0];
|
||||
|
||||
alert(`File name: ${file.name}`); // e.g my.png
|
||||
alert(`Last modified: ${file.lastModified}`); // e.g 1552830408824
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
```smart
|
||||
Please note: the input may select multiple files, so `input.files` is an array-like object with them. Here we have only one file, but it's still the same, so we just take `input.files[0]`.
|
||||
```
|
||||
|
||||
## FileReader
|
||||
|
||||
[FileReader](https://www.w3.org/TR/FileAPI/#dfn-filereader) is an object with the sole purpose of reading from `Blob` (and hence `File` too) objects.
|
||||
|
||||
It's event based, as reading from disk may take time.
|
||||
|
||||
The constructor:
|
||||
|
||||
```js
|
||||
let reader = new FileReader(); // no arguments
|
||||
```
|
||||
|
||||
The main methods:
|
||||
|
||||
- **`readAsArrayBuffer(blob)`** -- read the data as `ArrayBuffer`
|
||||
- **`readAsText(blob, [encoding])`** -- read the data as a string (encoding is `utf-8` by default)
|
||||
- **`readAsDataURL(blob)`** -- encode the data as base64 data url.
|
||||
- **`abort()`** -- cancel the operation.
|
||||
|
||||
As the reading proceeds, there are events:
|
||||
- `loadstart` -- loading started.
|
||||
- `progress` -- occurs during reading.
|
||||
- `load` -- no errors, reading complete.
|
||||
- `abort` -- `abort()` called.
|
||||
- `error` -- error has occured.
|
||||
- `loadend` -- reading finished with either success or failure.
|
||||
|
||||
At the end:
|
||||
- `reader.result` is the result (if successful)
|
||||
- `reader.error` is the error (if failed).
|
||||
|
||||
The most widely used events are for sure `load` and `error`.
|
||||
|
||||
Here's an example:
|
||||
|
||||
```html run
|
||||
<input type="file" onchange="readFile(this)">
|
||||
|
||||
<script>
|
||||
function readFile(input) {
|
||||
let file = input.files[0];
|
||||
|
||||
let reader = new FileReader();
|
||||
|
||||
reader.onload = function() {
|
||||
console.log(reader.result);
|
||||
};
|
||||
|
||||
reader.onerror = function() {
|
||||
console.log(reader.error);
|
||||
};
|
||||
|
||||
reader.readAsText(file);
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
|
||||
```smart header="`FileReaderSync` is available for workers only"
|
||||
For Web Workers, there also exists a synchronous variant of `FileReader`, called [FileReaderSync](https://www.w3.org/TR/FileAPI/#FileReaderSync).
|
||||
|
||||
Reading methods `read*` do not generate events, but rather return the result, as regular functions do.
|
||||
|
||||
That's only inside a Web Worker though, because delays and hang-ups in Web Workers are less important, they do not affect the page.
|
||||
```
|
||||
|
||||
|
||||
It most often used to read from files, and
|
BIN
6-binary/04-file/blob.png
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
6-binary/04-file/blob@2x.png
Normal file
After Width: | Height: | Size: 23 KiB |
|
@ -60,7 +60,7 @@ To do the request, we need 3 steps:
|
|||
|
||||
```js
|
||||
xhr.onload = function() {
|
||||
alert(`Loaded: ${xhr.status} ${xhr.responseText}`);
|
||||
alert(`Loaded: ${xhr.status} ${xhr.response}`);
|
||||
};
|
||||
|
||||
xhr.onerror = function() { // only triggers if the request couldn't be made at all
|
||||
|
@ -69,7 +69,8 @@ To do the request, we need 3 steps:
|
|||
|
||||
xhr.onprogress = function(event) { // triggers periodically
|
||||
// event.loaded - how many bytes downloaded
|
||||
// event.total - total number of bytes (if server set Content-Length header)
|
||||
// event.lengthComputable = true if the server sent Content-Length header
|
||||
// event.total - total number of bytes (if lengthComputable)
|
||||
alert(`Received ${event.loaded} of ${event.total}`);
|
||||
};
|
||||
```
|
||||
|
@ -91,13 +92,17 @@ xhr.onload = function() {
|
|||
if (xhr.status != 200) { // analyze HTTP status of the response
|
||||
alert(`Error ${xhr.status}: ${xhr.statusText}`); // e.g. 404: Not Found
|
||||
} else { // show the result
|
||||
alert(`Done, got ${xhr.responseText.length} bytes`); // responseText is the server
|
||||
alert(`Done, got ${xhr.response.length} bytes`); // responseText is the server
|
||||
}
|
||||
};
|
||||
|
||||
xhr.onprogress = function(event) {
|
||||
// event.total=0 if the server did not send Content-Length header
|
||||
alert(`Received ${event.loaded} of ${event.total}`);
|
||||
if (event.lengthComputable) {
|
||||
alert(`Received ${event.loaded} of ${event.total} bytes`);
|
||||
} else {
|
||||
alert(`Received ${event.loaded} bytes`); // no Content-Length
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
xhr.onerror = function() {
|
||||
|
@ -113,10 +118,8 @@ Once the server has responded, we can receive the result in the following proper
|
|||
`statusText`
|
||||
: HTTP status message (a string): usually `OK` for `200`, `Not Found` for `404`, `Forbidden` for `403` and so on.
|
||||
|
||||
`responseText`
|
||||
: The text of the server response, if JSON, then we can `JSON.parse` it.s
|
||||
|
||||
If the server returns XML with the correct header `Content-type: text/xml`, then there's also `responseXML` property with the parsed XML document. We can query it with `xhr.responseXml.querySelector("...")` and perform other XML-specific operations.
|
||||
`response` (old scripts may use `responseText`)
|
||||
: The server response.
|
||||
|
||||
If we changed our mind, we can terminate the request at any time. The call to `xhr.abort()` does that:
|
||||
|
||||
|
@ -134,8 +137,46 @@ xhr.timeout = 10000; // timeout in ms, 10 seconds
|
|||
|
||||
If the request does not succeed within the given time, it gets canceled and `timeout` event triggers.
|
||||
|
||||
## Response Type
|
||||
|
||||
### Ready states
|
||||
We can use `xhr.responseType` property to set the response format:
|
||||
|
||||
- `""` (default) -- get as string,
|
||||
- `"text"` -- get as string,
|
||||
- `"arraybuffer"` -- get as `ArrayBuffer` (for binary data, see chapter <info:arraybuffer-and-views>),
|
||||
- `"blob"` -- get as `Blob` (for binary data, see chapter <info:blob>),
|
||||
- `"document"` -- get as XML document (can use XPath and other XML methods),
|
||||
- `"json"` -- get as JSON (parsed automatically).
|
||||
|
||||
For example, let's get the response as JSON:
|
||||
|
||||
```js run
|
||||
let xhr = new XMLHttpRequest();
|
||||
|
||||
xhr.open('GET', '/article/xmlhttprequest/example/json');
|
||||
|
||||
*!*
|
||||
xhr.responseType = 'json';
|
||||
*/!*
|
||||
|
||||
xhr.send();
|
||||
|
||||
// the response is {"message": "Hello, world!"}
|
||||
xhr.onload = function() {
|
||||
let responseObj = xhr.response;
|
||||
alert(responseObj.message); // Hello, world!
|
||||
};
|
||||
```
|
||||
|
||||
```smart
|
||||
In the old scripts you may also find `xhr.responseText` and even `xhr.responseXML` properties.
|
||||
|
||||
They exist for historical reasons, to get either a string or XML document. Nowadays, we should set the format in `xhr.responseType` and get `xhr.response` as demonstrated above.
|
||||
```
|
||||
|
||||
|
||||
|
||||
## Ready states
|
||||
|
||||
`XMLHttpRequest` changes between states as it progresses. The current state is accessible as `xhr.readyState`.
|
||||
|
||||
|
@ -186,7 +227,7 @@ try {
|
|||
if (xhr.status != 200) {
|
||||
alert(`Error ${xhr.status}: ${xhr.statusText}`);
|
||||
} else {
|
||||
alert(xhr.responseText);
|
||||
alert(xhr.response);
|
||||
}
|
||||
} catch(err) { // instead of onerror
|
||||
alert("Request failed");
|
||||
|
@ -275,7 +316,7 @@ There are 3 methods for HTTP-headers:
|
|||
}, {});
|
||||
```
|
||||
|
||||
## POST requests
|
||||
## POST, FormData
|
||||
|
||||
To make a POST request, we can use the built-in [FormData](https://developer.mozilla.org/en-US/docs/Web/API/FormData) object.
|
||||
|
||||
|
@ -293,7 +334,7 @@ Create it, optionally from a form, `append` more fields if needed, and then:
|
|||
|
||||
For instance:
|
||||
|
||||
```html
|
||||
```html run
|
||||
<form name="person">
|
||||
<input name="name" value="John">
|
||||
<input name="surname" value="Smith">
|
||||
|
@ -308,8 +349,10 @@ For instance:
|
|||
|
||||
// send it out
|
||||
let xhr = new XMLHttpRequest();
|
||||
xhr.open("POST", "/url");
|
||||
xhr.open("POST", "/article/xmlhttprequest/post/user");
|
||||
xhr.send(formData);
|
||||
|
||||
xhr.
|
||||
</script>
|
||||
```
|
||||
|
||||
|
@ -333,7 +376,7 @@ xhr.setRequestHeader('Content-type', 'application/json; charset=utf-8');
|
|||
xhr.send(json);
|
||||
```
|
||||
|
||||
`XMLHttpRequest` can also send binary data using Blob and similar objects. We'll cover binary data later, in the chapter about `fetch`.
|
||||
The `.send(body)` method is pretty omnivore. It can send almost everything, including Blob and BufferSource objects.
|
||||
|
||||
## Tracking upload progress
|
||||
|
||||
|
@ -371,6 +414,7 @@ xhr.upload.onerror = function() {
|
|||
};
|
||||
```
|
||||
|
||||
|
||||
## Summary
|
||||
|
||||
Typical code of the GET-request with `XMLHttpRequest`:
|
||||
|
@ -389,7 +433,7 @@ xhr.onload = function() {
|
|||
return;
|
||||
}
|
||||
|
||||
// get the response from xhr.responseText
|
||||
// get the response from xhr.response
|
||||
};
|
||||
|
||||
xhr.onprogress = function(event) {
|
||||
|
|
|
@ -28,6 +28,14 @@ function accept(req, res) {
|
|||
}
|
||||
|
||||
}
|
||||
} else if (req.url == '/json') {
|
||||
res.writeHead(200, {
|
||||
// 'Content-Type': 'application/json;charset=utf-8',
|
||||
'Cache-Control': 'no-cache'
|
||||
});
|
||||
|
||||
res.write(JSON.stringify({message: "Hello, world!"}));
|
||||
res.end();
|
||||
} else {
|
||||
file.serve(req, res);
|
||||
}
|
||||
|
|
36
7-network/10-xmlhttprequest/post.view/index.html
Normal file
|
@ -0,0 +1,36 @@
|
|||
<!doctype html>
|
||||
<script>
|
||||
(async () {
|
||||
|
||||
const response = await fetch('long.txt');
|
||||
const reader = response.body.getReader();
|
||||
|
||||
const contentLength = +response.headers.get('Content-Length');
|
||||
let receivedLength = 0;
|
||||
let chunks = [];
|
||||
while(true) {
|
||||
const chunk = await reader.read();
|
||||
|
||||
if (chunk.done) {
|
||||
console.log("done!");
|
||||
break;
|
||||
}
|
||||
|
||||
chunks.push(chunk.value);
|
||||
receivedLength += chunk.value.length;
|
||||
console.log(`${receivedLength}/${contentLength} received`)
|
||||
}
|
||||
|
||||
|
||||
let chunksMerged = new Uint8Array(receivedLength);
|
||||
let length = 0;
|
||||
for(let chunk of chunks) {
|
||||
chunksMerged.set(chunk, length);
|
||||
length += chunk.length;
|
||||
}
|
||||
|
||||
let result = new TextDecoder("utf-8").decode(chunksMerged);
|
||||
console.log(result);
|
||||
})();
|
||||
|
||||
</script>
|
55
7-network/10-xmlhttprequest/post.view/server.js
Normal file
|
@ -0,0 +1,55 @@
|
|||
let http = require('http');
|
||||
let url = require('url');
|
||||
let querystring = require('querystring');
|
||||
let static = require('node-static');
|
||||
let file = new static.Server('.', {
|
||||
cache: 0
|
||||
});
|
||||
|
||||
|
||||
function accept(req, res) {
|
||||
|
||||
if (req.method == 'POST') {
|
||||
let chunks = [];
|
||||
let length = 0;
|
||||
|
||||
req.on('data', function (data) {
|
||||
chunks.push(data);
|
||||
length += data.length;
|
||||
|
||||
// Too much POST data, kill the connection!
|
||||
if (length > 1e6) {
|
||||
request.connection.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
req.on('end', function() {
|
||||
// let post = JSON.parse(chunks.join(''));
|
||||
|
||||
if (req.url == '/user') {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ message: 'User saved' }));
|
||||
} else if (req.url == '/image') {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ message: "Image saved", imageSize: length }));
|
||||
} else {
|
||||
res.writeHead(404);
|
||||
res.end("Not found");
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
} else {
|
||||
file.serve(req, res);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
// ------ запустить сервер -------
|
||||
|
||||
if (!module.parent) {
|
||||
http.createServer(accept).listen(8080);
|
||||
} else {
|
||||
exports.accept = accept;
|
||||
}
|