我们每天都在使用各种各样的框架,这些框架伴随着我们每天的工作。通过使用这些框架的目的是为了解放我们,很少人去真正关心这些框架的背后都做了些什么。我也使用了不少的框架,通过这些流行框架也让我学习到了一些知识,就想把这些东西分享出来。
每个标题都是一个独立的主题,完全可以根据需要挑有兴趣的阅读。
字符串转DOM 经常使用jquery的小伙伴对下面的代码应该一点都不陌生:
1 2 3 var text = $('<div>hello, world</div>' );$('body' ).append (text)
以上代码执行的结果就是在页面增加了一个div节点。抛开jQuery, 代码可能会变得稍稍复杂:
1 2 3 4 5 6 7 8 9 10 var strToDom = function (str ) { var temp = document .createElement ('div' ); temp.innerHTML = str; return temp.childNodes [0 ]; } var text = strToDom ('<div>hello, world</div>' );document .querySelector ('body' ).appendChild (text);
这段代码,跟使用jQuery的效果是一模一样的,哈哈jQuery也不过如此嘛。如果你这么想你就错了。下面两种代码运行的有什么区别:
1 2 3 4 5 var tableTr = $('<tr><td>Simple text</td></tr>' );$('body' ).append (tableTr); var tableTr = strToDom ('<tr><td>Simple text</td></tr>' );document .querySelector ('body' ).appendChild (tableTr);
表面上看没任何的问题,如果用开发者工具看页面结构的话,会发现:
strToDom
仅仅创建了一个文本节点,而不是一个真正的tr
标签。原因是包含HTML元素的字符串通过解析器在浏览器中运行,解析器忽略了没有放置在正确的上下文中的标签, 因此我们只能得到一个文本节点。
jQuery
是如何解决这个问题的呢? 通过分析源码,我找到了下面的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 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
元素,我们需要创建两层嵌套: table
、tbody
。
有了这个Map映射表后,我们就可以拿到最终需要的标签。下面代码演示了如何从<tr><td>hello word</td></tr>
中取到tr
:
1 2 var match = /<\s*\w.*?>/g .exec (str);var tag = match[0 ].replace (/</g , '' ).replace (/>/g , '' );
剩下的就是根据合适的上下文返回DOM元素, 最终我们将strToDom
进行最终的修改:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 var strToDom = 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; var j = map[0 ]+1 ; while (j--) { element = element.lastChild ; } } else { element.innerHTML = str; element = element.lastChild ; } return element; }
通过 match != null
判断是创建的是标签还是文本节点。这一次我们通过浏览器可以创建一个有效的DOM树。最后通过使用while循环,直到取到我们想要的标签,最后返回这个标签。
AngularJS 依赖注入 当我们开始使用AngularJS时,它的双向数据绑定让人印象深刻。此外另一个神奇特征就是依赖注入。下面是一个简单的例子:
1 2 3 4 5 function TodoCtrl ($scope, $http ) { $http.get ('users/users.json' ).success (function (data ) { $scope.users = data; }); }
这是一个典型的AngularJS控制器写法:通过发起一个HTTP请求,从JSON文件获取数据,并将数据赋值给 $scope.users
。AngularJS框架会自动将$scope
和$http
注入控制器中。让我们看看它是如何实现的。
看一个例子,我们想将用户姓名显示到页面上,为了简单起见,采用的mock假数据模拟http请求:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 var dataMockup = ['John' , 'Steve' , 'David' ];var body = document .querySelector ('body' );var ajaxWrapper = { get : function (path, cb ) { console .log (path + ' requested' ); cb (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)
displayUsers(body, ajaxWrapper)
执行需要两个依赖项:body和ajaxWrapper。我们的目标是直接调用displayUsers()而没有传递参数,也能按我们期望的运行。
大部分的框架提供了依赖注入机制有一个模块,通常叫injector 。所有的依赖统一在这里注册,并提供对外访问的接口:
1 2 3 4 5 6 7 8 9 var injector = { storage : {}, register : function (name, resource ) { this .storage [name] = resource; }, resolve : function (target ) { } };
其中关键的resolve的实现:它接收一个目标对象,通过返回一个闭包,包装target并调用它。例如:
1 2 3 4 5 resolve : function (target ) { return function ( ) { target (); }; }
这样我们就可以调用我们需要的依赖的函数了。
下一步就是获取target的参数列表了,这里我引用了AngularJS的实现方式:
1 2 3 4 5 6 7 8 9 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
。它将通过目标函数转换为一个字符串,同时还将注释给去掉了, 最终得到参数信息:
1 2 3 4 5 6 7 8 9 10 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数组的第二个元素包含了所有的参数, 通过参数名称就可以得到injector 中存储的依赖项了。 下面是具体的实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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
转换成数组, 通过检查injector 中是否注册了同名的依赖,如果存在,将依赖项放入一个新的数组作为参数传递给 target
函数。
调用的代码应该是这样的:
1 2 3 4 5 injector.register ('domEl' , body); injector.register ('ajax' , ajaxWrapper); displayUsers = injector.resolve (displayUsers); displayUsers ();
这样的实现的好处是,我们将domEl和ajax注入到任意想要的函数中。我们甚至可以实现应用的配置化。不再需要将参数传来传去,代价仅仅是通过register
和 resolve
。
目前为止我们的自动注入并不是完美的,存在两个缺点:
1、函数不支持自定义参数。
2、上线代码压缩导致参数名字改变,导致无法获取正确的依赖项。
这两个问题AngualrJS
已经全部解决了,有兴趣可以看我的另一篇文章: javascript实现依赖注入的思路 ,里面详细介绍了依赖注入的完整解决方案。
Ember Computed属性 可能现在大多数人一听到计算属性,首先想到的是 Vue
中的 Computed
计算属性。其实在 Ember
框架也提供了这样一个特性,用于计算属性的属性。有点绕口,看一个官方例子吧:
1 2 3 4 5 6 7 8 9 10 11 12 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 : "Kobe" , lastName : "Bryant" }); ironMan.get ('fullName' )
Person 对象具有firstName和lastName属性。computed属性fullName返回包含person全名的连接字符串。令人奇怪的地方在于fullName的函数使用了.property
方法。 我们看一下 property
的代码:
1 2 3 4 5 6 Function .prototype .property = function ( ) { var ret = Ember .computed (this ); return ret.property .apply (ret, arguments ); };
通过添加新属性调整全局函数对象的原型。在类定义期间运行一些逻辑是一种很好的方法。
Ember
使用 getter
和 setter
来操作对象的数据。这就简化了计算属性的实现,因为我们之前还有一层要处理实际的变量。但是,如果我们能够将计算属性与普通js对象一起使用,那就更有趣了。例如:
1 2 3 4 5 6 7 8 9 10 11 12 var User = { firstName : 'Kobe' , lastName : 'Bryant' , name : function ( ) { } }; console .log (User .name ); User .name = 'LeBron James' ;console .log (User .firstName ); console .log (User .lastName );
name作为一个常规属性,本质上就是一个获取或设置firstName和lastName的函数。
JavaScript有一个内置的特性,可以帮助我们实现这个想法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 var User = { firstName : 'Kobe' , lastName : 'Bryant' , }; 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
。我们要做的就是编写这两个方法的实现逻辑。运行上面的代码,我们就能得到想要的结果:
1 2 3 4 console .log (User .name ); User .name = 'LeBron James' ;console .log (User .firstName ); console .log (User .lastName );
Object.defineProperty
虽然是我们想要的,但显然我们不想每次都这么写。在理想的情况下,我们希望提供一个接口。在本节中,我们将编写一个名为 Computize
的函数,它将处理对象并以某种方式将name函数转换为具有相同名称的属性。
1 2 3 4 5 6 7 8 9 10 var Computize = function (obj ) { return obj; } var User = Computize ({ firstName : 'Kobe' , lastName : 'Bryant' , name : function ( ) { ... } });
我们想使用name方法作为setter,同时作为getter。这类似于Ember的计算属性。
现在,我们将自己的逻辑添加到函数对象的原型中:
1 2 3 Function .prototype .computed = function ( ) { return { computed : true , func : this }; };
这样就可以在每个Function定义后直接调用computed函数了。
1 2 3 name : function ( ) { ... }.computed ()
name属性不再是一个函数,而变成一个对象: { computed: true, func: this }
。其中 computed
等于true
, func
属性指向原本的函数。
真正神奇的事情发生在Computize helper的实现中。它遍历对象的所有属性,对所有的计算属性使用object.defineproperty:
1 2 3 4 5 6 7 8 9 10 11 12 13 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; }
注意: 我们将计算属性name删除了,原因是Object.defineProperty在某些浏览器下仅对未定义的属性起作用。
下面是使用.computed()函数的用户对象的最终版本:
1 2 3 4 5 6 7 8 9 10 11 12 var User = Computize ({ firstName : 'Kobe' , lastName : 'Bryant' , 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赋值,最终返回完整的名字。
结束 在大型框架和库的背后包含着许多优秀前辈的经验。通过学习这些框架能够让我们更好理解这些框架背后的原理,能够脱离框架开发,这点很重要。