Improve your JavaScript knowledge by parsing source code

Original author: Carl Mungazi
  • Transfer
When you start your programming career, digging in the source code of open libraries and frameworks may seem a little scary. In this article, Karl Mungazi shares his experience of how he overcome his fear and began to use the source code to acquire knowledge and develop skills. He also uses Redux to show how he “parses” the library.

Do you remember when you first immersed yourself in the code of a library or framework that you often use? In my life, this moment came at my first job as a front-end developer three years ago.

We just rewrote an outdated proprietary framework that was used to create interactive training courses. At the very beginning of the rewriting work, we looked at some turnkey solutions, including Mithril, Inferno, Angular, React, Aurelia, Vue, and Polymer. Since I was still a young Padawan (who had just switched from journalism to web development), I was terribly afraid of the complexity of each framework and the lack of understanding of how they work.

Understanding began to come when I began to carefully explore the Mithril framework. Since then, my knowledge of JavaScript - and programming in general - has been greatly strengthened thanks to the hours spent digging in the library internals, which I used daily at work and in my own projects. In this article I will tell you how to use your favorite library as a tutorial.

image
I started reading the source code with the hyperscript function from Mithril

Pros of parsing source code


One of the main advantages of parsing source code is that you can learn a lot. When I started parsing the Mithril code, I had a very poor idea of ​​what the virtual DOM was. When I finished, I already knew that the virtual DOM is a technique involving the creation of a tree of objects that describe the user interface. This tree can then be converted to DOM elements using a DOM API like document.createElement. To update, a new tree is created that describes the future state of the interface and then compared with the previous version of this tree.

I read about this in many articles and manuals, but the most instructive was to observe all this while working on our application. I also learned to ask the right questions when comparing frameworks. Instead of comparing ratings, for example, you can ask the question “How does the way this framework works with changes affect the performance and convenience of the end user?”

Another plus is the development of an understanding of a good application architecture. Despite the fact that most open-source projects are generally more or less similar in structure to their repositories, they still have differences. The structure of Mithril is very flat and if you are well versed in its API, you can make quite realistic assumptions about the code in the render, router and request folders. React’s structure, on the other hand, reflects its new architecture. The developers separated the module responsible for updating the UI (react-reconciler) from the module responsible for rendering the DOM elements (react-dom).

One of the advantages of this separation for developers is that they can write their own renderers.using hooks in react-reconciler. Parcel, the module builder I recently studied, also has a packages folder, just like React. The key module is called parcel-bundler, it contains code that is responsible for creating assemblies, the operation of the module update server (hot module server), and the command line tool.

image
Parsing the source code soon leads you to read the JavaScript specification.

Another plus, which was a big surprise for me, is that it makes it easier for you to read the official JavaScript specification. The first time I turned to her when I was trying to figure out the difference between throw Error and throw new Error (spoiler is nothing) I asked this question because Mithril used throw Error in the implementation of the m function and I wondered why it was better than throw new Error. Then I also learned that the operators && and || not necessarily return Boolean values , I found the rules by which the operator of non-strict comparison == “resolves” the values ​​and the reason why Object.prototype.toString.call ({}) returns '[object Object]'.

How to parse the source code


There are many ways to parse source code. The easiest way seems to me the following: select a method from your library and describe what happens when you call it. It is not worth describing each step, you just need to try to understand its general principles and structure.

Recently, I parsed ReactDOM.render this way and learned a lot about React Fiber and some of the difficulties in implementing it. Fortunately, React is very popular and the presence of a large number of articles on the same topic from other developers accelerated the process.

This dive into the code also introduced me to the concept of co-operative scheduling , the window.requestIdleCallback method , and a live example of a linked list.(React processes updates by sending them to the queue, which is a linked list of updates with priorities). In the process, it would be nice to create a simple application using the library. This makes debugging easier since you don’t have to deal with the stack trace of other libraries.

If I do not do a detailed review, then I will open the node_modules folder in the project I'm working on or take a look at GitHub. I always do this when I come across a bug or an interesting feature. When reading the code on GitHub, make sure that this is the latest version. The code of the latest version can be seen by clicking on the button for changing branches and selecting "tags". Changes in libraries and frameworks are ongoing, so you are unlikely to want to parse something that might not be in the next version.

