AngularJS【转】【译】JavaScript魔法揭秘–探索当前风靡框架中一些效应的拍卖体制

推荐语:

后天推荐一篇三星同事的同事翻译的一篇小说,推荐的最主要缘由是用作贰个BlackBerry员工甚至早上还是能写作品,由不得小钗不钦佩!!!

里头的jQuery、angular、react皆是可怜完好无损的框架,各有特点,各位可以看看

编辑:github 原文链接:Revealing
the Magic of
JavaScript

jnotnull 发布在 JavaScript译文

大家每天都在利用大量的工具,区其余库和框架已经化为大家常见工作的一局地。咱们采用他们是因为我们不想再度造轮子,固然大家只怕并不知道这个框架的法则。在这篇小说中,大家将揭秘当前流行框架中那多少个魔法处理机制。

由此字符串来创立DOM节点

随着单页应用的兴起,我们已经得以使用JS来做越多的事体了,业务的半数以上逻辑都将移到前台。大家以上边创造页面成分为例:

var text = $('<div>Simple text</div>');

$('body').append(text);

运营结果是:在此时此刻页面中新增了三个div成分。使用jquery,这些只需要一行代码就解决了,若是不用jquery,大概会多几行代码:

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

  temp.innerHTML = str;
  return temp.childNodes[0];
}
var text = stringToDom('<div>Simple text</div>');

document.querySelector('body').appendChild(text);

大家定义了多个和好的工具方法stringToDom,那几个方式做了之类事情:首先创造一个权且div成分,然后设定它的innerTHML属性,然后再次回到该DIV成分的首先个节点。同样的写法,下边的代码会拿到差异的结果:

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

var tableRow = stringToDom('<tr><td>Simple text</td></tr>');
document.querySelector('body').appendChild(tableRow);

从那个页面的表面上看,没有啥不一样。但是我们经过chrome的开发工具查看生成的HTML标记的话,会拿到一个幽默的结果,创设了三个文书成分。

相似大家的stringToDom
只创制了二个文书节点而不是tr标签。可是jquery却不知缘何可以健康运行。难题的来头是在浏览器端是通过解析器来分析含有HTML成分的字符串的。解析器会忽略掉那一个放错上下文地点的号子,由此我们只拿到了文件节点。row标签没有蕴涵在科学的table标签中,那对浏览器的解析器来说就是不合规的。

jquery通过创办正确的上下文后然后做些转换,可以成功的解决那个题材。若是我们深入到源码中可以看到上边的一个辉映:

 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;

其余一个急需新鲜处理的要素都对应到1个数组中,目标就是为了营造一个不易的DOM节点。例如,对于tr成分,大家要创造一个暗含tbody的table中,须求包裹两层。

虽说有了map,可是大家依然得先去查找到字符串中的截至标签是甚。上面的代码可以从<tr><td>Simple text</td></tr>抽取出tr标签。

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

剩下来要做的就是找到属性上下文,然后重回DOM成分。上面是stringToDom方法的最后版本:

var stringToDom = 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;
}

只顾下,我们有个判断 match !=
null条件用于判断string中是还是不是有tag标签,即便没有大家只是不难的回来文本节点。那里大家传入了天经地义的标签,所以浏览器可以创设三个健康的DOM节点了。在代码的末梢有的可以看出,通过利用贰个while循环,我们一直深深到我们想要的至极tag节点后赶回给了调用者。

上面让大家窥视下AngularJS中时时的借助注入。

揭秘AngularJS中的依赖注入

当大家第四回采取AngularJS的时候,大家自然对它的双向数据绑定留下了深远的熏陶,那第一个值得关怀的就是它那魔法般的依赖注入。上边看下简单的事例:

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

那是十一分经典的AngularJS控制器。它经过贰个http请求来获取1个json文件中的数据,然后放把数据放到当前的scope中。大家不可是TodoCtrl
方法-我们也远非其它机会去传递参数。可是框架形成了。这$scope和$http变量时从哪个地方来的吧?那实际2个一级酷的特色,差不多就是二个神奇的魔法。让大家来看下它的办事原理。

假诺大家系统中需要两个出示用户列表的JS函数。大家须求一个可以把变化的HTML设置到DOM节点的措施,一个装进了得到数据的Ajax请求的对象。为了简化例子,大家mock了多少和http请求。

var dataMockup = ['John', 'Steve', 'David'];
var body = document.querySelector('body');
var ajaxWrapper = {
  get: function(path, cb) {
    console.log(path + ' requested');
    cb(dataMockup);
  }
}

