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

推荐语:

今日推荐一首华为同事的同事翻译的平首文章,推荐的重点因是当做一个华为员工还晚上还能写稿子,由不足小钗不佩服!!!

个中的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;

其他一个内需新鲜处理的要素都对准承诺交一个数组中,目的就是是以构建一个科学的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请求来取得一个json文件被的数据,然后放大管数量放到当前之scope中。我们不仅是TodoCtrl
方法-我们为尚未任何机会去传递参数。但是框架形成了。那$scope和$http变量时自乌来的吧?这实在一个超级酷的特色,简直就是一个神奇的魔法。让咱来拘禁下它们的干活原理。

而我们系被要一个展示用户列表的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)我们该好见见3个名展示在页面上,同时以控制台上应有会输出/api/users这个log。我们得以说咱们的方依赖两独东东:body和ajaxWrapper。但是本咱们的对象是在未传递参数的情形下呢能健康办事,我们期望的一味经调用displayUsers()也克博得平等之结果。如果我们直接行使如达到之方式开展调用,会看如下结果:

Uncaught TypeError: Cannot read property ‘get’ of undefined

当下是为ajax参数没有受定义。

绝大多数提供依赖注入机制的框架还见面时有发生一个injector。如果利用了很依赖,那我们需要在injector中登记下。

吃咱来创造我们温馨的injector:

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

  }
};

咱仅仅待少独办法。第一独就是是register,他接依赖然后存储起来。第二单主意resolve接收一个出仗模块的函数target作为参数。这里的一个关键点是咱而控制好不可知吃流入器调用我们的点子。resolve方法吃归了一个涵盖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();
  }
}

脚是出口结果

图片 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注入到又多的章程吃。这不需将一个目标从一个接近传递及外一个类似,它才发register和resolve方法。

自我们的injector还不够完美,还有提升的半空中,比如支持scope定义。target函数当前是如果让调用时候就是见面创一个新的scope,但是平常我们期待得以传递我们好之scope。我们还好让因支持用户从定义之参数。

若果想我们的代码在尽小化之后吧能正常工作来说,那injector会变的更是错综复杂。我们领略,最小化工具会替换函数、变量甚至方法参数的名字。而我辈的逻辑都是借助这些名字的,所以我们应有考虑生。我们从AngularJS中找到了一个解决方案:

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"

此地发出一个看似定义了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。这对于定义一个接近来说,是一个不胜好之周转逻辑的路子。

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的接口。我们只需要一个定义类的法子,在这边,我们会刻画一个利用函数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的特性与一个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模板

而恐怕听说过Facebook的框架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代码。Facebook的开发者称这种解析器为JSX。JSX解析器大约390k、12000行代码。因此其还是比较复杂的。在本节遇,我们将创一个非常简单,但是功能强大的东东:一个为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>"
]

此只有生一个表达式,里面的情是title。比较直观的不二法门是咱采取JS的replace函数去替换<%
title %>为comp
对象吃之多少。但是,这种办法只能运行为简单的性。如果发嵌套对象竟是一旦使用函数,比如下面的例子:

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

我们不用复杂的解析器,也绝不发明一种新的语言,我们仅仅所以本生JS。我们只要为此的哪怕是只有new
Function语法。

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

咱们能够通过其来创造函数体,而且好以其后去运作。因此我们需要了解表达式的位置与她面前的要素。那如果我们采取一个即之数组和一个游标,那代码应该是这样的:

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。

总结

当一个老大之框架和库函数背后都集中了充分睿智的开发者。他们找到的多招都大的琐碎的,甚至是神奇之。在这篇稿子中,我们总结了这些魔法。在JS世界面临,我们可于它而使用他们的代码是挺高的事务。

这首文章的代码都得打GitHub中下载到。

相关文章