вторник, 24 ноября 2020 г.

A programmer: JavaScript #24. AJAX, XMLHttpRequest, fetch(), async/await

Когда я первый раз узнал об AJAX-запросах, это было связано с необходимостью получать данные с сервера, не перезагружая страницу. И выглядело это приблизительно так:

Что означало: при нажатии кнопки на клавиатуре в поле "login", взять данные и передать их методом POST в обработчик ajax.php (название файла, разумеется, может быть любым - скрин с php-проекта), и в случае успешной отработки обработчика, вывести результат на страницу в элемент с названием "#login_warning".

И, собственно, вся асинхронность у меня стала после этого ассоциироваться с тем, что оказалось всего-лишь частью библиотеки jQuery. Но принципиально - это только частный случай Asynchronous JavaScript And XML (AJAX). О чем идет речь?

Ранее я говорил, что promise - это способ справиться с асинхронностью - сделать так, чтобы команды выполнялись последовательно, поскольку часто бывает так, что для выполнения следующей функции нужен результат предыдущей, а в JavaScript это не гарантировано. Чаще всего это нужно при отправке http-запросов, когда данные должны быть выведены после получения ответа от сервера.

Без promise'ов это коротко выглядит так: создается специальный объект XMLHttpRequest, не нем открывается соединение и посылается запрос. Заранее определяется, что должно выполниться при том или ином ответе сервера:


fetch() - это интерфейс получения запросов - то есть способ сделать то же самое. "Под капотом" он создает объект request'a, посылает его, но возвращает promise с обработкой response'а:

 

На самом деле там вставляются ещё проверки, формат вывода данных, а в случае POST-запроса ещё и дополнительные параметры, но принцип таков.

Ну и async/await - это приблизительно то же самое. async перед функцией указвает на то, что функция возвращает promise, соответственно, внутри функция будет ждать результата от promise'a (в этом месте пишется await). Ну и поскольку promise может завершиться с ошибкой, то всё это оборачивается в try-catch:


Информация - отсюда, отсюда, отсюда, отсюда, отсюда. На самом деле ещё много откуда, но это самые последние по времени использования ресурсы.

UPD: 

1. Нашел ещё один очень удобный ресурс - вот он. Здесь показаны разные варианты работы с асинхронными запросами в одном видео.

2. Про популярный нынче axios ничего не писал, но там логика приблизительно такая же.

четверг, 19 ноября 2020 г.

A programmer: JavaScript #23. Prototype inheritance (Прототипное наследование)

Если коротко, наследование - это способ получения полей и методов без того, чтобы явно декларировать их в самом объекте. Осуществляется это указанием, что данный объект является "наследником" какого-то другого объекта, а значит, он по умолчанию имеет те же поля и методы, что и "родитель", плюс обычно добавляет какие-то собственные или переопределяет родительские. Это удобно.

В JavaScript используется "прототипное наследование". Что это значит и в чем отличие от "классического" наследования. Под классическим наследованием я здесь буду понимать то, которое используется в Java. В классическом наследовании наследуются не объекты, а классы. А объект имеет те поля и методы, которые должны иметь объекты его класса (включая наследуемые от классов-родителей). Это создает структуру наследования, что имеет свои преимущества и недостатки.

Прототипное наследование отличается тем, что наследуются именно объекты и именно от объектов. То есть некоторый объект может объявить другой объект своим прототипом:

var object = Object.create(prototypeObject);

Здесь object объявляет себя наследником объекта prototypeObject, слеовательно, он имеет такие же, как у него поля и методы.

Собственно, можно наследовать не только объект от объекта, но и класс от класса - через конструкторы:

ClassA.prototype = Object.create(ClassB.prototype);

Только в этом случае сам конструктор тоже наследуется - и у ClassA будет конструктор от ClassB. Чтобы этого не происходило нужно отдельно его прописать (как бы "вернуть" себе обратно):

ClassA.prototype.constructor = ClassA;

