Popraw swoją wiedzę o JavaScript, czytając kod źródłowy

Opublikowany: 2022-03-10
Szybkie podsumowanie ↬ Kiedy jesteś jeszcze na początku swojej kariery programistycznej, zagłębianie się w kod źródłowy bibliotek i frameworków open source może być trudnym przedsięwzięciem. W tym artykule Carl Mungazi opowiada, jak przezwyciężył strach i zaczął używać kodu źródłowego, aby poszerzyć swoją wiedzę i umiejętności. Używa również Redux, aby zademonstrować, jak podchodzi do niszczenia biblioteki.

Czy pamiętasz, kiedy po raz pierwszy zagłębiłeś się w kod źródłowy biblioteki lub frameworka, z którego często korzystasz? Dla mnie ten moment nadszedł podczas mojej pierwszej pracy jako frontend developer trzy lata temu.

Właśnie skończyliśmy przepisywać wewnętrzny framework, którego używaliśmy do tworzenia kursów e-learningowych. Na początku przepisywania spędziliśmy czas na badaniu wielu różnych rozwiązań, w tym Mithril, Inferno, Angular, React, Aurelia, Vue i Polymer. Ponieważ byłem bardzo początkującym (właśnie przerzuciłem się z dziennikarstwa na tworzenie stron internetowych), pamiętam, że czułem się onieśmielony złożonością każdego frameworka i nie rozumiałem, jak każdy z nich działa.

Moje zrozumienie wzrosło, gdy zacząłem dokładniej badać wybrany przez nas framework, Mithril. Od tego czasu moją wiedzę o JavaScript — i ogólnie o programowaniu — bardzo pomogły godziny, które spędziłem głęboko zagłębiając się w biblioteki, których używam na co dzień w pracy lub we własnych projektach. W tym poście podzielę się kilkoma sposobami, w jakie możesz wykorzystać swoją ulubioną bibliotekę lub framework i wykorzystać je jako narzędzie edukacyjne.

Kod źródłowy funkcji hiperkryptu Mithrila
Moje pierwsze wprowadzenie do czytania kodu odbyło się za pomocą funkcji hiperskryptu Mithril. (duży podgląd)
Więcej po skoku! Kontynuuj czytanie poniżej ↓

Korzyści z czytania kodu źródłowego

Jedną z głównych zalet czytania kodu źródłowego jest liczba rzeczy, których możesz się nauczyć. Kiedy po raz pierwszy zajrzałem do bazy kodu Mithril, miałem mgliste pojęcie o tym, czym jest wirtualny DOM. Kiedy skończyłem, wyszedłem ze świadomością, że wirtualny DOM to technika polegająca na tworzeniu drzewa obiektów opisujących, jak powinien wyglądać Twój interfejs użytkownika. To drzewo jest następnie przekształcane w elementy DOM przy użyciu interfejsów API DOM, takich jak document.createElement . Aktualizacje wykonywane są poprzez utworzenie nowego drzewa opisującego przyszły stan interfejsu użytkownika, a następnie porównanie go z obiektami ze starego drzewa.

Czytałem o tym wszystkim w różnych artykułach i samouczkach i chociaż było to pomocne, możliwość obserwowania tego w pracy w kontekście wysłanej przez nas aplikacji była dla mnie bardzo pouczająca. Nauczył mnie także, jakie pytania zadać, porównując różne frameworki. Zamiast na przykład patrzeć na gwiazdy GitHub, teraz wiedziałem, jak zadawać pytania takie jak: „W jaki sposób sposób, w jaki każda platforma wykonuje aktualizacje, wpływa na wydajność i wrażenia użytkownika?”

Kolejną korzyścią jest wzrost Twojego uznania i zrozumienia dobrej architektury aplikacji. Podczas gdy większość projektów open-source na ogół ma tę samą strukturę ze swoimi repozytoriami, każdy z nich zawiera różnice. Struktura Mithrila jest dość płaska i jeśli znasz jego API, możesz zgadywać kod w folderach takich jak render , router i request . Z drugiej strony struktura Reacta odzwierciedla jego nową architekturę. Opiekunowie oddzielili moduł odpowiedzialny za aktualizacje interfejsu użytkownika ( react-reconciler ) od modułu odpowiedzialnego za renderowanie elementów DOM ( react-dom ).

