Pull to refresh

Улучшайте свое знание JavaScript разбирая исходный код

Reading time8 min
Views17K
Original author: Carl Mungazi
Когда вы только начинаете карьеру программиста копание в исходном коде открытых библиотек и фреймворков может казаться чем-то страшным. В этой статье Карл Мунгази делится опытом как он поборол свой страх и стал использовать исходный код для приобретения знаний и развития навыков. Он также использует Redux чтобы показать как он «разбирает» библиотеку.

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

Мы как раз только что переписали устаревший собственный фреймворк, который использовался для создания курсов интерактивного обучения. В самом начале работ по переписыванию мы изучили некоторые готовые решения, включая Mithril, Inferno, Angular, React, Aurelia, Vue, и Polymer. Поскольку я был еще юным падаваном (только что перешедшим из журналистики в веб-разработку), то был ужасно напуган сложностью каждого фреймворка и непониманием того, как они работают.

Понимание стало приходить, когда я начал тщательно исследовать фреймворк Mithril. С тех пор мое знание JavaScript — и программирования в целом — существенно подкрепилось благодаря часам, потраченным на копание во внутренностях библиотек, которые я ежедневно использовал на работе и в собственных проектах. В этой статье я расскажу как можно использовать вашу любимую библиотеку в качестве учебного пособия

image
Чтение исходного кода я начал с функции hyperscript из Mithril

Плюсы разбора исходного кода


Один из главных плюсов разбора исходного кода — можно узнать много нового. Когда я начинал разбирать код Mithril, то очень слабо представлял себе что такое виртуальный DOM. Когда же закончил — уже знал, что виртуальный DOM это техника, включающая создание дерева объектов, описывающих пользовательский интерфейс. Затем это дерево может быть преобразовано в DOM-элементы с использованием DOM API вроде document.createElement. Для обновления же создается новое дерево, описывающее будущее состояние интерфейса и затем сравнивается с предыдущим вариантом этого самого дерева.

Я читал об этом во многих статьях и руководствах, но самым поучительным было наблюдать за всем этим в процессе работы над нашим приложением. Также я научился задавать правильные вопросы при сравнении фреймворков. Вместо того, чтобы сравнивать рейтинги, например, можно задать вопрос «Как способ работы данного фрейворка с изменениями влияет на производительность и удобство конечного пользователя?»