Технически это выглядит так, что любой объект имеет скрытое свойство [[Prototype]], которое можно явно задать (иначе оно равно null):

let object.__proto__ = prototypeObject;

 Здесь __proto__ - геттер/сеттер для [[Prototype]],в более поздних версиях языка он заменяется функциями Object.getPrototypeOf и , хотя __proto__ тоже поддерживается.

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

Важно отметить, что this всегда указывает на сам объект, но не на его протоип. То есть прототип выступает для наследника как хранилище объектов. Поясняю:

Пусть в прототипе есть метод:

setState() {

    this.state = true;

}

Если его вызвать на экземпляре наследника (object.setState()), то state = true будет присвоен наследнику, а не прототипу.

Информация взята отсюда, отсюда и отсюда.

A programmer: JavaScript #22. Closures (Замыкания)

Короткая и простая тема. Думал я поначалу. Есть, как минимум, два объяснения, что это такое.

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

function private() {

    let a = 0;

    return function() {

        return a;

    };

};

В ней никто не имеет доступа к переменной а извне, и потому не может её изменить.

Во-вторых, замыкания - это функция внутри функции. Основная польза от этого в том, что её можно присвоить какой-то переменной, а вызвать потом через эту переменную. Например, результатом присвоения let variable = private(); будет variable, которая при вызове variable() будет возвращать а. Одним из самых распространенных применеий замыканий является счетчик: 

function private() {

    let a = 0;

    return function() {

        return a++;

    }

}

let counter = private(); 

тогда при каждом вызове counter() будет возвращаться значение, на единицу большее, чем при предыдущем вызове функции.

Вот тут-то мне и стало непонятно: если функция отработала, то она должна была бы уничтожиться? Тогда в следующий раз заново бы создавалась и инициализировалась новая let a = 0;? Тогда о каком счетчике может быть речь?

Ответ пришлось поискать, и вот он: объект существует пока существует ссылка на него. И здесь мы имеем, что да, функция private() отработала, но внутренняя по отношению к ней функция продолжает существовать, поскольку в момент вызова private() её присвоили другому объекту (где она может ожидать своего вызова). А значит, и private() не уничтожена, а переменная изменила своё значение (в данном случае а = а+1)!

Если детализировать, то:

1. При вызове функции создается лексическое окружение (этой функции) - LexicalEnvironment, в котором, например, будут искаться переменные, если они не были найдены локально внутри. Грубо говоря, это внешняя область видимости по отношению к данной функции.

2. Также создании любая функции получает скрытое свойство [[Environment]], которое ссылается на её окружение (и тем самым как бы "запоминает" его). Для внутренней функции окружением будет внешняя функция с теми значениями переменных, которые на данный момент в ней есть.

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

Подробнее - здесь.

четверг, 12 ноября 2020 г.

A programmer: JavaScript #21. Event loop + Promises

JavaScript - однопоточный язык, то есть у него один стек, следовательно, только одна комманда может быть выполнена в единицу времени. Но если у тебя только один поток, как у JavaScript, это может привести к серьёзным задержкам. Проблема с однопоточностью решается "асинхронностью". То есть JavaScript использует дополнительные ресурсы браузера или NodeJS или какого-либо другого окружения, кроме собственно основного движка, для того, чтобы иметь возможность что-то делегировать этим ресурсам и не ждать окончания одной операции, чтобы приступить к следующей.

Это реализуется через "цикл событий" ("event loop"):

  1. Когда функция вызывается, она загружается в стек.
  2. Но потом она начинает выполняться в "дополнительных ресурсах" пока не выполнится до конца.
  3. Пока она выполняется "в стороне", другие функции из стека могут быть в него загружены.
  4. Когда функция выполнена, она возвращается сначала в "очередь на возвращение" ("task queue") и обратно в стек.

Event loop - потому что JavaScript циклом мечется между стеком, и "очередью", проверяя, 1) пустой ли стек, и если стек пустой, то 2) есть ли в очереди то, что нужно возвращать. И если ещё нету, бежит дальше по кругу, если есть - помещает это (то, что первое в очереди) в стек.

