From Philip

3. DOM optimization (events delegation, batches, doc fragment, throttle, debounce, unsubscribe, remove DOM refs in variables)

events delegation

Всплытие и перехват событий позволяет реализовать один из самых важных приёмов разработки – делегирование.

Идея в том, что если у нас есть много элементов, события на которых нужно обрабатывать похожим образом, то вместо того, чтобы назначать обработчик каждому, мы ставим один обработчик на их общего предка.

Из него можно получить целевой элемент event.target, понять на каком именно потомке произошло событие и обработать его.

table.onclick = function(event) {
  let td = event.target.closest('td'); // (1)

  if (!td) return; // (2)

  if (!table.contains(td)) return; // (3)

  highlight(td); // (4)
};

Разберём пример:

  1. Метод elem.closest(selector) возвращает ближайшего предка, соответствующего селектору. В данном случае нам нужен <td>, находящийся выше по дереву от исходного элемента.

  2. Если event.target не содержится внутри элемента <td>, то вызов вернёт null, и ничего не произойдёт.

  3. Если таблицы вложенные, event.target может содержать элемент <td>, находящийся вне текущей таблицы. В таких случаях мы должны проверить, действительно ли это <td> нашей таблицы.

  4. И если это так, то подсвечиваем его.

В итоге мы получили короткий код подсветки, быстрый и эффективный, которому совершенно не важно, сколько всего в таблице <td>.

doc fragment

documentFragment - "фрагмент документа" наиболее близок по смыслу к обычному DOM-элементу.

Вставить пачку узлов единовременно поможет DocumentFragment. Это особенный кросс-браузерный DOM-объект, который похож на обычный DOM-узел, но им не является.

Синтаксис для его создания:

var fragment = document.createDocumentFragment();

В него можно добавлять другие узлы.

fragment.appendChild(node);

Его можно клонировать:

fragment.cloneNode(true); // клонирование с подэлементами

У DocumentFragment нет обычных свойств DOM-узлов, таких как innerHTML, tagName и т.п. Это не узел.

Его «Фишка» заключается в том, что когда DocumentFragment вставляется в DOM – то он исчезает, а вместо него вставляются его дети. Это свойство является уникальной особенностью DocumentFragment.

Например, если добавить в него много LI, и потом вызвать ul.appendChild(fragment), то фрагмент растворится, и в DOM вставятся именно LI, причём в том же порядке, в котором были во фрагменте.

Псевдокод:

// хотим вставить в список UL много LI

// делаем вспомогательный DocumentFragment
var fragment = document.createDocumentFragment();

for (цикл по li) {
  fragment.appendChild(list[i]); // вставить каждый LI в DocumentFragment
}

ul.appendChild(fragment); // вместо фрагмента вставятся элементы списка\

Особенность: фрагмент при использовании «портится». Точнее — опустошается. Когда мы делаем div.appendChild(fragment), все дочерние элементы фрагмента переносятся в div. А поскольку элемент не может иметь более одного родителя, это означает, что они из фрагмента изымаются! Чтобы избежать этого поведения в случае, когда оно нежелательно, можно использовать cloneNode.

throttle and debounce

Троттлинг функции означает, что функция вызывается не более одного раза в указанный период времени (например, раз в 10 секунд). Другими словами ― троттлинг предотвращает запуск функции, если она уже запускалась недавно. Троттлинг также обеспечивает регулярность выполнение функции с заданной периодичностью.

Debouncing функции означает, что все вызовы будут игнорироваться до тех пор, пока они не прекратятся на определённый период времени. Только после этого функция будет вызвана. Например, если мы установим таймер на 2 секунды, а функция вызывается 10 раз с интервалом в одну секунду, то фактический вызов произойдёт только спустя 2 секунды после крайнего (десятого) обращения к функции.

Применение: видео игры(троттлинг для выстрела в 1 сек один раз), автозаполнение(если бы не debounce АPI дергался бы после каждой введенной буквы)

4. event.stopPropagation() vs event.preventDefault()

event.stopPropagation()

Всплытие идёт с «целевого» элемента прямо наверх. По умолчанию событие будет всплывать до элемента <html>, а затем до объекта document, а иногда даже до window, вызывая все обработчики на своём пути.

Но любой промежуточный обработчик может решить, что событие полностью обработано, и остановить всплытие.

Для этого нужно вызвать метод event.stopPropagation().

Если у элемента есть несколько обработчиков на одно событие, то даже при прекращении всплытия все они будут выполнены.

То есть, event.stopPropagation() препятствует продвижению события дальше, но на текущем элементе все обработчики будут вызваны.

Для того, чтобы полностью остановить обработку, существует метод event.stopImmediatePropagation(). Он не только предотвращает всплытие, но и останавливает обработку событий на текущем элементе.

