推荐语:
今天推荐一篇华为同事的同事翻译的一篇文章,推荐的主要原因是作为一个华为员工居然晚上还能写文章,由不得小钗不佩服!!!
其中的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(); }}
下面是输出结果
如果我们去查看第二个元素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
新闻热点
疑难解答