И здесь есть большая проблема: часто мы не можем знать, в какой очередности будут выполняться функции. То есть что выполнится первым - то, что идет следующим в коде или то, что вернется из очереди?

Так вот, чтобы избежать синхронности придумали колбеки ("callback"). Колбек - это функция, которая передается в качестве параметра в другую функцию, если нужно, чтобы она была выполнена после той, в которую передана (или в нужном месте внутри). Например:

function callback() {

    console.log('I am callback!');

};

let a = 'I am a parameter of anotherFunction!';

function anotherFunction(a, callback) {

    console.log(a);

    callback();

}

Тогда, если нам важна очередность выполнения, мы должны вкладывать колбек в колбек в колбек и т.д. В этом коде легко запутаться, особенно потому, что переданные функции могут быть описаны совершенно в другом месте, а здесь только вызваны - это называется "ад колбеков".

Чтобы его избежать, придумали (например) промисы ("promises").

Промис - это объект, который "обещает" выполнить то, что внутри. Он принимает функцию, которая имеет параметры resolve и reject, которые, соответственно, вызываются в случае успешного или неуспешного выполнения (вызываются с необходимыми параметрами). То есть, промис не только имеет возвращаемое значение той функции, которая выполнялась внутри него, но и состояние, успешно или нет прошло выполнение. И это состояние можно использовать - в случае неуспешного выполнения - ловить ошибку, добавив к промису через точку catch(...), а в случае успешного - вызывать следующую функцию, добавив к промису через точку then(...). Ну и соответственно, можно промисы цеплять в цепочку один за одним, если принципиально последовательное выполнение определенных функций.

И поскольку я вчера тупил, и не смог написать промис на собеседовании, то вот пример:

let promise = new Promise(function(resolve, request) {

    let data = 5 + 3;

    resolve(data);

}).then(function(data) {

     console log(data + 125);

});

Как-то так.

понедельник, 9 ноября 2020 г.

A programmer: JavaScript #20. Pattern "Observer"

В общих чертах петтерн "observer" (обозреватель, наблюдатель) предполагает, что есть некий объект, который меняет своё состояние и оповещает об этом тех, кто подписан на уведомления (subscribers). Применительно к React'у это более, чем актуально, поскольку при изменении состояния (данных), компоненты должны перерисовываться, а значит, должны следить за изменениями сообветствующих данных.

При этом компоненты, которые себя перерисовывают, получают данные, зависят от них, но state, который хранит состояние, не должен зависеть от них - то есть он не должен сам вызывать метод перерисовки, находящийся в компонентах. Тогда как компоненты узнают, что state изменился?

А вот как: 

С одной стороны, файл, где происходит отрисовка компонентов (допустим, index.js) вызывает rerender() и передает туда state, который предварительно импортирует:

rerender(state);

То есть index.js зависит от state'а, как и положено.

Но index.js также вызывает метод subscribe и передает туда вызов свой rerender():

subscribe(rerender);

Теперь rerender() может быть вызван прямо в state, при этом state от него не зависит, а только использует реализацию.

С другой стороны, при изменении state вызывает callSubscriber(), но его реализацию он получает в распоряжение из метода subscribe():

// некоторые изменения данных

callSubscriber();

В самом state он описывается приблизительно так:

export const subscribe = (observer) => {
     rerender = observer;
}

То есть теперь callSubscriber(), вызываемый в state, - это тот самый rerender(), который реализован в index.js.

Следовательно, теперь при любом изменениии state, он вызывает метод перерисовки, который он получил через subscribе. Это означает, что state по-преженему не зависит от UI, потому что ему всё равно, как реализована перерисовка - он её только вызывает, но не импортирует.

P.S.: Обычно паттерн "observer" объясняют на примере Java. В некотором смысле оно понятнее, потому что там есть интерфейсы как отдельные сущности. Но вот мне помогло объяснение в исполнении Димыча.