event.preventDefault()

Действия браузера по умолчанию

Многие события автоматически влекут за собой действие браузера.

Например:

  • Клик по ссылке инициирует переход на новый URL.

  • Нажатие на кнопку «отправить» в форме – отсылку её на сервер.

  • Зажатие кнопки мыши над текстом и её движение в таком состоянии – инициирует его выделение.

Если мы обрабатываем событие в JavaScript, то зачастую такое действие браузера нам не нужно. К счастью, его можно отменить.

Есть два способа отменить действие браузера:

  • Основной способ – это воспользоваться объектом event. Для отмены действия браузера существует стандартный метод event.preventDefault().

  • Если же обработчик назначен через on<событие> (не через addEventListener), то также можно вернуть false из обработчика.

В следующем примере при клике по ссылке переход не произойдёт:

<a href="/" onclick="return false">Нажми здесь</a>
или
<a href="/" onclick="event.preventDefault()">здесь</a>

5. Всплытие/погружение/перехват события

Стандарт DOM Events описывает 3 фазы прохода события:

  1. Фаза погружения (capturing phase) – событие сначала идёт сверху вниз.

  2. Фаза цели (target phase) – событие достигло целевого(исходного) элемента.

  3. Фаза всплытия (bubbling stage) – событие начинает всплывать.

При наступлении события – самый глубоко вложенный элемент, на котором оно произошло, помечается как «целевой» (event.target).

  • Затем событие сначала двигается вниз от корня документа к event.target, по пути вызывая обработчики, поставленные через addEventListener(...., true), где true – это сокращение для {capture: true}.

  • Далее обработчики вызываются на целевом элементе.

  • Далее событие двигается от event.target вверх к корню документа, по пути вызывая обработчики, поставленные через on<event> и addEventListener без третьего аргумента или с третьим аргументом равным false.

Каждый обработчик имеет доступ к свойствам события event:

  • event.target – самый глубокий элемент, на котором произошло событие.

  • event.currentTarget (=this) – элемент, на котором в данный момент сработал обработчик (тот, на котором «висит» конкретный обработчик)

  • event.eventPhase – на какой фазе он сработал (погружение=1, фаза цели=2, всплытие=3).

Любой обработчик может остановить событие вызовом event.stopPropagation(), но делать это не рекомендуется, так как в дальнейшем это событие может понадобиться, иногда для самых неожиданных вещей.

Итак, при клике на <td> событие путешествует по цепочке родителей сначала вниз к элементу (погружается), затем оно достигает целевой элемент (фаза цели), а потом идёт наверх (всплытие), вызывая по пути обработчики.

Всплытие идёт прямо наверх. Обычно событие будет всплывать наверх и наверх, до элемента <html>, а затем до document, а иногда даже до window, вызывая все обработчики на своём пути.

Но любой промежуточный обработчик может решить, что событие полностью обработано, и остановить всплытие.

Для остановки всплытия нужно вызвать метод event.stopPropagation().

Чаще всего речь идет только о всплытии, потому что другие стадии, как правило, не используются и проходят незаметно для нас.

Обработчики, добавленные через on<event>-свойство или через HTML-атрибуты, или через addEventListener(event, handler) с двумя аргументами, ничего не знают о фазе погружения, а работают только на 2-ой и 3-ей фазах.

Чтобы поймать событие на стадии погружения, нужно использовать третий аргумент capture вот так:

elem.addEventListener(..., {capture: true})
// или просто "true", как сокращение для {capture: true}
elem.addEventListener(..., true)

Существуют два варианта значений опции capture:

  • Если аргумент false (по умолчанию), то событие будет поймано при всплытии.

  • Если аргумент true, то событие будет перехвачено при погружении.

Обратите внимание, что хоть и формально существует 3 фазы, 2-ую фазу («фазу цели»: событие достигло элемента) нельзя обработать отдельно, при её достижении вызываются все обработчики: и на всплытие, и на погружение.

6. Перемещение/навигация по DOM

OR

node.parentNode; // родительский узел 
node.previousSibling; // предыдущий, на этом же уровне, узел 
node.nextSibling; // следующий, на этом же уровне, узел 
node.firstChild; // первый дочерний элемент 
node.lastChild; // последний дочерний элемент

JS:

1. Modules types in JS

Модули — это паттерн, который в разных формах и на разных языках используется разработчиками с 60-х и 70-х годов.

В идеале, модули JavaScript позволяют нам:

  • абстрагировать код, передавая функциональные возможности сторонним библиотекам, так что нам не придётся разбираться во всех сложностях их реализации;

  • инкапсулировать код, скрывая его внутри модуля, если не хотим, чтобы его изменяли;

  • переиспользовать код, избавляясь от необходимости писать одно и то же снова и снова;

  • управлять зависимостями, легко изменяя зависимости без необходимости переписывать наш код.