A more superficial version of learning source code is what I call a “quick look”. Somehow I installed express.js, opened the node_modules folder and went through the dependencies. If README did not give me a satisfactory explanation, I read the source. This led me to interesting discoveries:

  • Express uses two modules to merge objects, and the operation of these modules is very different. merge-descriptors only adds the properties found in the source object, and also adds non-enumerable properties, while utils-merge goes over the enumerated properties of the object and its entire prototype chain. merge-descriptors uses Object.getOwnPropertyNames () and Object.getOwnPropertyDescriptor (), and utils-merge uses for..in;
  • The setprototypeof module provides a cross-platform option for specifying the prototype of the created (instantiated) object;
  • escape-html is a 78-line string escaping module, after which the content can be inserted into HTML;

Although these discoveries are most likely not useful right away, a general understanding of the dependencies of your library or framework is very helpful.

Debugging browser tools are your best friends when debugging code on the frontend. Among other things, they allow you to stop the program at any time and check at the same time its status, skip the function, or go inside or exit it. In minified code, this is not possible - that's why I unpack this code and put it in the corresponding file in the node_modules folder.

image
Use the debugger as a useful application. Make an assumption, and then test it.

Case study: connect function in Redux


React-Redux is a library for managing the state of React applications. When I work with popular libraries like this, I start by looking for articles about their use. In preparing this example, I reviewed this article . This is another plus point in learning the source code - it leads you to informative articles like this one that improve your thinking and understanding.

Connect is a react-redux function that links the react component and the redux store of an application. How? According to the documentation, she does the following:
“... returns a new, related component class, which is a wrapper of the component passed to it.”
After reading this, I ask the following questions:

  • Do I know patterns or concepts where functions return input parameters wrapped with additional functionality?
  • If so, how do I use this based on the description from the documentation?

Usually the next step is to create a primitive application using the connect function. Nevertheless, in this situation, I used a new application on React, which we worked on, because I wanted to understand connect in the context of an application that would most likely soon hit production.

The component I focused on looks something like this:

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

This is a container component that serves as a wrapper for the four smaller related components. One of the first things you find in the file that connect exports is the comment “connect is the facade for connectAdvanced”. Already at this stage we can learn something: we have the opportunity to observe the “facade” pattern in action. At the end of the file, we see connect exporting a call to the createConnect function. Its parameters are a set of default values ​​that are destructurized as follows:

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

And we have one more instructive moment: export of the called function and destructuring of the function arguments by default. Restructuring is instructive for us because the code could be written like this:

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

As a result, we would get an error - Uncaught TypeError: Cannot destructure property 'connectHOC' of 'undefined' or 'null'. This would happen because the function has no default argument values.

Note: to better understand the restructuring of arguments, you can read David Walsh's article . Some points may seem trivial, depending on your knowledge of the language - then you can focus on those points that you are not familiar with.

The createConnect function itself does nothing. It just returns the connect function that I used here:

export default connect(null, mapDispatchToProps)(MarketContainer)

It takes four optional arguments and the first three of them pass through the match function , which helps determine their behavior based on which arguments are passed, as well as their type. It turns out that since the second argument passed to match is one of the three functions imported into connect, I need to choose where to go next.

There is also something to learn from the proxy function used to wrap the first argument in connect, if these arguments are functions; from the isPlainObject utility used to check plain objects or from the warning module, which shows how you can make a debugger that will break on all errors. After the match function, we move on to connectHOC, the function that takes our react component and associates it with redux. There is another function call that returns wrapWithConnect - a function that actually handles the binding of the component to the repository.

Looking at the connectHOC implementation, I can guess why the details of the connect implementation should be hidden. This is essentially the heart of react-redux and contains logic that should not be accessible through connect. Even if we dwell on this, then later, if we need to dig deeper, we will already have the source material with a detailed explanation of the code.

Summarize


Learning the source code is very complicated at first. But, like everything else, it becomes easier over time. His task is not to understand everything, but to bring out something useful for himself - a common understanding and new knowledge. It is very important to be careful during the whole process and delve into the details.

For example, I found the isPlainObject function interesting because it uses this if (typeof obj! == 'object' || obj === null) return false to ensure that the argument passed is a simple object. When I first read this code, I thought, why not just use Object.prototype.toString.call (opts)! == '[object Object]', which would reduce the code and separate objects from their subtypes like Date. But already in the next line it is clear that even if suddenly (suddenly!) A developer using connect returns a Date object, for example, checking Object.getPrototypeOf (obj) === null can handle this.

Another unexpected point in isPlainObject in this place:

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

Finding an answer on Google led me to this thread on StackOverflow, and to this comment on GitHub's Redux, which explains how this code handles situations where, for example, an object is transferred from an iFrame.

-

For the first time I decided to translate the article. I would be grateful for clarification, advice and recommendations

Also popular now: