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

Опубликовано: 2022-03-10
Краткое резюме ↬ Когда вы только начинаете свою карьеру программиста, копание в исходном коде библиотек и фреймворков с открытым исходным кодом может оказаться непростой задачей. В этой статье Карл Мунгази рассказывает, как он преодолел свой страх и начал использовать исходный код для улучшения своих знаний и навыков. Он также использует Redux, чтобы продемонстрировать, как он подходит к разбору библиотеки.

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

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

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

Исходный код функции гиперскрипта Mithril
Мое первое знакомство с чтением кода произошло через функцию гиперскрипта Mithril. (Большой превью)
Еще после прыжка! Продолжить чтение ниже ↓

Преимущества чтения исходного кода

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

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

Еще одним преимуществом является повышение вашей оценки и понимания хорошей архитектуры приложений. Хотя большинство проектов с открытым исходным кодом обычно имеют одинаковую структуру со своими репозиториями, каждый из них содержит различия. Структура Mithril довольно плоская, и если вы знакомы с его API, вы можете сделать обоснованные предположения о коде в таких папках, как render , router и request . С другой стороны, структура React отражает его новую архитектуру. Мейнтейнеры отделили модуль, отвечающий за обновления пользовательского интерфейса ( react-reconciler ), от модуля, отвечающего за рендеринг элементов DOM ( react-dom ).

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

Раздел спецификации JavaScript, в котором объясняется, как работает Object.prototype.toString.
Пройдет совсем немного времени, прежде чем исходный код, который вы читаете, приведет вас к спецификации 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 является популярным фреймворком, я наткнулся на множество статей, написанных другими разработчиками по той же проблеме, и это ускорило процесс.

Это глубокое погружение также познакомило меня с концепцией совместного планирования, методом 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 .

Исходный код функции ReactDOM.render
Подходите к отладке так же, как к любому другому приложению. Сформулируйте гипотезу, а затем проверьте ее. (Большой превью)

Практический пример: функция Connect в Redux

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

connect — это функция React-Redux, которая соединяет компоненты React с хранилищем Redux приложения. Как? Ну, согласно документам, он делает следующее:

«... возвращает новый связанный класс компонента, который является оболочкой для компонента, который вы передали».

Прочитав это, я бы задал следующие вопросы:

  • Известны ли мне какие-либо шаблоны или концепции, в которых функции принимают входные данные, а затем возвращают те же входные данные, но с дополнительными функциями?
  • Если я знаю какие-либо такие шаблоны, как бы я реализовал это на основе объяснений, приведенных в документах?

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

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

 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'. Это связано с тем, что функция не имеет аргумента по умолчанию, на который можно было бы вернуться.

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

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

 export default connect(null, mapDispatchToProps)(MarketContainer)

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

Есть моменты обучения с прокси-функцией, используемой для переноса первого аргумента для connect , если эти аргументы являются функциями, утилитой isPlainObject , используемой для проверки простых объектов, или модулем warning , который показывает, как вы можете настроить свой отладчик для прерывания всех исключений. После функций сопоставления мы переходим к функции 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) }

Некоторые поиски в Google привели меня к этому потоку StackOverflow и проблеме Redux, объясняющей, как этот код обрабатывает такие случаи, как проверка объектов, которые происходят из iFrame.

Полезные ссылки по чтению исходного кода

  • «Как перепроектировать фреймворки», Макс Корецкий, Medium
  • «Как читать код», Ария Стюарт, GitHub