Совершенствуйте свои знания JavaScript, читая исходный код
Опубликовано: 2022-03-10Вы помните, как впервые углубились в исходный код часто используемой библиотеки или фреймворка? Для меня этот момент наступил во время моей первой работы фронтенд-разработчиком три года назад.
Мы только что закончили переписывать внутреннюю унаследованную структуру, которую использовали для создания курсов электронного обучения. В начале переписывания мы потратили время на изучение ряда различных решений, включая Mithril, Inferno, Angular, React, Aurelia, Vue и Polymer. Поскольку я был совсем новичком (я только что переключился с журналистики на веб-разработку), я помню, как меня пугала сложность каждого фреймворка, и я не понимал, как каждый из них работает.
Мое понимание возросло, когда я начал более глубоко исследовать выбранный нами фреймворк Mithril. С тех пор моим знаниям JavaScript и программирования в целом очень помогли часы, которые я провел, глубоко копаясь во внутренностях библиотек, которые я ежедневно использую на работе или в своих собственных проектах. В этом посте я поделюсь некоторыми способами, которыми вы можете взять свою любимую библиотеку или фреймворк и использовать ее в качестве образовательного инструмента.
Преимущества чтения исходного кода
Одним из основных преимуществ чтения исходного кода является множество вещей, которые вы можете узнать. Когда я впервые заглянул в кодовую базу 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, которая определяет, как работает язык. Впервые я прочитал спецификацию, когда исследовал разницу между 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
.
Практический пример: функция 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