,但这次我们传递了正确的DOM标签,使浏览器可以创建一个有效的 DOM 树,最终通过一个while循环,不断地递归,直到找到我们想要的标签。
下面是在 CodePen 显示的执行结果:
<tr><td>Simple texttd>tr>
接下来我们来探讨angularjs的依赖注入。
探讨 Angularjs 的依赖注入
当我们开始使用 AngularJS 时,最令人印象深刻的便是双向数据绑定,但我们注意到的第二件事便是其神奇的依赖注入,下面是一个简单的例子:
function TodoCtrl($scope, $http) {
$http.get('users/users.json').success(function(data) {
$scope.users = data;
});
}
这是一个典型的 AngularJS 控制器,它执行 HTTP 请求,从一个 JSON 文件读取数据,并把它传递给目前的上下文,我们并没有执行 TodoCtrl
函数,甚至没有传递任何参数的机会,框架帮我们做了,这些 $scope 和 $HTTP 变量从哪里来的?这是一个超级酷的功能,像黑魔法一样,让我们来看看它是如何实现的。
现在假设我们的系统有一个显示用户信息的 JavaScript 函数,它需要访问 DOM 元素并放置最终生成的 HTML ,以及一个 Ajax 包装器来获取数据,为了简化这个例子,我们将伪造实体模型数据和 HTTP 请求。
var dataMockup = ['John', 'Steve', 'David'];
var body = document.querySelector('body');
var ajaxWrapper = {
get: function(path, cb) {
console.log(path + ' requested');
cb(dataMockup);
}
}
我们将使用
标签作为内容载体。 ajaxWrapper 是模拟请求的对象,dataMockup 包含了我们的用户数组。下面是我们将使用的函数:
var displayUsers = function(domEl, ajax) {
ajax.get('/api/users', function(users) {
var html = '';
for(var i=0; i < users.length; i++) {
html += ''
+ users[i] + '';
}
domEl.innerHTML = html;
});
}
显然,如果我们运行 displayUsers(body,ajaxWrapper)
我们会看到页面上显示了3个名字,控制台输出了/API/users requested
,我们可以说,我们的方法有两个依赖 —— body 和 ajaxWrapper 。现在我们的想法是让函数工作时无需传递参数,也就是我们要得到相同的结果仅通过调用displayUsers()
即可,如果使用现有的代码去执行,结果将是:
Uncaught TypeError: Cannot read property 'get' of undefined
这是正常的,因为 AJAX 参数没有定义。
大多数提供依赖注入的框架通常都有一个称为注入器的模块,使用它需要将依赖注册,然后,我们的资源再由这个注入器模块传递给应用程序逻辑。
现在我们来创建自己的注入器:
var injector = {
storage: {},
register: function(name, resource) {
this.storage[name] = resource;
},
resolve: function(target) {
}
};
我们只需要两个方法:第一个,register
,它接受我们的资源(依赖)并在内部存储;第二个我们接受注入的目标target
- 即那些有依赖性,需要接受他们作为参数的函数,这里的关键点是注入器不应调用我们的函数,这是我们的工作,我们应该能够控制,我们的解决方法是在 resolve
方法返回一个包裹了 target
的闭包并调用它。例如:
resolve: function(target) {
return function() {
target();
};
}
使用该方法,我们将有机会来调用函数以及所需的依赖关系,并且,我们未改变应用程序的工作流程,注入器仍然是独立的东西,并没有带任何逻辑。
然而,当传递 displayUsers
函数作为 Resolve
方法的参数时并没有作用:
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
是一样的东西,它把目标函数源码转为字符串,删除注释(如果有的话),并提取参数,让我们的 resolve
函数使用这段代码并查看结果:
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
转换为一个数组,之后,我们查询依赖是否在我们的 storage
对象注册了,如果是我们则传递他们到 target
函数,注入器以外的代码看起来像这样:
injector.register('domEl', body);
injector.register('ajax', ajaxWrapper);
displayUsers = injector.resolve(displayUsers);
displayUsers();
这种实现的好处是,我们可以在许多函数注入 DOM 元素和 Ajax warpper ,我们甚至可以像这样分发我们的应用程序配置,不再需要在类与类之间传递各种对象,仅需一个 register
和 resolve
方法即可。
当然,我们的注入器是不完美的,还有一些改进的空间,比如对自定义作用域的支持,target
现在在一个全新的作用域内被调用,但通常我们会想使用我们自定义的,另外我们应该支持传递参数给依赖。
如果我们既想保持代码短小又能正常工作,则注入器将变得更加复杂,正如我们所知道的我们这个缩小版替换了函数名,变量和方法的参数,而且因为我们的逻辑依赖于这些名称我们需要考虑变通,一个可能的解决方案是再次使用AngularJs:
displayUsers = injector.resolve(['domEl', 'ajax', displayUsers]);
与之前仅仅传递 displayUsers
不同,现在还传递了实际依赖的名字。
现在看看这个例子的最终结果:
John
Steve
David
采用 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')
这里有有个包含firstName和lastName属性的类,其中计算的属性FullName返回一个包含该人的全名的字符串,奇怪的是我们使用 .property
用于 fullname
函数的后面,除此之外没看到其他特别的地方。我们再次来看看这个框架的代码使用了何种魔法:
Function.prototype.property = function() {
var ret = Ember.computed(this);
return ret.property.apply(ret, arguments);
};
Ember为全局Function
对象扩充了一个property
方法,这是一个很好的方式,在类的定义中运行一些逻辑。
Ember使用getter
和setter
方法对对象的数据进行操作。这简化了动态属性的实现,因为我们在操作实际变量之前添加了一个层,然而,如果我们能够使用动态属性与普通的JavaScript对象这将更有趣,例如:
var User = {
firstName: 'Tony',
lastName: 'Stark',
name: function() {
}
};
console.log(User.name);
User.name = 'John Doe';
console.log(User.firstName);
console.log(User.lastName);
name
被当成普通的属性使用,但在实现上却是一个函数,其获取或设置firstName
和lastName
。
如下是JavaScript的一个内置的功能,它可以帮助我们实现这个想法,看看下面的代码片段:
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
方法接受一个对象、对象名称以及需要定义的属性的getter,setter方法,所有我们要做的就是写这两个方法的主体,就是这样,我们将能够运行上面的代码中,我们会得到预期的结果:
console.log(User.name);
User.name = 'John Doe';
console.log(User.firstName);
console.log(User.lastName);
Object.defineProperty
正是我们所需要的,但我们不希望强制开发人员每次都这样写,我们可能需要提供一个 polyfill(译者注:类似于插件、扩展),用于执行额外的逻辑,或者类似的东西,在理想的情况下,我们希望提供类似于Ember的一个接口,只有一个函数是类定义的一部分,在本节中,我们将编写一个名为Computize
的工具函数来处理我们的对象并以某种方式将name函数转换为同名属性。
var Computize = function(obj) {
return obj;
}
var User = Computize({
firstName: 'Tony',
lastName: 'Stark',
name: function() {
...
}
});
我们希望使用name
方法作为setter和getter,这类似于Ember的动态属性。
现在,让我们添加我们自己的逻辑到Function
对象的原型:
Function.prototype.computed = function() {
return { computed: true, func: this };
};
一旦我们添加了上面的代码,我们就可以添加computed()
方法到每个函数定义的结尾:
name: function() {
...
}.computed()
其结果是,name
属性不再是一个函数了,而是一个对象,其包含值为true的属性computed
以及值为原函数的属性func
,真正的魔法发生在Computize
帮手的实施,它会遍历对象的所有属性,并使用Object.defineProperty来重定义那些computed属性为true的属性:
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
只适用于那些尚未定义的属性。
下面是一个使用.Computize()
函数的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
,它检测第一个参数是否存在,若存在,则分割并重新赋值到对应的属性。
我们已经提到过想要的用法,但让我们再来看一个例子:
console.log(User.name);
User.name = 'John Doe';
console.log(User.firstName);
console.log(User.lastName);
console.log(User.name);
以下是最终结果:
Tony Stark
Krasimir
Tsonev
Krasimir Tsonev
译者注:其实本节描述的本质,就是如何把对象的方法作为一个属性来使用,类似高级语言中的get和set,那么其底层的关键实现是使用了 Object.defineProperty
来重定义属性的getter和setter方法。
疯狂的 React 模板
你可能听说过Facebook的框架 React,它的设计思想是一切皆为组件,其中最有趣的是组件的定义,让我们来看看下面的例子:
<script type="text/jsx">;
var HelloMessage = React.createClass({
render: function() {
return Hello {this.props.name}
;
}
});
script>;
我们开始思考的第一件事是,这是一段JavaScript代码,但它是无效的,它有一个 render
函数,但显而易见会抛出一个语法错误,但是这里的诀窍是,这段代码放在一个自定义type的
标签里,因此浏览器不会处理它,这意味着它可以避免浏览器的语法检查。React 有自己的解析器,会把这段代码转换为有效的JavaScript代码,Facebook的工程师开发了类XML的语言JSX,JSX转换器大小是390K,并包含大约12000行代码,所以,这有点复杂。在本节中,我们将创建一个简单的函数来实现它,但功能还是相当强悍的,该解析器会把HTML模板转换为类似 React 的风格。
Facebook采取的方法是混合JavaScript代码和HTML标记,现在假设我们有下面的模板:
<script type="text/template" id="my-content">;
<div class="content">;
<h1>;<% title %>;h1>;
div>;
script>;
然后添加一个类似这样的组件:
var Component = {
title: 'Awesome template',
render: '#my-content'
}
上面的代码定义了模板的ID和引用的数据,接下来我们需要实现一个融合这两种元素的引擎,让我们命名为 Engine
并像这样启动它:
var Engine = function(comp) {
var parse = function(tplHTML) {
}
var tpl = document.querySelector(comp.render);
if(tpl) {
var html = parse(tpl.innerHTML);
return stringToDom(html);
}
}
var el = Engine(Component);
我们先得到的
标记的内容,解析并生成HTML字符串,并把生成HTML转换成一个有效的DOM元素,并返回最终结果,请注意,我们使用了本文第一节写的 stringToDom
函数。
现在,让我们来实现 parse
函数,我们的首要任务是从HTML中提取出表达式,即那些在 <%
和 %>
之间的语句,我们使用一个正则表达式来找到他们,并用一个简单的 while 循环来遍历所有的匹配:
var parse = function(tplHTML) {
var re = /<%([^%>]+)?%>/g;
while(match = re.exec(tplHTML)) {
console.log(match);
}
}
上述代码的运行结果是:
[
"<% title %>",
"title",
index: 55,
input: "">
<% title %>
"
]
我们找到了一个表达式,其内容是 title
,一个最简单直观的方法是使用JavaScript的 replace
函数替换<% title %>
为传过来的 Comp
对象的数据,然而,这只适用于简单的对象,如果我们有嵌套的对象,甚至,如果我们想使用函数,例如像这样就行不通了:
var Component = {
data: {
title: 'Awesome template',
subtitle: function() {
return 'Second title';
}
},
render: '#my-content'
}
为了避免创建一个异常复杂的解析器甚至使用Javascript发明一种新的语言,一种最佳的办法就是使用 new Function
:
var fn = new Function('arg', 'console.log(arg + 1);');
fn(2);
我们可以动态构建一个函数体并延迟执行,但我们首先得知道表达式的位置以及其后面的内容,如果我们使用一个临时数组和 cursor
,则 while
循环的代码会是这样:
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]});
cursor = match.index + match[0].length;
}
code.push(tplHTML.substr(cursor, tplHTML.length - cursor));
console.log(code);
}
在控制台的输出说明我们做对了:
[
"">
",
{ code: "title" },
"
"
]
数组 code
最终应该转变为一个字符串,这将是一个函数的主体部分,例如:
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">
class=
"content">
<h1><%
title %>
h1>
<%
while(c = colors.shift()) { %>
<p><%
c %>
p>
<%
} %>
div>
script>
如果还是仅连接表达式并期望列出颜色那就错了,考虑不再使用简单的字符串追加,而是把它们收集在数组中,下面便是修改后的解析函数:
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]});
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') {
line = line.replace(/"/g, '\\"');
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);
}
在函数的开始,我们把模板中所有语句存储在 code
数组中,之后我们遍历 code
数组,并尝试把每一条语句存储在数组 r
中,如果语句是字符串,则清除换行符和制表符,并用引号包裹,然后通过 push
方法添加到数组 r
中,如果语句是一个代码片段,我们先检查它是不是包含一个有效的JavaScript操作符,如果是无效的,那么我们不将它添加到数组,而只是纯粹地添加到新的一行,最后我们来看下最后一句 console.log
的输出结果:
var r=[];
r.push("");
r.push(title);
r.push("
");
while(c = colors.
shift()) {
r.
push(
""
);
r.
push(c);
r.
push(
"");
}
r.
push(
" ");
return r.join("");
非常好,不是吗?格式化成了正确的JavaScript代码,这将在我们 Component
的上下文中执行并产生需要的HTML标记。
最后剩下的一件事是就是创建实际的函数并运行:
body = 'with(component) {' + body + '}';
return new Function('component', body).apply(comp, [comp]);
我们把生成的代码包裹在 with
语句中,以便在 Component
的上下文中运行它,否则我们需要使用 this.title
和this.colors
来取代 title
和 colors
。
以下是在CodePen的演示结果:
Awesome template
read
green
blue
总结
在这些风靡的框架和库的背后,隐藏着及其聪明的工程师,他们发现棘手的问题并使用巧妙的解决方案,是不平凡的,甚至有点不可思议,在这篇文章中,我们揭示了这些魔法,这对我们在 JavaScript 的世界学习和使用他们的代码很有好处。
本文的代码可以从GitHub上下载。
注:本文转自公司同事的翻译。