preface

AngularJS has slowly faded out of existence, but as the first generation OF MVVM frameworks, AngularJS has provided many functional design references for subsequent frameworks. Examples include reactive state, dependency injection, and modular development.

AngularJS source code is hard to read, not because the code is poorly written, but because the internal functions are so heavily abstracting and encapsulated by functional programming that most functions are nested at such a deep level that it takes a lot of debugging and tracking to really see what’s going on underneath.

This paper will extract the core logic of the framework to explain, along the top-down process from the overall grasp of how the framework is designed and implemented. From the AngularJS implementation we can also get a general idea of what features a front-end framework contains and how they are implemented.

Quickly review

Here’s a quick way to get familiar with using the AngularJS framework by writing two simple files in one.

index.html

<! DOCTYPE html> <html lang="en"> <head> <script src="./lib/angular.js"></script> <script src="./lib/main.js"></script> </head> <body ng-app="myapp"> <div ng-controller="contrl"> <div ng-click="clickHanlder()">{{name}}</div> </div> </body> </html>Copy the code

Ng-app defines the module name, and ng-controller defines the controller. Also bind the div with a clickHanlder click event and render the state name.

main.js

angular.module('myapp', []).controller('contrl', function ($scope) { $scope.name = 'hello world'; $scope.clickHanlder = function () { $scope.name = $scope.name + '! '; }; });Copy the code

Call angular.module to define the module myApp with the same name as defined in the template above. Next, define controller Contrl, again matching the name on the template. Define a state name and event clickHanlder in the controller function.

Refresh the browser after the above configuration, and the interface will render Hello World. If you click on the copy, it triggers a status update and the interface changes to Hello World! .

Angular defines a module called MyApp, which takes over the DOM portion of the page in the index.html template that uses the ng-app definition. We then define a controller contrl under this module, which in turn takes over the page DOM of the controller of the same name under the MyApp module.

A state name and event clickHanlder are defined in the controller. The state name value hello World directly renders the {{name}} defined on the template. When the user clicks on the copy, a change in the value of name is triggered, which in turn automatically triggers a re-rendering of the page.

Loading process

var angular = window.angular || (window.angular = {}); //1. Add properties and methods to the Angular object, and finally mount a module function that creates the module publishExternalAPI(angular); //2. Execute the front-end js file content,angular.module(...) .contoller(...) // When the interface is loaded, execute the following code, which searches the DOM of the document with the ng-app tag. This DOM is called Document angularInit(Document, bootstrap); Angular defines the module to take over the Element, which corresponds to the document bootstrap(Element,modules) in step 3. Invoke (['$rootScope', '$rootElement', '$compile', '$injector', function bootstrapApply(scope, element, compile, injector) { ... }, ]); Function bootstrapApply(scope, element, compile, injector) { scope.$apply(function () { element.data('$injector', injector); compile(element)(scope); }); $rootScope.$digest();Copy the code

An Angular object is first defined and exposed for global use. You then add various properties and methods to the object, including a.module() method that you run to create modules.

At this point, the dom of the page has not finished loading, and the main.js file written by the front end will be executed first. By running angular.module(…) .contoller(…) The code collects the defined code content.

After the PAGE’S DOM loads, Angular looks for the part of the page’S DOM that defines the Ng-app. The next task is to have the module defined by main.js manage the module corresponding to the page DOM.

Angular starts scanning the DOM to initiate compilation and linking of page content. So as to achieve the purpose of module object management and control page.

Finally, start dirty check to trigger page rendering, and render all state data defined by the controller to the page for display.

Dependency injection

Dependency injection and custom directives run through the Angular framework globally. The Angular framework is a granular aggregation. All the small independent functions are written as small services. A service is an object that contains properties and methods for a particular function. The case is as follows.

angular.module('myapp', []).controller('contrl', function ($scope,$http) { $scope.name = 'hello world'; $scope.clickHanlder = function () { $http.get("http://www.xxx.com/api/get_list").success( function(data){ Console. log(data)// Get interface data})}; });Copy the code

The $HTTP in the above code is a service that contains many methods to interact with the back end, such as POST or GET. To use the service, add the $HTTP parameter to the function to retrieve the service and use it in the callback function.

Services are small modules with independent functions, and the dependency injection mechanism is very convenient for injecting any service we want to use. For example, if we change $HTTP to $window, then we can use the properties and methods contained in the $window service in the function. Services such as $HTTP and $window are defined and maintained in a separate area of the globe, effectively decoupling the functional code from the business code. All functional code is defined separately and maintained separately. Business code that wants to use a service can use dependency injection directly.

Dependency injection mechanism makes the entire code structure change very flexible, the following uses $HTTP as an example to explore the entire dependency injection implementation.

The first step is to define globally a service provider $HttpProvider that interacts with the back end, from which the $HTTP service can be generated.

Function $HttpProvider() {var defaults = {headers: {common: {Accept: 'application/json, text/plain, */*', }, post: shallowCopy(CONTENT_TYPE_APPLICATION_JSON), }, xsrfCookieName: 'XSRF-TOKEN' }; this.$get = [ '$httpBackend', '$browser', '$cacheFactory', '$rootScope', '$q', '$injector', function ( $httpBackend, $browser, $cacheFactory, $rootScope, $q, $injector) {return $HTTP;}Copy the code

If we look closely at the service provider defined above, we can easily see that this.$get is an array. The first few elements of this array are also services, and the last element is a function.

The function placed at the bottom of the array is the one that generates the $HTTP service. So why are there other services in front of the array? This is where dependency injection comes in again.

$HttpProvider () {$HttpProvider () {$HttpProvider () {$HttpProvider () {$HttpProvider () {$HttpProvider () {$HttpProvider () {$HttpProvider () {$HttpProvider (); Dependency injection allows the service provider to inject other services so that the code for each service can be granulated as much as possible and maintained independently.

This.$get function returns $HTTP object. This.$get function returns $HTTP object. The above code is just a static data structure. The next step is to implement the procedure described above and return a $HTTP object.

Go back to step 5 of the loading process above to create an injector that will fetch a service instance.

//modulesToLoad = ["ng",'$provide', function ($provide) {},"myApp"] const injector = createInjector(modulesToLoad); // Generate an injectorCopy the code

The createInjector function has been used to generate an injector and is the core logic for dependency injection. Internally, the createInternalInjecto method has been called twice.

The first invocation defines a providerCache object dedicated to global service providers, at which point Angular registers globally defined providers such as $HttpProvider to providerCache. Here’s the code.

$provide.provider({
  ...
  $http: $HttpProvider
})
Copy the code

The second call defines the instanceCache object that stores the service objects that have already been instantiated. The injector generated above is the instanceCache object for this special storage service. Now restore the entire dependency injection process from the Controller run procedure.

angular.module('myapp', []).controller('contrl', function ($scope,$http) { $scope.name = 'hello world'; $scope.clickHanlder = function () { $http.get("http://www.xxx.com/api/get_list").success( function(data){ Console. log(data)// Get interface data})}; });Copy the code

After the compile phase resolves to the Ng-Controller directive, the link phase handles the controller’s logic. Controller execution returns a closure function.

controller = function(){ //expression = function ($scope,$http) {... } $injector.invoke(expression, instance, locals, constructor); //locals has scope}Copy the code

You can see from this that the controller underlying calls the invoke method of the injector.

Internally, invoke will initially determine the type of expression and, if it is an array, split it directly into two groups. The first parameter is used as the dependency injection parameter list, and the last parameter is used as the execution function. If it is just a function like the one above, it first calls the toString method to convert the function to a string. Get the parameter names of the function, $scope and $HTTP, through regular expressions.

First, inject $scope, which is not a globally defined service and is obtained directly from locals. The second parameter, $HTTP, is a serious service.

Inside the invoke method, it first calls the injector’s getService(“$HTTP “) method. The $Injector will first check whether instanceCache of instanceInjector has cached this service. If there is no cache, it will call the getService method of providerInjector. At this point, the providerInjector providerCache caches the providerCache of all service providers globally. It can get $HttpProvider.

We then start instantiating the service provider $HttpProvider, which executes the $HttpProvider.$get method. At this point, the $HttpProvider relies on several other services. It then calls Invoke to walk through the dependency injection process several more times until all the services are fetched. It can execute the function at the end of $get. When the function completes, it returns an object, $HTTP, containing properties and methods related to the back-end interaction. This object is the resulting $HTTP service. This service is not easy to request next time to go through the above process again, so for efficiency in instanceCache cache, you need to get directly from instanceCache next time.

With $scope and $HTTP, you can execute the code inside the Controller. To review the process, in order to execute the code in controller, it depends on the $scope and $HTTP services. $HTTP depends on other services. Only when all other services are generated can it return to the logic of generating $HTTP service, and finally it can return to the controller to execute the logic code in the controller.

Compilation and linking

Compilation and linking pave the way for subsequent responsive changes. In ES5, object.defineProperty listens for data to modify pages. AngularJS doesn’t use this API; it implements a responsive system in its own way.

Compilation and linking are the first steps to building a responsive system. When the responsive system is up and running, the front-end programmer changes the data state such as $scope.name=… , the page can be updated automatically without the programmer having to write the logic for dom manipulation.

Compilation phase

The main thing you do in the compile phase is scan the DOM tree to parse each node with instructions. If code associated with a custom directive is found on an attribute, such as ng-if or ng-repeat, the element node is removed and a comment code replacement (applyDirectivesToNode) is generated, along with a link function for the node and a function publicLin that renders the content of the node kFn. If text nodes with {{name}} display status are found, they are also encapsulated as custom instructions and link functions are generated. These custom directive functions that handle ng-if,ng-repeat, and text nodes are defined elsewhere by Angular. Each custom directive is an object with a compile method that contains the logic to manipulate the DOM.

Step 6 of the loading process, described above, begins compiling.

compile(element)(scope); // Element corresponds to the DOM tree and scope corresponds to $rootScopeCopy the code

The compile function code is as follows.

function compile($compileNodes,transcludeFn){ ... Var compositeLinkFn = compileNodes($compileNodes, transcludeFn); . return function publicLinkFn (scope,cloneConnectFn){ ... }}Copy the code

The result of compile(Element) returns the publicLinkFn function and generates the compositeLinkFn link function. The link function is available in the closure publicLinkFn. The most important part of the compilation phase is how to generate the link function.

The code for compileNodes is the most complex part of the Angular framework. The following is a brief description of its workflow, if not combined with the source code reading is difficult to understand.

Inside compileNodes, it scans the entire DOM node where the user wrote the ORIGINAL DOM in the HTML file by fetching the list of instructions on the node (e.g.

). Then link the logic processed by the ngIf directive to the DOM node to generate the node’s link function nodeLinkFn.

How is the nodeLinkFn function generated? It determines whether the node is a text node or an element node based on its type. If it is a text node, it checks for {{}}. If it encapsulates a custom instruction {compile:fn}. If it is an element node, the this.directive registration directive is called to add a $get method to it. Encapsulate it as a Provider Factory and return the service corresponding to the directive. Eventually nodeLinkFn points to the compile function of the resulting instruction.

If the current node has children, call compilNodes recursively to generate the link function childLinkFn. Stores nodeLinkFn and childLinkFn and the index of the current node into the linkFns array and returns a closure function, compositeLinkFn.

LinkFns array is of the form [0,nodeLinkFn,childLinkFn].

If have brothers node, the data structure similar to [0, nodeLinkFn childLinkFn, 1, nodeLinkFn1, childLinkFn1].

For example, suppose the nodeList(DOM tree) is currently traversed, and it has only one div and no sibling nodes, so index I is 0. NodeLinkFn is generated by collecting directives and DOM elements above the div. Since the div has several subsets, it calls compileNodes again with all the subsets as arguments. The result is childLinkFn. Stuff I,nodeLinkFn, and childLinkFn into the linkFns array. The compositeLinkFn function is then returned. The linkFns array retrieved from the compositeLinkFn holds only three elements.

Above is the outermost case, now let’s push our horizons to get the recursive call compileNodes of childLinkFn to see what happens when we go through the subset.

At this point nodeList is equal to several child div elements below the parent div, and transcludeFn corresponds to the link function of the parent div. Now it’s time to iterate through the nodeList, collecting the instruction level of each child div and combining the instruction with the DOM to generate the nodeLinkFn of the child div. The function returns the compositeLinkF (compositeLinkF) of the index of the child divs,nodeLinkFn and childLinkFn Function of n. Remember that the linkFns referenced in the compositeLinkFn is not the top three elements, but the element composed of several divs.

Now let’s go back to the top. The top value is [index, nodeLinkFn, childLinkFn]. The index is the index that is no problem, nodeLinkFn is a function of the current dom and instructions after the link has no doubt. The third parameter, childLinkFn, is the compositeLinkFn function returned by the compositeLinkFn iterate through the second layer of sub-divs. The linkFns referenced in this compositeLinkFn function are the compositeLinkFn data from the sub-divs.

The ultimate goal of this function is to obtain the link function nodeLinkFn for each DOM node in the DOM tree that has a custom instruction tied to it. What exactly does this link function do? Angular parses a custom directive such as ng-if on each node to find the system-level custom directive logic that is already defined elsewhere in the framework. Finally, the associated code in nodeLinkFn is the code compiled by the compile method of the custom instruction.

Link phase

The above compilation phase will return a function publicLinkFn, when passed to scope to execute publicLinkFn will enter the link phase.

  function publicLinkFn(scope,cloneConnectF){
         if (cloneConnectFn) cloneConnectFn($linkNode, scope);
         if (compositeLinkFn)
              compositeLinkFn(
                scope,
                $linkNode,
                $linkNode,
                parentBoundTranscludeFn
            );
          return $linkNode;
  }
Copy the code

The compile phase generates the link function nodeLinkFn for each node of the DOM tree with custom instructions. All the link function (compositeLinkFn) is executed in the link phase. And returns the linked DOM node. In other words, dom nodes generated by publicLinkFn are bound and mapped to the states in scope. Once the data of scope is modified, the corresponding DOM will be refreshed.

The link function nodeLinkFn of all DOM nodes is executed once to complete the link of the dom of this layer. Then how does nodeLinkFn realize the mutual mapping between the data of this scope and the DOM elements of the page?

Whether it’s a text node {{name+”hello world”}} or an instruction like ngif and ngRepeat, executing nodeLinkFn will eventually compile the corresponding instruction’s function.

$scope.$watch(interplat, the dom callback) is executed in compile.$scope.$watchGroup is executed in ngRepeat.

Each custom instruction corresponds to an expression, for example, the expression corresponding to the text node above is name+” Hello World “. If you pass scope.name to the expression, you can evaluate the expression to get the latest state value (what the page should display).

If it is a text node, it operates on a DOM callback function like node.nodeValue = value. The ngIf callback adds or removes DOM nodes based on the expression’s value of true or false, and the ngRepeat callback renders the DOM list based on the expression’s value.

You can see that all the link phase does is perform a $scope.$watch operation on each DOM node with instructions. So what’s going on in $watch? How can it implement state monitoring?

After executing $scope.$watch once, Angular creates a data object in the $$Watchers array of $Scope.

{ fn:function(){... }, get:function(){... }, last:function(){... }}Copy the code

Fn is the callback function defined in the instruction to manipulate DOM, while get can get the value of the corresponding expression of the instruction by passing scope, and last will return the value of the last expression.

At the bottom of the link phase, the manipulation functions and values of the data state are stored by calling $scope.$watch, and then the page update will be triggered by entering the $digest phase.

Responsive update

Angular completes the compile and link phase and starts executing the Digest function, triggering a page refresh.

$digest:function(){ ... Scope if ((watchers = current.$$watchers)) {length = watchers. while (length--) { try { watch = watchers[length]; If ((value = watch.get(current))) {if (value = watch. == (last = watch.last)) { watch.last = watch.eq ? copy(value, null) : value; // Reset the last value of watch.fn(value, last === initWatchVal? value : last, current ); }}}Copy the code

$scope.$watch stores the operation function and value of the data status in the scope.$$Watchers array.

After executing the $digest method, the watch object in the $$Watchers array is retrieved in turn. The value of the latest expression is obtained by passing in the current scope. If it is found to be different from the last reserved value last, the state has changed and the page update needs to be triggered.

Watch. fn is the callback to update the page. After updating the page, assign the latest value to Watch.last.

In summary, Angular uses scope.watch to implement state monitoring and page rendering. This function encapsulates all operations of each node with instructions, including the function to get the expression value, the last value, and how to update the DOM, into a data object and stores it in the Scope.$$Watchers array. When entering the digest stage, it takes out each watch object in Scope.$$Watchers and decides whether to render the page by comparing whether the value of the corresponding expression of each watch changes.