Еще одним плюсом является развитие понимания хорошей архитектуры приложения. Несмотря на то, что большинство open-source проектов в общем-то более-менее похожи по структуре своих репозиториев, все равно у них есть различия. Структура Mithril весьма плоская и если вы хорошо разбираетесь в его API — можете делать вполне реалистичные предположения о коде в папках render, router и request. С другой стороны, структура React`a отражает его новую архитектуру. Разработчики отделили модуль, отвечающий за обновление UI (react-reconciler) от модуля, отвечающего за рендеринг DOM-элементов (react-dom).

Одно из преимуществ такого разделения для разработчиков — они могут писать свои собственные рендереры с помощью хуков в react-reconciler. Parcel, сборщик модулей который я недавно изучал, тоже имеет папку packages, как и React. Ключевой модуль называется parcel-bundler, он содержит код, который отвечает за создание сборок, работу сервера обновления модулей (hot module server) и инструмент командной строки.

image
Разбор исходного кода вскоре приведет вас к чтению спецификации JavaScript

Еще один плюс, который был для меня большим сюрпризом, — вам становится проще читать официальную спецификацию JavaScript. Впервые я обратился к ней, когда пытался разобраться, чем отличается throw Error и throw new Error (спойлер — ничем). Я задался этим вопросом потому что Mithril использовал throw Error в имплементации функции m и мне стало интересно, а чем же оно лучше throw new Error. Потом я узнал также, что операторы && и || не обязательно возвращают булевы значения, нашел правила, по которым оператор нестрогого сравнения == «разруливает» значения и причину по которой Object.prototype.toString.call({}) возвращает '[object Object]'.

Как разбирать исходный код


Разбирать исходный код можно по-разному. Самым простым способом мне кажется следующий: выбрать метод из вашей библиотеки и описать, что происходит, когда вы его вызываете. Не стоит описывать каждый шаг, нужно просто попытаться понять его общие принципы и структуру.

Недавно я таким образом разобрал ReactDOM.render и таким образом многое узнал о React Fiber и некоторых сложностях в его имплементации. К счастью, React весьма популярен и наличие большого количества статей на ту же тему от других разработчиков ускорило процесс.

Это погружение в код также познакомило меня с концепцией co-operative scheduling, методом window.requestIdleCallback и живым примером связного списка (React обрабатывает обновления, отправляя их в очередь, которая является связным списком обновлений с приоритетами). В процессе неплохо бы создать простейшее приложение с использованием библиотеки. Это упрощает дебаггинг, поскольку вам не придется иметь дело со стек-трейсом других библиотек.

Если я не делаю подробный обзор, то открою папку node_modules в проекте над которым работаю или загляну на GitHub. Всегда так делаю, натыкаясь на баг или интересную фичу. Читая код на GitHub, убедитесь в том, что это последняя версия. Код последней версии можно увидеть кликнув по кнопке смены веток и выбрав «теги». Изменения в библиотеках и фреймворках происходят постоянно, так что вряд ли вы захотите разбирать что-то, чего может не быть в следующей версии.

Более поверхностный вариант изучения исходного кода — то, что я называю «беглый взгляд». Как-то я установил express.js, открыл папку node_modules и прошелся по зависимостям. Если README не давал мне удовлетворительного пояснения, я читал исходник. Это привело меня к интересным открытиям:

  • Express использует два модуля для слияния объектов, и работа этих модулей очень отличается. merge-descriptors добавляет только свойства, найденные в исходном объекте, а также добавляет неперечисляемые свойства, в то время как utils-merge пробегается по перечисляемым свойствам объекта и всей его цепочки прототипов. merge-descriptors использует Object.getOwnPropertyNames() и Object.getOwnPropertyDescriptor(), а utils-merge использует for..in;
  • Модуль setprototypeof предоставляет кросплатформенный вариант задания прототипа создаваемого (инстанциируемого) объекта;
  • escape-html — это 78-строчный модуль экранирования строк, после обработки которым контент можно вставлять в HTML;

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

Дебаггинг инструменты браузера — ваши лучшие друзья при дебаге кода на фронтенде. Кроме всего прочего они позволяют останавливать выполнение программы в любой момент и проверить при этом ее состояние, пропустить выполнение функции, либо же зайти внутрь оной или выйти из нее. В минифицированном коде это невозможно — поэтому я этот код распаковываю и помещаю в соответствующий файл в папке node_modules.

image
Используйте дебаггер как полезное приложение. Сделайте предположение, а затем проверьте его.

Пример из практики: функция connect в Redux


React-Redux — это библиотека для управления состоянием приложений на React. Когда я работаю с популярными библиотеками вроде этой, то начинаю с поиска статей об их использовании. При подготовке этого примера я ознакомился с данной статьей. Это еще один плюс изучения исходного кода — оно ведет вас к информативным статьям вроде этой, которые улучшают ваше мышление и понимание.

Connect — это функция react-redux, которая связывает react-компонент и redux-хранилище приложения. Как? Согласно документации она делает следующее:
“… возвращает новый, связанный класс компонента, который является оберткой переданного в нее компонента.”
Прочитав это я задаюсь следующими вопросами:

  • Знаю ли я паттерны или концепции, где функции возвращают входящие параметры обернутыми дополнительным функционалом?
  • Если да, то как мне использовать это, основываясь на описании из документации?

Обычно следующим шагом будет создание примитивного приложения с использованием функции connect. Тем не менее в данной ситуации я использовал новое приложение на React, над которым мы работали, потому что хотел понять connect в контексте приложение которое вполне вероятно скоро попало бы на production.

Компонент, на котором я сосредоточился, выглядит примерно так:

class MarketContainer extends Component {
 // code omitted for brevity
}

const mapDispatchToProps = dispatch => {
 return {
   updateSummary: (summary, start, today) => dispatch(updateSummary(summary, start, today))
 }
}

export default connect(null, mapDispatchToProps)(MarketContainer);

Это компонент-контейнер, который служит оберткой для четырех меньших связанных компонентов. Одна из первых вещей, которые вы обнаружите в файле, который экспортирует connect это комментарий «connect — это фасад для connectAdvanced». Уже на этом этапе мы можем кое-чему поучиться: у нас есть возможность наблюдать паттерн «фасад» в действии. В конце файла мы видим, что connect экспортирует вызов функции createConnect. Ее параметры — набор значений по умолчанию, которые деструктуризируются следующим образом:

export function createConnect({
 connectHOC = connectAdvanced,
 mapStateToPropsFactories = defaultMapStateToPropsFactories,
 mapDispatchToPropsFactories = defaultMapDispatchToPropsFactories,
 mergePropsFactories = defaultMergePropsFactories,
 selectorFactory = defaultSelectorFactory
} = {})

И у нас есть еще один поучительный момент: экспорт вызываемой функции и деструктуризация аргументов функции по умолчанию. Деструктуризация поучительна для нас потому, что код мог бы быть написан так:

export function createConnect({
 connectHOC = connectAdvanced,
 mapStateToPropsFactories = defaultMapStateToPropsFactories,
 mapDispatchToPropsFactories = defaultMapDispatchToPropsFactories,
 mergePropsFactories = defaultMergePropsFactories,
 selectorFactory = defaultSelectorFactory
})

В результате мы получили бы ошибку — Uncaught TypeError: Cannot destructure property 'connectHOC' of 'undefined' or 'null'. Это бы произошло потому, что у функции нет значений аргументов по умолчанию.

Примечание: чтобы лучше понять деструктуризацию аргументов, вы можете прочесть статью David Walsh. Некоторые моменты могут показаться тривиальными, в зависимости от вашего знания языка — тогда вы можете сосредоточиться на тех моментах, с которыми не знакомы.

Сама по себе функция createConnect ничего не делает. Она просто возвращает функцию connect, которую я использовал здесь:

export default connect(null, mapDispatchToProps)(MarketContainer)

Она принимает четыре опциональных аргумента и первые три из них проходят через функцию match, которая помогает определить их поведение исходя из того, какие аргументы переданы, а также их типа. Выходит, раз второй аргумент, переданный в match — одна из трех функций, импортированных в connect, мне нужно выбрать, куда двигаться дальше.

Есть что почерпнуть также и из прокси-функции, используемой для обертки первого аргумента в connect, если эти аргументы — функции; из утилиты isPlainObject, используемой для проверки plain objects или из модуля warning, который показывает, как можно сделать дебаггер, который будет ломаться на всех ошибках. После функции match мы переходим к connectHOC — функции, которая берет наш react-компонент и связывает его с redux. Есть еще один вызов функции, возвращающий wrapWithConnect — функцию, которая фактически обрабатывает связывание компонента с хранилищем.

Глядя на имплементацию connectHOC я могу предположить, почему детали реализации connect должны быть скрыты. Это по сути сердце react-redux и содержит логику, которая не должна быть доступная через connect. Даже если остановиться на этом, то в последствии, если нужно будет копнуть глубже — у нас уже будет исходный материал с детальным пояснением кода.

Итоги подведем


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

Например, я посчитал функцию isPlainObject интересной, потому что она использует это if (typeof obj !== 'object' || obj === null) return false для того, чтобы убедиться, что переданный аргумент является простым объектом. Когда я впервые прочел этот код, то подумал, почему бы просто не использовать Object.prototype.toString.call(opts) !== '[object Object]', что позволило бы сократить код и отделить объекты от их подтипов вроде Date. Но уже в следующей строчке видно, что даже если вдруг (внезапно!) разработчик, использующий connect вернет объект Date, например, проверка Object.getPrototypeOf(obj) === null с этим справится.

Еще один неожиданный момент в isPlainObject в этом месте:

while (Object.getPrototypeOf(baseProto) !== null) {
 baseProto = Object.getPrototypeOf(baseProto)
}

Поиск ответа в Гугл привел меня в эту ветку на StackOverflow, и к этому комментарию на GitHub`е Redux, где поясняется, как этот код обрабатывает ситуации, когда, например, объект передан из iFrame.



Впервые решил перевести статью. Буду признателен за уточнения, советы и рекомендации
Tags:
Hubs:
Total votes 13: ↑13 and ↓0+13
Comments10

Articles