Jedną z korzyści płynących z tego jest to, że programiści mogą teraz łatwiej pisać własne, niestandardowe renderery, podłączając się do pakietu react-reconciler . Parcel, pakiet modułów, który ostatnio studiowałem, ma również folder packages , taki jak React. Kluczowy moduł nosi nazwę parcel-bundler i zawiera kod odpowiedzialny za tworzenie pakietów, uruchamianie serwera hot module oraz narzędzie wiersza poleceń.

Sekcja specyfikacji JavaScript, która wyjaśnia, jak działa Object.prototype.toString
Już niedługo kod źródłowy, który czytasz, doprowadzi Cię do specyfikacji JavaScript. (duży podgląd)

Kolejną korzyścią — która była dla mnie mile widzianą niespodzianką — jest to, że możesz wygodniej czytać oficjalną specyfikację JavaScript, która definiuje sposób działania języka. Po raz pierwszy przeczytałem specyfikację, gdy badałem różnicę między throw Error i throw new Error (ostrzeżenie spoilera — nie ma). Przyjrzałem się temu, ponieważ zauważyłem, że Mithril użył throw Error w implementacji swojej funkcji m i zastanawiałem się, czy jest korzyść z używania go w stosunku do throw new Error . Od tego czasu nauczyłem się również, że operatory logiczne && oraz || niekoniecznie zwracają wartości logiczne, znaleziono reguły regulujące sposób, w jaki == operator równości wymusza wartości i powód, dla którego Object.prototype.toString.call({}) zwraca '[object Object]' .

Techniki czytania kodu źródłowego

Istnieje wiele sposobów podejścia do kodu źródłowego. Znalazłem najłatwiejszy sposób na rozpoczęcie od wybrania metody z wybranej biblioteki i udokumentowania tego, co się dzieje, gdy ją wywołasz. Nie dokumentuj każdego kroku, ale spróbuj określić jego ogólny przepływ i strukturę.

Zrobiłem to ostatnio z ReactDOM.render i dzięki temu dowiedziałem się wiele o React Fiber i niektórych przyczynach jego wdrożenia. Na szczęście, ponieważ React jest popularnym frameworkiem, natknąłem się na wiele artykułów napisanych przez innych programistów na ten sam temat, co przyspieszyło cały proces.

To głębokie zanurzenie wprowadziło mnie również w koncepcje planowania kooperacyjnego, metodę window.requestIdleCallback i przykład z prawdziwego świata połączonych list (React obsługuje aktualizacje, umieszczając je w kolejce, która jest połączoną listą priorytetowych aktualizacji). W tym celu warto stworzyć bardzo podstawową aplikację korzystającą z biblioteki. Ułatwia to debugowanie, ponieważ nie musisz zajmować się śladami stosu spowodowanymi przez inne biblioteki.

Jeśli nie robię szczegółowej recenzji, otworzę folder /node_modules w projekcie, nad którym pracuję lub przejdę do repozytorium GitHub. Zwykle dzieje się tak, gdy natrafię na błąd lub interesującą funkcję. Czytając kod na GitHub, upewnij się, że czytasz z najnowszej wersji. Możesz wyświetlić kod z zatwierdzeń z tagiem najnowszej wersji, klikając przycisk służący do zmiany gałęzi i wybierając „tagi”. Biblioteki i frameworki podlegają ciągłym zmianom, więc nie chcesz dowiadywać się o czymś, co może zostać porzucone w następnej wersji.