воскресенье, 8 ноября 2020 г.

A programmer: JavaScript #19 The logic of React. Part 1

Снова про React. На этот раз вдохновился довольно объёмным курсом от Димыча (IT-KAMASUTRA, "Курс "React JS - путь самурая 1.0", уроки, практика") - он объясняет всё очень подробно и понятно, действительно большой молодец, рекомендую.

React – библиотека для фронтенда, то есть для удобной отрисовки страницы, которая должна быть видна в браузере. Собственно, пишу только об основных принципах и ключевых особенностях.

1. React используется для Single Page Application. «Классический вариант» работы с сайтом предполагает, что когда нужно показать что-то с данного сайта, браузер посылает запрос на сервер, и сервер присылает новую страницу. В свою очередь логика Single Page Application предполагает, что практически всё необходимое загружается в браузер сразу, и при запросах на новый контент уже сам браузер отрисовывает его, не обращаясь к серверу.

2. Для этого используется модульность и виртуальный DOM. Если DOM (Document Object Model) – это модель документа, то виртуальный DOM – это копия этой модели в памяти Reactа. В соответствии с виртуальным DOM’ом React «собирает» страницу из компонентов. Если нужно что-то перерисовать, он вносит необходимые изменения а нужный компонент, реагирует на изменения – поэтому и «React». В каждом компоненте есть метод render(), который, собственно, и отрисовывает его. Поэтому не затрагиваются все остальные элементы страницы, что обеспечивает быструю работу.

3. Сложность состоит в том, что слои отображения (User Interface (UI) – то, чем и являются компоненты) и бизнес-логика должны быть разделены. Изменения view предполагают операции с данными, но данные не должны храниться в самих компонентах – компоненты должны использовать только те данные, которые пришли из слоя бизнес-логики. Следовательно, их надо как-то передавать из уровня бизнес-логики в UI и обратно.

4. Из уровня бизнес-логики в UI данные передаются через props’ы – атрибуты компонентов. Они могут передаваться от родительских компонентов в наследников, в которых они становятся элементами state – состояния данного компонента.

5. Нюанс состоит в том, что если нужно передать props’ы на несколько уровней, то и прописывать их нужно на каждом уровне, что неудобно. Чтобы этого не делать, используют Context – некоторое хранилище данных. Компонент, в котором он создаётся, помещается в обёртку Context.Provider, который и передаёт необходимые props’ы, а остальные помещаются в обёртку Context.Consumer. Что означает, что они имеют доступ ко всем props’ам, которые предоставляет Context.Provider.

6. Для того, чтобы передать некоторые данные из UI на уровень бизнес-логики, в тех же props’ах компонентам передаются и необходимые методы. Сами методы прописаны на уровне бизнес-логики, но передаётся возможность их вызова.

7. На уровне бизнес-логики создают store – хранилище данных (состояний компонентов). С помощью переданных им методов компоненты передают туда данные. С помощью переданных компонентам props’ов компоненты перерисовывают себя – это постоянно работающий цикл. Такое «архитектурное решение» называется Flux. Для его реализации существуют «системы управления состоянием» («state management system»), наиболее известная из них – Redux.

8. В React возможет функциональный и объектно-ориентированный (ООП) подходы. В первом случае оперируют функциями, во втором – классами. Классы могут содержать и собственный state – состояние, и собственные функции. Если объект вызывает собственный метод, используется this. Но если мы передаем метод куда-либо, то контекст выполнения меняется, тогда this будет указывать на другой объект. Чтобы не происходило путаницы, метод привязывают к его контексту с помощью метода bind().

9. В Store кроме данных содержатся методы для работы с данными. Обычно их выносят в общий метод, называемый dispatch(), и передают ему (кроме необходимых данных) объект action, который имеет определенный тип. В зависимости от типа dispatch() выполняет нужный функционал. Это сокращает запись в компонентах (вместо того, чтобы прописывать разные отдельные методы).

 Продолжение следует.