首页 > 网站 > WEB开发 > 正文

【转】【译】JavaScript魔法揭秘--探索当前流行框架中部分功能的处理机制

2024-04-27 14:20:55
字体:
来源:转载
供稿:网友

【转】【译】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();  }}

下面是输出结果

Revealing the AngularJS dependency injection

如果我们去查看第二个元素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和ajaxWr

发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表