Innym mniej skomplikowanym sposobem odczytywania kodu źródłowego jest metoda, którą lubię nazywać „pobieżnym spojrzeniem”. Na początku, kiedy zacząłem czytać kod, zainstalowałem express.js , otworzyłem jego folder /node_modules i przejrzałem jego zależności. Jeśli README nie dostarczyło mi zadowalającego wyjaśnienia, przeczytałem źródło. Zrobienie tego doprowadziło mnie do tych interesujących ustaleń:

  • Express opiera się na dwóch modułach, które łączą obiekty, ale robią to na bardzo różne sposoby. merge-descriptors dodaje tylko właściwości znalezione bezpośrednio w obiekcie źródłowym, a także łączy właściwości niewyliczalne, podczas gdy utils-merge iteruje tylko po właściwościach wyliczalnych obiektu, a także tych znalezionych w łańcuchu prototypów. merge-descriptors używa Object.getOwnPropertyNames() i Object.getOwnPropertyDescriptor() podczas gdy utils-merge używa for..in ;
  • Moduł setprototypeof zapewnia wieloplatformowy sposób ustawiania prototypu obiektu;
  • escape-html to 78-wierszowy moduł do ucieczki ciągu treści, aby mógł być interpolowany w treści HTML.

Chociaż wyniki prawdopodobnie nie będą przydatne od razu, przydatne jest ogólne zrozumienie zależności używanych przez bibliotekę lub platformę.

Jeśli chodzi o debugowanie kodu front-endu, narzędzia do debugowania Twojej przeglądarki są Twoim najlepszym przyjacielem. Między innymi pozwalają zatrzymać program w dowolnym momencie i sprawdzić jego stan, pominąć wykonanie funkcji lub wejść lub wyjść z niej. Czasami nie będzie to możliwe od razu, ponieważ kod został skrócony. Staram się go unminify i skopiować niezminifikowany kod do odpowiedniego pliku w folderze /node_modules .

Kod źródłowy funkcji ReactDOM.render
Podejdź do debugowania tak, jak do każdej innej aplikacji. Sformułuj hipotezę, a następnie ją przetestuj. (duży podgląd)

Studium przypadku: funkcja Connect w Redux

React-Redux to biblioteka służąca do zarządzania stanem aplikacji React. Mając do czynienia z takimi popularnymi bibliotekami, jak te, zaczynam od wyszukania artykułów, które zostały napisane na temat ich implementacji. Robiąc to w tym studium przypadku, natknąłem się na ten artykuł. To kolejna dobra rzecz w czytaniu kodu źródłowego. Faza badawcza zwykle prowadzi do artykułów informacyjnych, takich jak ten, które tylko poprawiają twoje myślenie i rozumienie.

connect to funkcja React-Redux, która łączy komponenty React ze sklepem Redux aplikacji. W jaki sposób? Cóż, zgodnie z dokumentacją wykonuje następujące czynności:

„...zwraca nową, połączoną klasę komponentu, która otacza przekazany komponent.”

Po przeczytaniu tego zadałbym następujące pytania:

  • Czy znam jakieś wzorce lub koncepcje, w których funkcje pobierają dane wejściowe, a następnie zwracają te same dane wejściowe z dodatkową funkcjonalnością?
  • Jeśli znam takie wzorce, w jaki sposób zaimplementowałbym to w oparciu o wyjaśnienie podane w dokumentach?

Zwykle następnym krokiem byłoby stworzenie bardzo prostej przykładowej aplikacji, która używa connect . Jednak tym razem zdecydowałem się użyć nowej aplikacji React, którą budujemy w Limejump, ponieważ chciałem zrozumieć connect w kontekście aplikacji, która ostatecznie trafi do środowiska produkcyjnego.

Komponent, na którym się skupiam, wygląda tak:

 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);

Jest to komponent kontenerowy, który owija cztery mniejsze połączone komponenty. Jedną z pierwszych rzeczy, które napotkasz w pliku eksportującym metodę connect jest ten komentarz: connect to fasada nad connectAdvanced . Nie posuwając się daleko mamy swój pierwszy moment nauki: możliwość obserwowania wzorca projektowego elewacji w działaniu . Na końcu pliku widzimy, że connect eksportuje wywołanie funkcji o nazwie createConnect . Jego parametry to zbiór domyślnych wartości, które zostały zdestrukturyzowane w następujący sposób:

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