Форматы модулей

Формат модуля — это синтаксис, который используется для его определения.

До создания ECMAScript 6, или ES2015, в JavaScript не было официального синтаксиса для определения модулей. А значит, опытные разработчики предлагали разные форматы определения.

Вот несколько наиболее известных и широко используемых:

  • асинхронное определение модуля (Asynchronous Module Definition или AMD);

  • CommonJS;

  • универсальное определение модуля (Universal Module Definition или UMD);

  • System.register;

  • формат модуля ES6.

Давайте рассмотрим каждый из них, чтобы вы смогли распознать их по синтаксису.

Асинхронное определение модуля (AMD)

Формат AMD используется в браузерах и применяет для определения модулей функцию define:

//Вызов функции define с массивом зависимостей и фабричной функцией
define(['dep1', 'dep2'], function (dep1, dep2) {

    //Определение модуля с помощью возвращаемого значения
    return function () {};
});

Формат CommonJS

Формат CommonJS применяется в Node.js и использует для определения зависимостей и модулей require и module.exports:

var dep1 = require('./dep1');  
var dep2 = require('./dep2');

module.exports = function(){  
 // ...
}

Универсальное определение модуля (UMD)

Формат UMD может быть использован как в браузере, так и в Node.js.

(function (root, factory) {
 if (typeof define === 'function' && define.amd) {
   // AMD. Подключение анонимного модуля
     define(['b'], factory);
 } else if (typeof module === 'object' && module.exports) {
   // Node. Не работает с CommonJS напрямую, 
   // только CommonJS-образными средами, которые поддерживают      

   // module.exports, как Node.
   module.exports = factory(require('b'));
 } else {
   // Глобальные переменные браузера (root это window)
   root.returnExports = factory(root.b);
 }
}(this, function (b) {
 //как-нибудь использовать b.

 // Просто возвращаем значение для определения модуля.
 // Этот пример возвращает объект, но модуль 
 // может вернуть и функцию как экспортируемое значение.
 return {};
}));

System.registerА

Формат System.register был разработан для поддержки синтаксиса модулей ES6 в ES5:

import { p as q } from './dep';

var s = 'local';

export function func() {  
 return q;
}

export class C {  
}

Формат модулей ES6

В ES6 JavaScript уже поддерживает нативный формат модулей.

Он использует токен export для экспорта публичного API модуля:

// lib.js

// Экспорт функции
export function sayHello(){  
 console.log('Hello');
}

// Не экспоруемая функция
function somePrivateFunction(){  
 // ...
}

и токен import для импорта частей, которые модуль экспортирует:

import { sayHello } from './lib';

sayHello();  
// Hello

Мы можем даже присваивать импорту алиас, используя as:

import { sayHello as say } from './lib';

say();  
// Hello

или загружать сразу весь модуль:

import * as lib from './lib';

lib.sayHello();  
// Hello

Формат также поддерживает экспорт по умолчанию:

// lib.js

// Экспорт дефолтной функции
export default function sayHello(){  
 console.log('Hello');
}

// Экспорт недефолтной функции
export function sayGoodbye(){  
 console.log('Goodbye');
}

который можно импортировать, например, так:

import sayHello, { sayGoodbye } from './lib';

sayHello();  
// Hello

sayGoodbye();  
// Goodbye

Вы можете экспортировать не только функции, но и всё, что пожелаете:

// lib.js

// Экспорт дефолтной функции
export default function sayHello(){  
 console.log('Hello');
}

// Экспорт недефолтной функции
export function sayGoodbye(){  
 console.log('Goodbye');
}

//  Экспорт простого значения
export const apiUrl = '...';

// Экспорт объекта
export const settings = {  
 debug: true
}

К сожалению, нативный формат модулей пока поддерживают не все браузеры.

Мы можем использовать формат модулей ES6 уже сегодня, но для этого потребуется компилятор наподобие Babel, который будет переводить наш код в формат ES5, такой, как AMD или CommonJS, перед тем, как код будет запущен в браузере.

2. Encapsulation in JS

Классы Get, Set in object Замыкание

3. Immutability, types, prevent object from writing

const object

Вы не можете назначить объект чему-то другому, в отличие от let и var, но вы все равно можете изменить значение каждого свойства, удалить свойство или создать свойство.

Object.preventExtensions(myObject)

Это предотвратит добавление новых свойств к объекту, обновление и удаление существующих свойств по-прежнему разрешено.

Object.seal(myObject)

Это предотвратит добавление новых и удаление существующих свойств в объект и из объекта, но обновление существующих свойств по-прежнему разрешено.

Object.freeze(myObject)

Это предотвратит изменение объекта. С замораживанием вы уверены, что он нигде не изменился в вашем коде.

4. Property descriptors

Метод Object.defineProperty - позволяет устанавливать свойствам некоторые настройки (можно ли свойство изменять, удалять и др.)

var o = {};

o.a = 1;
// Эквивалентно записи:
Object.defineProperty(o, 'a', {
  value: 1,
  writable: true,
  configurable: true,
  enumerable: true
});


// С другой стороны,
Object.defineProperty(o, 'a', { value: 1 });

// эквивалентно записи:
Object.defineProperty(o, 'a', {
  value: 1,
  writable: false,
  configurable: false,
  enumerable: false
});

Синтаксис

Object.defineProperty(объект, 'имя свойства', дескриптор);

Дескриптор - это объект, который описывает поведение свойства. В нем могут быть следующие свойства (в скобках указаны значения по умолчанию):

value //значение свойства (undefined)
writable //если true - свойство можно перезаписывать (false)
configurable // если true, то свойство можно удалять (false)
enumerable //если true, то свойство видно в цикле for..in (false)
get //Функции, которая возвращает значение свойства (undefined)
set //Функции, которая записывает значение свойства (undefined);

Если со свойством произвести запрещенное действие, например, попытаться изменить в то время как writable = false, то ничего не произойдет (а в строгом режиме (при указании 'use strict') - будет ошибка). Также запрещено указывать value/writeble если указаны get/set.

Дескриптор свойства – это обычный JavaScript-объект, описывающий атрибуты и значение свойства.

Дескрипторы свойств, присутствующие в объектах, бывают двух основных типов: дескрипторы данных и дескрипторы доступа.

Дескриптор данных - это свойство, имеющее значение, которое может быть (а может и не быть) записываемым.

Дескриптор доступа - это свойство, описываемое парой функций - геттером и сеттером.

Дескриптор может быть только чем-то одним из этих двух типов. Он не может быть одновременно обоими.

И дескриптор данных, и дескриптор доступа являются объектами. Свойства configurable и enumerable являются общими для дескриптора данных и для дескриптора доступа, value и writable относятся только к дескриптору данных, а get и set - к дескриптору доступа. Дескриптор не может относится к обоим типам одновременно.

6. Constructor

Функции-конструкторы являются обычными функциями. Но есть два соглашения:

  1. Имя функции-конструктора должно начинаться с большой буквы.

  2. Функция-конструктор должна вызываться при помощи оператора "new".

Например:

function User(name) {
  this.name = name;
  this.isAdmin = false;
}

let user = new User("Вася");

alert(user.name); // Вася
alert(user.isAdmin); // false

Когда функция вызывается как new User(...), происходит следующее:

  1. Создаётся новый пустой объект, и он присваивается this.

  2. Выполняется код функции. Обычно он модифицирует this, добавляет туда новые свойства.

  3. Возвращается значение this.

Мы можем использовать конструкторы для создания множества похожих объектов.

7. Prototype

Все, кроме примитивов - объекты, т.е. все кроме:

  • undefined

  • null

  • boolean

  • number

  • string

  • Symbol

Каждый конструктор имеет специальное св-во prototype. Каждый объект, созданный конструктором, содержит ссылку на свойство prototype своего конструктора. Эта ссылка хранится в свойстве __proto__. obj.toString() - строка, представляющая объект.

Promise / Async / Ajax / Fetch

Callback => Promise => Async Await AJAX(XMLHTTPRequest) => Fetch Promise – это специальный объект, который содержит своё состояние. Вначале pending («ожидание»), затем – одно из: fulfilled («выполнено успешно») или rejected («выполнено с ошибкой»). Кроме status имеет и value. При коллбэках была огромная вложенность обратных вызовов и пирамид из }) в конце. Подобные случаи принято называть Callback Hell или Pyramid of Doom. Вот основные недостатки:

  • Такой код сложно читать.

  • В таком коде сложно обрабатывать ошибки и одновременно сохранять его «качество».

Для решения этой проблемы в JavaScript были придуманы промисы (англ. promises). Теперь глубокую вложенность коллбэков можно заменить ключевым словом then:

queryDatabase({ username: 'Arfat'})
  .then((user) => {
    const image_url = user.profile_img_url;
    return getImageByURL('someServer.com/q=${image_url}') 
      .then(image => transformImage(image))
      .then(() => sendEmail(user.email))
})
.then(() => logTaskInFile('...'))
.catch(() => handleErrors()) // Обработка ошибок