咱俩将动用body标签来承载内容。ajaxWrapper是三个接触请求的目的,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)大家理应可以看出2个名字显示在页面上,同时在控制台上应有会输出/api/users这一个log。咱们可以说我们的主意依赖五个东东:body和ajaxWrapper。然则未来大家的靶子是在不传递参数的意况下也能健康工作,大家愿意的只经过调用displayUsers()也能取得平等的结果。若是大家直接使用如上的方式开展调用,会看出如下结果:

Uncaught TypeError: Cannot read property ‘get’ of undefined

那是因为ajax参数没有被定义。

大部提供依赖注入机制的框架都会有1个injector。若是采纳了丰硕依赖,那大家要求在injector中注册下。

让大家来创设我们和好的injector:

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

  }
};

大家只必要八个措施。第二个就是register,他接受倚重然后存储起来。第二个格局resolve接收2个有依靠模块的函数target作为参数。那里的3个关键点是大家要控制好不大概让流入器调用大家的方法。resolve方法中回到了1个包蕴target()的闭包。看下代码:

resolve: function(target) {
  return function() {
    target();
  };
}

诸如此类大家就有可以在不更改使用流程的景观下来访问函数了。injector当前仍然三个独立的还要不带有其余逻辑的不二法门。

当然,把displayUsers 传递给resove函数如故不行

displayUsers = injector.resolve(displayUsers);
displayUsers();

只怕报错。下一步就是找出target参数到底需求如何,是还是不是都以它的着重?那里大家得以参考下AngularJS。同样我自个儿深入看了下源码找到了上面那段代码:

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

大家忽略掉一部分细节代码,只看大家须求的。annotate方法和我们的resolve方法很像。它转换传递过去的target为字符串,删除掉注释代码,然后抽取其中的参数。让我们看下它的实践结果:

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

上边是出口结果

AngularJS 1

即使大家去查看第二个成分argDecl数组的话,大家会师到它所要求依靠对象。那正是大家需要的,因为通过名字大家就能从storage中查到依靠的财富了。下边的那个版本可以做到我们的目的:

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<argDecl.length; i++) {
    if(this.storage[argDecl[i]]) {
      args.push(this.storage[argDecl[i]]);
    }
  }
  return function() {
    target.apply({}, args);
  }
}

专注大家接纳了.split(/,
?/g)把字符串domEl、ajax转换到了数组。接下来大家来校验尊敬是或不是注册了,假使注册的话大家把它传递给target函数作为参数。注入器的代码应该是这般的:

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

displayUsers = injector.resolve(displayUsers);
displayUsers();

那般完成的裨益是大家可以得以吧DOM和ajaxWrapper注入到越多的方法中。这不需求把1个目的从一个类传递到另2个类,它唯有register和resolve方法。

理所当然大家的injector还不够周详,还有升高的上空,比如辅助scope定义。target函数当前是一旦被调用时候就会创立1个新的scope,不过日常大家目的在于得以传递我们和好的scope。大家还足以让正视襄助用户自定义的参数。

比方想我们的代码在最小化之后也能平常干活以来,那injector会变的尤为复杂。我们清楚,最小化工具会替换函数、变量甚至方法参数的名字。而小编辈的逻辑都以依靠那么些名字的,所以大家相应考虑下。大家从AngularJS中找到了1个缓解方案:

displayUsers = injector.resolve(['domEl', 'ajax', displayUsers]);

我们不光传递displayUsers,我们还传递器重对象的名字。

使用Ember的盘算属性

Ember是当下最风靡框架之一。它有诸多实用的表征。其中统计属性十分幽默。计算属性就是用三个函数来担任属性。让大家来看下Ember文档中的二个简易例子:

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: "Tony",
  lastName:  "Stark"
});
ironMan.get('fullName') // "Tony Stark"

此间有3个类定义了firstName和lastName属性。计算属性fullName再次来到贰个组装后的人的全名字符串。那里比较面生的的地点是我们采用.property方法跟着函数后边赋值给fullName。小编个人从不曾在何地看到过这种写法。同样,小编从源码中找到了答案:

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

此间我们见到它在Function的原型对象中追加了一个新的性质property。那对于定义多少个类来说,是3个十分好的运营逻辑的门径。

AngularJS,Ember使用get、set来操作对象属性。那简化了总计属性的落到实处,因为在大家操作当中忽略掉了七个封装层。不过越来越有意思的是我们是或不是可以在JS原生对象上使用统计属性呢。看下边的例子:

var User = {
  firstName: 'Tony',
  lastName: 'Stark',
  name: function() {
    // getter + setter
  }
};

console.log(User.name); // Tony Stark
User.name = 'John Doe';
console.log(User.firstName); // John
console.log(User.lastName); // Doe

name是二个不以为奇的靶子属性,可是此间被给予了贰个方法,可以设定可能取得到firstName和lastName。

