We use a variety of frameworks every day, and these frameworks accompany our daily work. The purpose of using these frameworks is to liberate us, and few people really care about what goes on behind them. I’ve also used a number of frameworks, and I’ve learned a few things from these popular frameworks, so I thought I’d share them.

Each title is a separate topic, and you can read it with interest.

String to DOM

If you are a regular user of jquery, you will be familiar with the following code:

var text = $('<div>hello, world</div>');

$('body').append(text)
Copy the code

The result of the above code execution is to add a div node to the page. Without jQuery, the code might get a little more complicated:

var strToDom = function(str) {
    var temp = document.createElement('div');

    temp.innerHTML = str;
    return temp.childNodes[0];
}

var text = strToDom('<div>hello, world</div>');

document.querySelector('body').appendChild(text);
Copy the code

This code is exactly the same as using jQuery. If you think so, you’re wrong. What is the difference between the following two types of code:

var tableTr = $('<tr><td>Simple text</td></tr>');
$('body').append(tableTr);

var tableTr = strToDom('<tr><td>Simple text</td></tr>');
document.querySelector('body').appendChild(tableTr);
Copy the code

On the surface there is nothing wrong, but if you look at the page structure using developer tools, you will see:

StrToDom just creates a text node, not a real TR tag. The reason is that strings containing HTML elements run through the browser through the parser, which ignores tags that are not placed in the correct context, so we only get a text node.

How does jQuery solve this problem? By analyzing the source code, I found the following code:

var wrapMap = {
  option: [1.'<select multiple="multiple">'.'</select>'].legend: [1.'<fieldset>'.'</fieldset>'].area: [1.'<map>'.'</map>'].param: [1.'<object>'.'</object>'].thead: [1.'<table>'.'</table>'].tr: [2.'<table><tbody>'.'</tbody></table>'].col: [2.'<table><tbody></tbody><colgroup>'.'</colgroup></table>'].td: [3.'<table><tbody><tr>'.'</tr></tbody></table>']._default: [1.'<div>'.'</div>']}; wrapMap.optgroup = wrapMap.option; wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; wrapMap.th = wrapMap.td;Copy the code

Each element requires special handling of array allocation. The idea is to build the right nesting level of DOM elements and dependencies to get what we need. For the tr element, for example, we need to create two levels of nesting: table and tBody.

Once we have this Map, we can get the final labels we need. Get tr from < TD > Hello word
:

var match = /<\s*\w.*? >/g.exec(str);
var tag = match[0].replace(/</g.' ').replace(/>/g.' ');
Copy the code

All that’s left is to return the DOM element based on the appropriate context, and finally we’ll make the final modifications to strToDom:

var strToDom = function(str) {
  var wrapMap = {
    option: [1.'<select multiple="multiple">'.'</select>'].legend: [1.'<fieldset>'.'</fieldset>'].area: [1.'<map>'.'</map>'].param: [1.'<object>'.'</object>'].thead: [1.'<table>'.'</table>'].tr: [2.'<table><tbody>'.'</tbody></table>'].col: [2.'<table><tbody></tbody><colgroup>'.'</colgroup></table>'].td: [3.'<table><tbody><tr>'.'</tr></tbody></table>']._default: [1.'<div>'.'</div>']}; wrapMap.optgroup = wrapMap.option; wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; wrapMap.th = wrapMap.td;var element = document.createElement('div');
  var match = /<\s*\w.*? >/g.exec(str);

  if(match ! =null) {
    var tag = match[0].replace(/</g.' ').replace(/>/g.' ');
    var map = wrapMap[tag] || wrapMap._default, element;
    str = map[1] + str + map[2];
    element.innerHTML = str;
    // Descend through wrappers to the right content
    var j = map[0] +1;
    while(j--) { element = element.lastChild; }}else {
    // if only text is passed
    element.innerHTML = str;
    element = element.lastChild;
  }
  return element;
}
Copy the code

Through the match! = null determines whether a label or text node is being created. This time we can create a valid DOM tree through the browser. Finally, we return the tag by using a while loop until we get the tag we want.

AngularJS dependency injection

When we started using AngularJS, its two-way data binding was impressive. Another magic feature is dependency injection. Here’s a simple example:

function TodoCtrl($scope, $http) {
  $http.get('users/users.json').success(function(data) {
    $scope.users = data;
  });
}
Copy the code

This is typical AngularJS controller writing: get data from a JSON file by making an HTTP request and assign the data to $scope.users. The AngularJS framework automatically injects $scope and $HTTP into the controller. Let’s see how it works.

Let’s look at an example where we want to display the user’s name on the page. For simplicity, we mock the HTTP request with mock data:

var dataMockup = ['John'.'Steve'.'David'];
var body = document.querySelector('body');
var ajaxWrapper = {
  get: function(path, cb) {
    console.log(path + ' requested'); cb(dataMockup); }}var displayUsers = function(domEl, ajax) {
  ajax.get('/api/users'.function(users) {
    var html = ' ';
    for(var i=0; i < users.length; i++) {
      html += '<p>' + users[i] + '</p>';
    }
    domEl.innerHTML = html;
  });
}

displayUsers(body, ajaxWrapper)
Copy the code

DisplayUsers (body, ajaxWrapper) execution requires two dependencies: body and ajaxWrapper. Our goal is to call displayUsers() directly without passing arguments, and it will work as expected.

Most frameworks provide a dependency injection mechanism and have a module, usually called Injector. All dependencies are registered here and provided with an interface for external access:

var injector = {
  storage: {},
  register: function(name, resource) {
    this.storage[name] = resource;
  },
  resolve: function(target) {}};Copy the code

The key implementation of Resolve is this: it takes a target object, wraps target and calls it by returning a closure. Such as:

resolve: function(target) {
  return function() {
    target();
  };
}
Copy the code

This allows us to call the dependent functions we need.

The next step is to get the target argument list. Here I refer to the AngularJS implementation:

var FN_ARGS = /^function\s*[^\(]*\(\s*([^\)]*)\)/m;
var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*? \*\/))/mg; . function annotate(fn) { ... fnText = fn.toString().replace(STRIP_COMMENTS,' '); argDecl = fnText.match(FN_ARGS); . }Copy the code

I screened out other code details, leaving only the ones that were useful to us. Annotate corresponds to our own Resolve. It converts the object function to a string, with the comments removed, resulting in parameter information:

resolve: function(target) {
  var FN_ARGS = /^function\s*[^\(]*\(\s*([^\)]*)\)/m;
  var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*? \*\/))/mg;
  fnText = target.toString().replace(STRIP_COMMENTS, ' ');
  argDecl = fnText.match(FN_ARGS);
  console.log(argDecl);
  return function() { target(); }}Copy the code

Open the console:

The second element of the argDecl array contains all of the parameters, which gives the injector stored dependencies. Here is the concrete implementation:

resolve: function(target) {
  var FN_ARGS = /^function\s*[^\(]*\(\s*([^\)]*)\)/m;
  var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*? \*\/))/mg;
  fnText = target.toString().replace(STRIP_COMMENTS, ' ');
  argDecl = fnText.match(FN_ARGS)[1].split(/, ?/g);
  var args = [];
  for(var i=0; i&lt; argDecl.length; i++) {if(this.storage[argDecl[i]]) {
      args.push(this.storage[argDecl[i]]); }}return function() { target.apply({}, args); }}Copy the code

Through the split (/,? /g) convert the string domEl, ajax to an array by checking whether the Injector has registered a dependency of the same name, and if so, passing the dependency into a new array as an argument to the target function.

The code called should look like this:

injector.register('domEl', body);
injector.register('ajax', ajaxWrapper);

displayUsers = injector.resolve(displayUsers);
displayUsers();
Copy the code

The nice thing about this implementation is that we inject domEl and Ajax into any desired function. We can even configure the application. You no longer need to pass parameters back and forth, just register and resolve.

Our auto-injection is not perfect so far, with two drawbacks:

1. Functions do not support custom parameters.

2. The parameter name changes due to the compression of the on-line code, resulting in the failure to obtain the correct dependency.

AngualrJS has solved both of these problems. If you are interested, you can read my other article: javascript implementation of dependency injection ideas, which details the complete solution of dependency injection.

Ember Computed properties

Probably the first thing most people think of when they hear Computed properties is Computed properties in Vue. In fact, the Ember framework also provides a feature for calculating attributes of attributes. Here’s an official example:

App.Person = Ember.Object.extend({
  firstName: null.lastName: null.fullName: function() {
    return this.get('firstName') + ' ' + this.get('lastName');
  }.property('firstName'.'lastName')});var ironMan = App.Person.create({
  firstName: "Kobe".lastName:  "Bryant"
});
ironMan.get('fullName') // "Kobe Bryant"
Copy the code

The Person object has firstName and lastName attributes. Computed fullName returns a connection string containing the fullName of person. The odd thing is that the fullName function uses the.property method. Let’s look at the code for the property:

Function.prototype.property = function() {
  var ret = Ember.computed(this);
  // ComputedProperty.prototype.property expands properties; no need for us to
  // do so here.
  return ret.property.apply(ret, arguments);
};
Copy the code

Adjust the prototype of the global function object by adding new properties. It’s a good idea to run some logic during class definition.

Ember uses getters and setters to manipulate an object’s data. This simplifies the implementation of the computed properties because we have another layer to deal with the actual variables before. However, it would be more interesting if we could use computed properties with ordinary JS objects. Such as:

var User = {
  firstName: 'Kobe'.lastName: 'Bryant'.name: function() {
    // getter + setter}};console.log(User.name); // Kobe Bryant
User.name = 'LeBron James';
console.log(User.firstName); // LeBron
console.log(User.lastName); // James
Copy the code

Name, as a general property, is essentially a function that gets or sets firstName and lastName.

JavaScript has a built-in feature that helps us implement this idea:

var User = {
  firstName: 'Kobe'.lastName: 'Bryant'};Object.defineProperty(User, "name", {
  get: function() { 
    return this.firstName + ' ' + this.lastName;
  },
  set: function(value) { 
    var parts = value.toString().split(/ /);
    this.firstName = parts[0];
    this.lastName = parts[1]? parts[1] : this.lastName; }});Copy the code

The Object.defineProperty method can accept an Object, its property name, getters, and setters. All we need to do is write the implementation logic for these two methods. Run the above code to get the desired result:

console.log(User.name); // Kobe Bryant
User.name = 'LeBron James';
console.log(User.firstName); // LeBron
console.log(User.lastName); // James
Copy the code

Object.defineproperty is what we want, but obviously we don’t want to write it this way every time. Ideally, we want to provide an interface. In this section, we’ll write a function called Computize that will process objects and somehow convert the name function into attributes with the same name.

var Computize = function(obj) {
  return obj;
}
var User = Computize({
  firstName: 'Kobe'.lastName: 'Bryant'.name: function() {... }});Copy the code

We want to use the name method as the setter and also as the getter. This is similar to Ember’s calculated properties.

Now we add our own logic to the prototype of the function object:

Function.prototype.computed = function() {
  return { computed: true.func: this };
};
Copy the code

This allows you to call computed functions directly after each Function definition.

name: function() {... }.computed()Copy the code

The name attribute is no longer a function, but an object: {computed: true, func: this}. Computed equals true, and the func attribute points to the original function.

The really magical thing is the implementation of the Computize Helper. It iterates through all attributes of the object, using Object.defineProperty for all calculated attributes:

var Computize = function(obj) {
  for(var prop in obj) {
    if(typeof obj[prop] == 'object' && obj[prop].computed === true) {
      var func = obj[prop].func;
      delete obj[prop];
      Object.defineProperty(obj, prop, {
        get: func,
        set: func }); }}return obj;
}
Copy the code

Note: We removed the calculated attribute name because Object.defineProperty only works on undefined attributes in some browsers.

Here is the final version of the user object using the.computed() function:

var User = Computize({
  firstName: 'Kobe'.lastName: 'Bryant'.name: function() {
    if(arguments.length > 0) {
      var parts = arguments[0].toString().split(/ /);
      this.firstName = parts[0];
      this.lastName = parts[1]? parts[1] : this.lastName;
    }
    return this.firstName + ' ' + this.lastName;
  }.computed()
});
Copy the code

The logic of the function is to check if there are any arguments, split the arguments, assign firstName and lastName, and return the full name.

The end of the

Behind large frameworks and libraries lies the experience of many great predecessors. It is important that learning about these frameworks gives us a better understanding of the principles behind them and allows us to move away from framework development.