마지막으로 제이쿼리의 메소드 체이닝에 대해 알아보겠습니다!
제이쿼리를 보면 메소드를 연속으로 사용하는 경우가 많죠. 이번 시간은 $('#app-root').children().parent()
가 어떻게 동작하는 지 알아보겠습니다! children 메소드를 호출한 후 parent를 하면 다시 원래의 $('#app-root')
가 나와야겠죠? children과 parent 메소드는 아래 코드에 들어 있습니다. 왜 굳이 이렇게 만들었는지는 모르겠습니다. (이후 버전을 보면 바뀝니다)
jQuery.extend(
init: function(){
jQuery.initDone = true;
jQuery.each( jQuery.macros.axis, function(i,n){ // (1)
jQuery.fn[ i ] = function(a) {
var ret = jQuery.map(this,n); // (2)
if ( a && a.constructor == String )
ret = jQuery.filter(a,ret).r;
return this.pushStack( ret, arguments ); // (8)
};
});
...
});
jQuery.macros = {
axis: {
parent: "a.parentNode",
ancestors: jQuery.parents,
parents: jQuery.parents,
next: "jQuery.sibling(a).next",
prev: "jQuery.sibling(a).prev",
siblings: jQuery.sibling,
children: "jQuery.sibling(a.firstChild)"
},
...
};
jQuery.init();
함수들을 jQuery.macros.axis
객체에 넣어둔 후에 jQuery.each로 각각 prototype에 추가했습니다. (1)을 보면, 매개변수 i는 parent, children 같은 속성명이고, n은 "a.parentNode", "jQuery.sibling(a.firstChild)" 같은 속성값입니다. (2)에서 jQuery.map은 n이 문자열이면 return + n으로 바꿔서 map합니다. 따라서 parent 경우는 function() { return a.parentNode; };
가 되죠. 그리고 결과를 배열에 담아 반환합니다. children도 마찬가지고요. sibling 메소드 코드를 볼까요?
sibling: function(elem, pos, not) {
var elems = [];
var siblings = elem.parentNode.childNodes; // (3)
for ( var i = 0; i < siblings.length; i++ ) { // (4)
if ( not === true && siblings[i] == elem ) continue; // (5)
if ( siblings[i].nodeType == 1 ) // (6)
elems.push( siblings[i] );
if ( siblings[i] == elem )
elems.n = elems.length - 1;
}
return jQuery.extend( elems, { // (7)
last: elems.n == elems.length - 1,
cur: pos == "even" && elems.n % 2 == 0 || pos == "odd" && elems.n % 2 || elems[pos] == elem,
prev: elems[elems.n - 1],
next: elems[elems.n + 1]
});
},
그리고 $('#app-root').children()
을 했다고 생각해봅시다. elem 변수는 "jQuery.sibling(a.firstChild)"에 따라 [#app-root의 첫 번째 자식노드]
입니다. (3)에서 siblings는 [#app-root의 자식들]
이 되고요. (4)에서 반복문을 돌며 siblings 변수를 검사하는데 (5)는 not이 undefined니까 넘어가고요.
(6)에서 siblings[i]의 nodeType이 1(1이면 Element)입니다. 따라서 모든 자식들이 elems 배열에 push됩니다. (7)은 그냥 elems가 return 된다고만 보면 됩니다. 다시 (2)로 가면 ret은 [#app-root의 자식들]
이죠. (8)(제일 위의 코드에 있습니다)에서 this.pushStack(ret, arguments)
으로 children 메소드는 끝이납니다.
이제 this.pushStack 메소드를 봅시다. a는 지금 ret이, args에는 []가 담겨있습니다.
pushStack: function(a, args) {
var fn = args && args[args.length-1]; // (8)
if ( !fn || fn.constructor != Function ) { // (9)
if ( !this.stack ) this.stack = [];
this.stack.push( this.get() ); // (10)
this.get( a ); // (11);
} else {
var old = this.get();
this.get( a );
if ( fn.constructor == Function )
return this.each( fn );
this.get( old );
}
return this;
}
(8)은 args가 빈 배열이라 fn은 undefined가 됩니다. 따라서 (9)에서 this.get()
을 한 것을 stack 배열에 넣게 되는데요. get 메소드도 봐야할 것 같네요. num은 undefined입니다.
get: function( num ) {
// Watch for when an array (of elements) is passed in
if ( num && num.constructor == Array ) { // (12)
// Use a tricky hack to make the jQuery object
// look and feel like an array
this.length = 0;
[].push.apply( this, num );
return this;
} else { // (13)
return num == undefined ?
// Return a 'clean' array
jQuery.map( this, function(a){ return a } ) : // (14)
// Return just the object
this[num];
}
}
num이 undefined이니까 (13), (14)으로 가는데, jQuery.map이 그냥 그대로 this를 return하기 때문에 (10)에서는 this.stack.push([#app-root])
가 됩니다. (11)에서 한 번 더 this.get을 호출하는데 이버에는 num이 a(ret)입니다. 따라서 (12)로 가죠. 이번에는 [].push.apply( this, num)
이라는 코드가 나오네요.
this 객체에 배열처럼 num을 push하라는 건데 이게 가능은 합니다. 이렇게 하면 결과는 { 0: #app-root의 자식노드, length: 1, stack: [#app-root], __proto__: jQuery }
가 됩니다. stack은 방금 전에 this.stack에 push한 결과입니다. 결국 이것이 결과가 되어 반환됩니다. prototype이 jQuery 객체이기 때문에 다시 모든 jQuery 메소드를 사용할 수 있습니다.
정말 복잡하죠? 저도 분석하는 데 오랜 시간이 걸렸습니다. 이제 다시 parent 메소드를 하게 되면 return a.parentNode
로부터 시작하여 다시 이 과정을 거친 뒤 #app-root가 반환됩니다.
이렇게 제이쿼리 분석을 마칩니다. 제이쿼리는 3버전까지 오면서 코드가 많이 바뀌었습니다. 하지만 그 기본 원리는 같다는 걸 기억하세요! 다음 강좌는 함수형 프로그래밍 기법 중 하나인 커링에 대해 알아보겠습니다!