JS值有个放置的特色可以协助大家兑现我们的想法。接着看上面的代码:

var User = {
  firstName: 'Tony',
  lastName: 'Stark'
};
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;
  }
});

Object.defineProperty方法接受一个上下文、属性名称以及get/set方法。大家要做的就是贯彻里面的多少个措施,仅此而已。大家将运转方面的代码并且能够获得到期望的结果:

console.log(User.name); // Tony Stark
User.name = 'John Doe';
console.log(User.firstName); // John
console.log(User.lastName); // Doe

Object.defineProperty确实是我们必要的,不过我们不想强制逐个开发者每一次都重写这几个措施。大家须要提供二个原生辅助的逻辑代码,就恍如于Ember的接口。大家只须要3个定义类的艺术,在那里,大家会写一个应用函数Computize用来把目的中的函数中传递的名号转换到对象中质量的称号。

var Computize = function(obj) {
  return obj;
}
var User = Computize({
  firstName: 'Tony',
  lastName: 'Stark',
  name: function() {
    ...
  }
});

笔者们想选拔set来设定名称,同时采用get来赢得名称。那和Ember的计量属性情外相近。

当今就让我们增添大家的逻辑代码到函数的原型中呢:

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

若是我们增添了上面的代码,大家就会为每一个函数增加了多个.computed()方法了。

name: function() {
  ...
}.computed()

结果就是name属性不在是函数了,而是贰个持有computed为true的属性和1个func属性的对象。真正的魔法暴发在自定义辅助方法的兑现上,它贯穿于全部对象的习性上。大家会在统计属性上应用Object.defineProperty:

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

专注大家删除了原生的习性名称。在有个别浏览器中Object.defineProperty只运维于还没有存在的属性上。

上面是多个使用.computed()方法最后版本的User对象。

var User = Computize({
  firstName: 'Tony',
  lastName: 'Stark',
  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()
});

在这一个重回全名的函数中可以观测到firstName和lastName的变迁。在此间判断是或不是认清了参数,假诺传了参数则把他们分设到firstName和lastName中。

大家已经提过期望的接口,然则我们再来看下:

console.log(User.name); // Tony Stark
User.name = 'John Doe';
console.log(User.firstName); // John
console.log(User.lastName); // Doe
console.log(User.name); // John Doe

上边是CodePen中运作的结果:

疯狂的React模板

你或者听他们讲过非死不可的框架React。它的营造思想就是一切都是组件。其中感兴趣的就是关于组件的概念。让我们看下如下例子:

<script type="text/jsx">;
  /** @jsx React.DOM */
  var HelloMessage = React.createClass({
    render: function() {
      return <div>Hello {this.props.name}</div>;
    }
  });
</script>;

自作者看看到那段代码的时候我们会想到那是JS,不过不是合法的,那里的render方法恐怕会报错。可是此间的手法是那段代码放在了script标签中,同时赋值给了概念的变量中。浏览器不会处理它代表大家的代码是高枕无忧的。React有它本身的解析器,会把定义好的代码转换到合法的JS代码。非死不可的开发者称那种解析器为JSX。JSX解析器大致390k、13000行代码。由此它依然相比复杂的。在本节中,我们将开创3个至极不难,可是作用强大的东东:三个以React风格解析HTML模板的JS类。

Facebook选拔的法子是鱼龙混杂使用JS代码和HTML标签。将来一经大家有如下的模版:

<script type="text/template" id="my-content">;
  <div class="content">;
    <h1>;<% title %>;</h1>;
  </div>;
</script>;

追加二个零部件:

var Component = {
  title: 'Awesome template',
  render: '#my-content'
}

想尽是大家钦点template
id,然后定义要被使用的数码。剩下的就是我们的落到实处了:连接八个成分的发动机。我们称之为Engine,它应该是这么的:

var Engine = function(comp) {
  var parse = function(tplHTML) {
    // ... magic
  }
  var tpl = document.querySelector(comp.render);
  if(tpl) {
    var html = parse(tpl.innerHTML);
    return stringToDom(html);
  }
}
var el = Engine(Component);

我们将拿到script标签里面的始末,然后解析它后生成HTML字符串。在更换HTML为DOM成分之后,把它看作结果重临截止。注意大家用了stringToDom函数,我们在首先节中早已见过了。

现行让我们初阶写parse函数。我们主要职务是从表明式中分别出HTML标记。表达式中大家要寻找<%和%>之间的字符串。我们应用正则表达式去遍历查找他们:

var parse = function(tplHTML) {
  var re = /<%([^%>]+)?%>/g;
  while(match = re.exec(tplHTML)) {
    console.log(match);
  }
}

上述代码的之行结果如下:

[
    "<% title %>", 
    "title", 
    index: 55, 
    input: "<div class="content"><h1><% title %></h1></div>"
]

那边只有3个表达式,里面的内容是title。比较直观的法门是我们运用JS的replace函数去替换<%
title %>为comp
对象中的数据。可是,那种方法只好运营于简单的习性。要是有嵌套对象竟是要选择函数,比如上面的例证:

var Component = {
  data: {
    title: 'Awesome template',
    subtitle: function() {
      return 'Second title';
    }
  },
  render: '#my-content'
}

我们毫不复杂的解析器,也无须发美赞臣(Karicare)种新的言语,我们只用原生JS。我们要用的就是唯有new
Function语法。

var fn = new Function('arg', 'console.log(arg + 1);');
fn(2); // outputs 3

大家可以透过它来成立函数体,而且可以在其后去运转。因而大家须求知道表明式的职责以及它后边的要素。这假设我们采用2个一时半刻的数组和三个游标,那代码应该是如此的:

var parse = function(tplHTML) {
  var re = /<%([^%>]+)?%>/g;
  var code = [], cursor = 0;
  while(match = re.exec(tplHTML)) {
    code.push(tplHTML.slice(cursor, match.index));
    code.push({code: match[1]}); // <-- expression
    cursor = match.index + match[0].length;
  }
  code.push(tplHTML.substr(cursor, tplHTML.length - cursor));
  console.log(code);
}

代码的出口结果如下:

[
  "<div class="content"><h1>", 
  { code: "title" },
  "</h1></div>"
]

代码数据数组将会被转换到字符串来作为函数体。举例:

return "<div class=\"content\"><h1>" + title + "</h1></div>";

出口那些结果照旧分外不难的。大家得以写几个巡回来遍历代码数据的因从来判断它是字符串照旧对象。可是那不得不覆盖部分气象。假设大家有如下的模版该咋做呢:

// component
var Component = {
  title: 'Awesome template',
  colors: ['read', 'green', 'blue'],
  render: '#my-content'
}

// template
<script type="text/template" id="my-content">
    <div class="content">
        <h1><% title %></h1>
        <% while(c = colors.shift()) { %>
            <p><% c %></p>
        <% } %>
    </div>
</script>

大家无法只是连连表明式就能获取颜色列表。由此,我们不用字符串连接字符串,大家要把它们手机风靡云涌放到数组中。上边是翻新版本的parse函数:

var parse = function(tplHTML) {
  var re = /<%([^%>]+)?%>/g;
  var code = [], cursor = 0;
  while(match = re.exec(tplHTML)) {
    code.push(tplHTML.slice(cursor, match.index));
    code.push({code: match[1]}); // <-- expression
    cursor = match.index + match[0].length;
  }
  code.push(tplHTML.substr(cursor, tplHTML.length - cursor));
  var body = 'var r=[];\n';
  while(line = code.shift()) {
    if(typeof line === 'string') {
      // escaping quotes
      line = line.replace(/"/g, '\\"');
      // removing new lines
      line = line.replace(/[\r\t\n]/g, '');
      body += 'r.push("' + line+ '");\n'
    } else {
      if(line.code.match(/(^( )?(if|for|else|switch|case|break|while|{|}))(.*)?/g)) {
        body += line.code + '\n';
      } else {
        body += 'r.push(' + line.code + ');\n';
      }
    }
  }
  body += 'return r.join("");';
  console.log(body);
}

若果代码数组被填充玩大家就起来营造函数体了。模板的每行都会被积存到贰个数组r中。如果这行是字符串,我们会引号举办转义并且去除掉换行符、回车符和tab符,然后增添到数组中。如若是代码,则需求校验是不是是合法的JS操作符,倘诺是JS语法则不会添加到数组中。console.log会有如下输出:

var r=[];
r.push("<div class=\"content\"><h1>");
r.push(title);
r.push("</h1>");

while(c = colors.shift()) { 
  r.push("<p>");
  r.push(c);
  r.push("</p>");
}

r.push("</div>");
return r.join("");

老大好,不是么?这么些JS的天个性式化工具,将会得到到大家想要的结果。

剩余要做的工作就是运维大家创制的函数:

body = 'with(component) {' + body + '}';
return new Function('component', body).apply(comp, [comp]);

大家通过应用with语句来把上下文设定为component,假诺不适用它我们须求使用this.title和this.colors而不是title和colors。

总结

在3个大的框架和库函数背后都汇集了十分睿智的开发者。他们找到的无数招数都尤其的琐碎的,甚至是神奇的。在那篇小说中,我们统计了这一个魔法。在JS世界中,大家得以从它们并且选取他们的代码是不行棒的事情。

那篇小说的代码都可以从GitHub中下载到。

相关文章