Код стал читаться сверху вниз, а не слева направо, как это было в случае с обратными вызовами. Это плюс к читаемости. Однако и у промисов есть свои проблемы:

  • Всё ещё нужно работать с кучей .then.

  • Вместо обычного try/catch нужно использовать .catch для обработки всех ошибок.

  • Работа с несколькими промисами в цикле не всегда интуитивно понятна и местами сложна.

Async/Await

Добавление async-функций в ES2017 (ES8) сделало работу с промисами легче.

  • Важно отметить, что async-функции работают поверх промисов.

  • Эти функции не являются принципиально другими концепциями.

  • Async-функции были задуманы как альтернатива коду, использующему промисы.

  • Используя конструкцию async/await, можно полностью избежать использование цепочек промисов.

  • С помощью async-функций возможно организовать работу с асинхронным кодом в синхронном стиле.

Цель функций async/await упростить использование promises синхронно и воспроизвести некоторое действие над группой Promises.

Синтаксис состоит из двух ключевых слов: async и await. Первое делает функцию асинхронной. Именно в таких функциях разрешается использование await. Использование await в любом другом случае вызовет ошибку.

// В объявлении функции
async function myFn() {
  // await ...
}

// В стрелочной функции
const myFn = async () => {
  // await ...
}

function myFn() {
  // await fn(); (синтаксическая ошибка, т. к. нет async)
}

Async-функции похожи на обычные функции в JavaScript, за исключением нескольких вещей:

Async-функции всегда возвращают промисы

async function fn() {
  return 'hello';
}
fn().then(console.log)
// hello

Функция fn возвращает строку 'hello'. Т. к. это асинхронная функция, значение строки обёртывается в промис (с помощью конструктора).

Код выше можно переписать и без использования async:

function fn() {
  return Promise.resolve('hello');
}
fn().then(console.log);
// hello

В таком случае, вместо async, код вручную возвращает промис.

Что происходит, когда внутри асинхронной функции возникает какая-нибудь ошибка?

async function foo() {
  throw Error('bar');
}

foo().catch(console.log);

Если ошибка не будет обработана, foo() вернёт промис с реджектом. В таком случае вместо Promise.resolve вернётся Promise.reject, содержащий ошибку.

Функция async может содержать выражение await, которое приостанавливает выполнение функции async и ожидает ответа от переданного Promise, затем возобновляя выполнение функции async и возвращая полученное значение. Ключевое слово await допустимо только в асинхронных функциях.

Пример 1

Если выражение является промисом, то async-функция будет приостановлена до тех пор, пока промис не выполнится. Если же выражение не является промисом, то оно конвертируется в промис через Promise.resolve и потом завершается.

// Функция задержки
// с возвращением случайного числа
const delayAndGetRandom = (ms) => {
  return new Promise(resolve => setTimeout(
    () => {
      const val = Math.trunc(Math.random() * 100);
      resolve(val);
    }, ms
  ));
};

async function fn() {
  const a = await 9;
  const b = await delayAndGetRandom(1000);
  const c = await 5;
  await delayAndGetRandom(1000);
  
  return a + b * c;
}

// Вызов fn
fn().then(console.log);

Как работает fn функция?

  1. После вызова fn функции первая строка конвертируется из const a = await 9; в const a = await Promise.resolve(9);.

  2. После использования await, выполнение функции приостанавливается, пока a не получит своё значение (в данном случае это 9).

  3. delayAndGetRandom(1000) приостанавливает выполнение fn функции, пока не завершится сама (после 1 секунды). Это, фактически, можно назвать остановкой fn функции на 1 секунду.

  4. Также delayAndGetRandom(1000) через resolve возвращает случайное значение, которое присваивается переменной b.

  5. Случай с переменной c идентичен случаю переменной a. После этого опять происходит пауза на 1 секунду, но теперь delayAndGetRandom(1000) ничего не возвращает, т. к. этого не требуется.

  6. Под конец эти значения считаются по формуле a + b * c. Результат обёртывается в промис с помощью Promise.resolve и возвращается функцией.

Пример 2

function resolveAfter2Seconds(x) {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(x);
    }, 2000);
  });
}

async function add1(x) {
  const a = await resolveAfter2Seconds(20);
  const b = await resolveAfter2Seconds(30);
  return x + a + b;
}

add1(10).then(v => {
  console.log(v);  // prints 60 after 4 seconds.
});

async function add2(x) {
  const a = resolveAfter2Seconds(20);
  const b = resolveAfter2Seconds(30);
  return x + await a + await b;
}

add2(10).then(v => {
  console.log(v);  // prints 60 after 2 seconds.
});

Не путайте await и Promise.all

Функция add1 приостанавливается на 2 секунды для первого await и еще на 2 для второго. Второй таймер создается только после срабатывания первого. В функции add2 создаются оба и оба же переходят в состояние await. В результате функция add2 завершится скорее через две, чем через четыре секунды, поскольку таймеры работают одновременно. Однако запускаются они все же не паралелльно, а друг за другом - такая конструкция не означает автоматического использования Promise.all. Если два или более Promise должны разрешаться параллельно, следует использовать Promise.all.

Пример 3 (с выбрасыванием исключения)

Когда функция async выбрасывает исключение

async function throwsValue() {
    throw new Error('oops');
}
throwsValue()
    .then((resolve) => {
            console.log("resolve:" + resolve);
        },
        (reject) => {
            console.log("reject:" + reject);
        });
//prints "reject:Error: oops"
//or
throwsValue()
    .then((resolve) => {
        console.log("resolve:" + resolve);
    })
    .catch((reject) => {
        console.log("reject:" + reject);
    });
//prints "reject:Error: oops"

AJAX

это Асинхронный JavaScript и XML. Это набор методов веб-разработки, которые позволяют веб-приложениям работать асинхронно — обрабатывать любые запросы к серверу в фоновом режиме. Под AJAX понимается не одна технология, и она не является новой. На самом деле это группа технологий (HTML, CSS, Javascript, XML, и т.д.), которые связываются вместе для создания современных веб-приложений.

AJAX запрос, - это комплекс выполняемых задач, в режиме «запрос-ответ».

В основе технологии лежит использование нестандартного объекта XMLHttpRequest, необходимого для взаимодействия со скриптами на стороне сервера. Объект может как отправлять, так и получать информацию в различных форматах включая XML, HTML и даже текстовые файлы. Самое привлекательное в Ajax — это его асинхронный принцип работы. С помощью этой технологии можно осуществлять взаимодействие с сервером без необходимости перезагрузки страницы. Это позволяет обновлять содержимое страницы частично, в зависимости от действий пользователя.

XMLHttpRequest – это встроенный в браузер объект, который даёт возможность делать HTTP-запросы к серверу без перезагрузки страницы. Включает в себя набор методов для работы в протоколах HTTP и HTTPS.

Несмотря на наличие слова «XML» в названии, XMLHttpRequest может работать с любыми данными, а не только с XML. Мы можем загружать/скачивать файлы, отслеживать прогресс и многое другое.

На сегодняшний день не обязательно использовать XMLHttpRequest, так как существует другой, более современный метод fetch.

В современной веб-разработке XMLHttpRequest используется по трём причинам:

  1. По историческим причинам: существует много кода, использующего XMLHttpRequest, который нужно поддерживать.

  2. Необходимость поддерживать старые браузеры и нежелание использовать полифилы (например, чтобы уменьшить количество кода).

  3. Потребность в функциональности, которую fetch пока что не может предоставить, к примеру, отслеживание прогресса отправки на сервер.

XMLHttpRequest имеет два режима работы: синхронный и асинхронный.

Рассмотрим асинхронный, так как в большинстве случаев используется именно он.

Чтобы сделать запрос, нужно выполнить три шага:

  1. Создать XMLHttpRequest.

    let xhr = new XMLHttpRequest(); // у конструктора нет аргументов

    xhr – это переменная или константа в которой будет хранится, - экземпляр класса XMLHttpRequest, объект с набором методов.

  2. Инициализировать его.

    xhr.open(method, URL, [async, user, password])

    Этот метод обычно вызывается сразу после new XMLHttpRequest. В него передаются основные параметры запроса:

    • method – HTTP-метод. Обычно это "GET" или "POST".

    • URL – URL, куда отправляется запрос: строка, может быть и объект URL.

    • async – если указать false, тогда запрос будет выполнен синхронно, это мы рассмотрим чуть позже.

    • user, password – логин и пароль для базовой HTTP-авторизации (если требуется).

    Заметим, что вызов open, вопреки своему названию, не открывает соединение. Он лишь конфигурирует запрос, но непосредственно отсылается запрос только лишь после вызова send.

  3. Послать запрос.

    xhr.send([body])

    Этот метод устанавливает соединение и отсылает запрос к серверу. Необязательный параметр body содержит тело запроса.

  4. Слушать события на xhr, чтобы получить ответ.

    Три наиболее используемых события:

    • load – происходит, когда получен какой-либо ответ, включая ответы с HTTP-ошибкой, например 404.

    • error – когда запрос не может быть выполнен, например, нет соединения или невалидный URL.

    • progress – происходит периодически во время загрузки ответа, сообщает о прогрессе.

Полный пример. Код ниже загружает /article/xmlhttprequest/example/load с сервера и сообщает о прогрессе:

// 1. Создаём новый XMLHttpRequest-объект
let xhr = new XMLHttpRequest();

// 2. Настраиваем его: GET-запрос по URL /article/.../load
xhr.open('GET', '/article/xmlhttprequest/example/load');

// 3. Отсылаем запрос
xhr.send();

// 4. Этот код сработает после того, как мы получим ответ сервера
xhr.onload = function() {
  if (xhr.status != 200) { // анализируем HTTP-статус ответа, если статус не 200, то произошла ошибка
    alert(`Ошибка ${xhr.status}: ${xhr.statusText}`); // Например, 404: Not Found
  } else { // если всё прошло гладко, выводим результат
    alert(`Готово, получили ${xhr.response.length} байт`); // response -- это ответ сервера
  }
};

xhr.onprogress = function(event) {
  if (event.lengthComputable) {
    alert(`Получено ${event.loaded} из ${event.total} байт`);
  } else {
    alert(`Получено ${event.loaded} байт`); // если в ответе нет заголовка Content-Length
  }

};

xhr.onerror = function() {
  alert("Запрос не удался");
};

Каждый ответ от сервера включает в себя результат запроса в следующих свойствах xhr:status Код состояния HTTP (число): 200, 404, 403 и так далее, может быть 0 в случае, если ошибка не связана с HTTP.

statusText Сообщение о состоянии ответа HTTP (строка): обычно OK для 200, Not Found для 404, Forbidden для 403, и так далее.

response (в старом коде может встречаться как responseText)Тело ответа сервера, данные, которые придут от сервера в виде строки

Тип ответа:

Мы можем использовать свойство xhr.responseType, чтобы указать ожидаемый тип ответа:

  • "" (по умолчанию) – строка,

  • "text" – строка,

  • "arraybuffer"ArrayBuffer (для бинарных данных, смотрите в ArrayBuffer, бинарные массивы),

  • "blob"Blob (для бинарных данных, смотрите в Blob),

  • "document" – XML-документ (может использовать XPath и другие XML-методы),

  • "json" – JSON (парсится автоматически).

К примеру, давайте получим ответ в формате JSON:

let xhr = new XMLHttpRequest();

xhr.open('GET', '/article/xmlhttprequest/example/json');

xhr.responseType = 'json';

xhr.send();

// тело ответа {"сообщение": "Привет, мир!"}
xhr.onload = function() {
  let responseObj = xhr.response;
  alert(responseObj.message); // Привет, мир!
};

Состояния запроса:

У XMLHttpRequest есть состояния(статусы HTTP-запроса), которые меняются по мере выполнения запроса. Текущее состояние можно посмотреть в свойстве xhr.readyState.

Список всех состояний, указанных в спецификации:

UNSENT = 0; // исходное состояние
OPENED = 1; // вызван метод open
HEADERS_RECEIVED = 2; // получены заголовки ответа
LOADING = 3; // ответ в процессе передачи (данные частично получены)
DONE = 4; // запрос завершён

Состояния объекта XMLHttpRequest меняются в таком порядке: 0123 → … → 34. Состояние 3 повторяется каждый раз, когда получена часть данных.

onreadystatechange – это событие которое случится когда нам придет ответ от сервера.

Изменения в состоянии объекта запроса генерируют событие readystatechange:

xhr.onreadystatechange = function() {
  if (xhr.readyState == 3) {
    // загрузка
  }
  if (xhr.readyState == 4) {
    // запрос завершён
  }
};

COMET

COMET – общий термин, описывающий различные техники получения данных по инициативе сервера.

Можно сказать, что AJAX – это «отправил запрос – получил результат», а COMET – это «непрерывный канал, по которому приходят данные».

Примеры COMET-приложений:

  • Чат – человек сидит и смотрит, что пишут другие. При этом новые сообщения приходят «сами по себе», он не должен нажимать на кнопку для обновления окна чата.

  • Аукцион – человек смотрит на экран и видит, как обновляется текущая ставка за товар.

  • Интерфейс редактирования – когда один редактор начинает изменять документ, другие видят информацию об этом. Возможно и совместное редактирование, когда редакторы видят изменения друг друга.

COMET-технологии позволяют организовать обновление данных на странице без участия пользователя.

На текущий момент технология COMET удобно реализуется во всех браузерах.

Fetch

Fetch - это улучшеный XMLHttpRequest, позволяет делать запросы, схожие с XMLHttpRequest (XHR). Основное отличие заключается в том, что Fetch API использует Promises (Обещания), которые позволяют использовать более простое и чистое API, избегать катастрофического количества callback'ов.

Синтаксис метода fetch:

let promise = fetch(url, [options]) 
  • url – URL, на который сделать запрос,

  • options – необязательный объект с настройками запроса.

Без options это простой GET-запрос, скачивающий содержимое по адресу url.