Ponownie natrafiamy na kolejny moment nauki: eksportowanie wywołanych funkcji i destrukturyzację domyślnych argumentów funkcji . Część destrukturyzująca jest momentem nauki, ponieważ kod został napisany w ten sposób:

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

Spowodowałoby to ten błąd Uncaught TypeError: Cannot destructure property 'connectHOC' of 'undefined' or 'null'. Dzieje się tak, ponieważ funkcja nie ma domyślnego argumentu, na który można by się odwołać.

Uwaga : Więcej informacji na ten temat można znaleźć w artykule Davida Walsha. Niektóre momenty nauki mogą wydawać się trywialne, w zależności od Twojej znajomości języka, dlatego może lepiej skupić się na rzeczach, których wcześniej nie widziałeś lub o których musisz dowiedzieć się więcej.

Sam createConnect nie robi nic w swojej treści funkcji. Zwraca funkcję o nazwie connect , której użyłem tutaj:

 export default connect(null, mapDispatchToProps)(MarketContainer)

Pobiera cztery argumenty, wszystkie opcjonalne, a pierwsze trzy argumenty przechodzą przez funkcję match , która pomaga zdefiniować ich zachowanie w zależności od tego, czy argumenty są obecne i ich typ wartości. Teraz, ponieważ drugi argument dostarczony do match jest jedną z trzech funkcji zaimportowanych do connect , muszę zdecydować, w którym wątku podążać.

Są momenty nauki z funkcją proxy używaną do zawijania pierwszego argumentu w celu connect , jeśli te argumenty są funkcjami, narzędziem isPlainObject używanym do sprawdzania zwykłych obiektów lub modułem warning , który pokazuje, jak ustawić debuger tak, aby przerywał wszystkie wyjątki. Po dopasowaniu funkcji dochodzimy do connectHOC , funkcji, która pobiera nasz komponent React i łączy go z Redux. Jest to kolejne wywołanie funkcji, które zwraca wrapWithConnect , funkcję, która faktycznie obsługuje połączenie komponentu ze sklepem.

Patrząc na connectHOC , rozumiem, dlaczego potrzebuje ona connect , aby ukryć szczegóły implementacji. Jest sercem React-Redux i zawiera logikę, której nie trzeba ujawniać przez connect . Nawet jeśli zakończę tutaj głębokie nurkowanie, gdybym kontynuował, byłby to idealny czas na zapoznanie się z materiałem referencyjnym, który znalazłem wcześniej, ponieważ zawiera on niezwykle szczegółowe wyjaśnienie bazy kodu.

Streszczenie

Czytanie kodu źródłowego jest początkowo trudne, ale jak ze wszystkim, z czasem staje się łatwiejsze. Celem nie jest zrozumienie wszystkiego, ale wyjście z inną perspektywą i nową wiedzą. Kluczem do sukcesu jest bycie świadomym całego procesu i intensywną ciekawość wszystkiego.

Na przykład uważam, że funkcja isPlainObject jest interesująca, ponieważ używa tego if (typeof obj !== 'object' || obj === null) return false , aby upewnić się, że podany argument jest zwykłym obiektem. Kiedy pierwszy raz przeczytałem jego implementację, zastanawiałem się, dlaczego nie używa Object.prototype.toString.call(opts) !== '[object Object]' , który jest mniej kodu i rozróżnia obiekty i podtypy obiektów, takie jak Date obiekt. Jednak czytanie następnej linii ujawniło, że w bardzo mało prawdopodobnym przypadku, gdy programista używający connect zwróci na przykład obiekt Date, zostanie to obsłużone przez Object.getPrototypeOf(obj) === null .

Kolejną intrygą w isPlainObject jest ten kod:

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

Niektóre wyszukiwania Google doprowadziły mnie do tego wątku StackOverflow i problemu Redux wyjaśniającego, w jaki sposób ten kod obsługuje przypadki, takie jak sprawdzanie obiektów, które pochodzą z iFrame.

Przydatne linki do czytania kodu źródłowego

  • „Jak odwrócić struktury inżynierskie”, Max Koretskyi, Medium
  • „Jak czytać kod”, Aria Stewart, GitHub