При вызове fetch возвращает ПРОМИС, который, когда получен ответ, выполняет коллбэки с объектом Response или с ошибкой, если запрос не удался.

Процесс получения ответа обычно происходит в два этапа.

Во-первых, promise выполняется с объектом встроенного класса Response в качестве результата, как только сервер пришлёт заголовки ответа.

На этом этапе мы можем проверить статус HTTP-запроса и определить, выполнился ли он успешно, а также посмотреть заголовки, но пока без тела ответа.

Промис завершается с ошибкой, если fetch не смог выполнить HTTP-запрос, например при ошибке сети или если нет такого сайта. HTTP-статусы 404 и 500 не являются ошибкой.

Мы можем увидеть HTTP-статус в свойствах ответа:

  • status – код статуса HTTP-запроса, например 200.

  • ok – логическое значение: будет true, если код HTTP-статуса в диапазоне 200-299.

Например:

let response = await fetch(url);

if (response.ok) { // если HTTP-статус в диапазоне 200-299
  // получаем тело ответа (см. про этот метод ниже)
  let json = await response.json();
} else {
  alert("Ошибка HTTP: " + response.status);
}

Во-вторых, для получения тела ответа нам нужно использовать дополнительный вызов метода.

Response предоставляет несколько методов, основанных на промисах, для доступа к телу ответа в различных форматах:

  • response.text() – читает ответ и возвращает как обычный текст,

  • response.json() – декодирует ответ в формате JSON,

  • response.formData() – возвращает ответ как объект FormData (разберём его в следующей главе),

  • response.blob() – возвращает объект как Blob (бинарные данные с типом),

  • response.arrayBuffer() – возвращает ответ как ArrayBuffer (низкоуровневое представление бинарных данных),

  • помимо этого, response.body – это объект ReadableStream, с помощью которого можно считывать тело запроса по частям.

Например, получим JSON-объект с последними коммитами из репозитория на GitHub:

let url = 'https://api.github.com/repos/javascript-tutorial/en.javascript.info/commits';
let response = await fetch(url);

let commits = await response.json(); // читаем ответ в формате JSON

alert(commits[0].author.login);

То же самое без await, с использованием промисов:

fetch('https://api.github.com/repos/javascript-tutorial/en.javascript.info/commits')
  .then(response => response.json())
  .then(commits => alert(commits[0].author.login));

Пример без await:

Объект response кроме доступа к заголовкам headers, статусу status и некоторым другим полям ответа, даёт возможность прочитать его тело, в желаемом формате, что было описано выше.

Итого: Типичный запрос с помощью fetch состоит из двух операторов await:

let response = await fetch(url, options); // завершается с заголовками ответа
let result = await response.json(); // читать тело ответа в формате JSON

Или, без await:

fetch(url, options)
  .then(response => response.json())
  .then(result => /* обрабатываем результат */)

Параметры ответа:

  • response.status – HTTP-код ответа,

  • response.oktrue, если статус ответа в диапазоне 200-299.

  • response.headers – похожий на Map объект с HTTP-заголовками.

Framework vs library

Фреймворк - это набор библиотек и инструментов. Библиотека - это просто программный модуль на определенном языке.

Библиотека - это сборка различный функций и подпрограмм, которая может быть перенесена и использоваться потом в различных приложениях. Основная идея в том, что они переносимы между приложениями и могут быть многократно использованы без изменений.

Фреймворк - это каркас для будущего приложения, в котором собраны все основные необходимые детали: библиотеки, структура, начальный код и т.д. Можно сказать, это минимальная заготовка, на основе которой Вы будете дописывать функционал и строить дальше приложение. Ваше приложение будет работать за счет того, что уже есть и заботливо для Вас заготовлено.

Фреймворк — его функции, в отличие от библиотеки, не вызываются вами, а наоборот, ваш код вызывается из него.

Библиотека — это готовый к использованию набор кода, который бежит в контексте приложения, и точно так же выполняет свою работу. То есть библиотека становится при подключении частью приложения.

API - это интерфейс взаимодействия с программой извне. У Вас есть сам по себе какой-то готовый продукт и он представляется черным ящиком и Вы хотите, что бы им могли пользоваться другие программы. Вы определяете методы взаимодействия с ним и описываете их, а сторонние программы им пользуются. Это самый простой вариант: возможность для приложения обратиться к коду вне этого приложения. Это набор функциональности для того, чтобы заставить внешнюю для программы сущность сделать свою работу.

  1. Linked list

Big O

ES2015

CoreHTML

CoreCSS (flexbox, positioning)

CoreJS

Design patterns

Architectural Patterns []

FE optimization / Performance

DOM/BOM

Agile/SCRUM

tasks

reduce, filter, map, forEach, DOM traversing

Last updated