소스 코드

jQuery 객체와 변수 $의 매핑

스크립트가 로드 되면서 jQuery 객체가 선언되고 해당 객체는 변수 $ 에 매핑된다.

function jQuery(a,c) {
  ...
}

// Map the jQuery namespace to the '$' one
var $ = jQuery;

jQuery.prototype 객체 변경

jQuery.fn = jQuery.prototype = {
	jquery: "$Rev: 509 $",
	size: ~
	get: ~
  ...
	add: ~
	is: ~
	domManip: ~ 
	pushStack: ~
};

jQuery.prototype 객체를 변경함과 동시에 jQuery.fn 이 참조하게 한다. 따라서 생성자 함수 jQuery 로 생성한 객체 인스턴스의 프로토타입 객체는 jQuery.fn 이 된다.

jQuery.extend()

jQuery.extend = jQuery.fn.extend = function(obj,prop) {
	if ( !prop ) { prop = obj; obj = this; }
	for ( var i in prop ) obj[i] = prop[i];
	return obj;
};

jQuery 객체와 jQuery.fn 객체 (jQuery.prototype 객체와 동일하므로 이렇게 부르기로 한다.) 에 메서드를 추가하며 이는 obj 객체에 prop 객체의 모든 프로퍼티를 추가하는 함수이다. 만약 prop 이 없을 경우 thisobj 의 프로퍼티를 추가하는 방식으로 구현된다. 이처럼 인자를 1개만 넘길 경우에 해당 메서드를 호출한 대상에게 모든 프로퍼티가 추가되므로 다음 코드는 각각 jQuery 객체와 jQuery.fn 객체의 기능을 확장하는 코드이다.

jQuery.extend();
jQuery.fn.extend();

jQuery의 기본 구조

위에서 설명한 내용들을 쉽게 그림으로 나타내면 다음과 같다.

jQuery 객체는 역시 함수객체이므로 당연히 자기자신의 메서드를 가질 수 있으며 이는 인스턴스에 특화되어 있는 것이 아닌 범용적으로 사용하는 코어 메서드로 구성된다.

jQuery 함수가 id 선택자를 다루는 방식

이제 jQuery 함수 객체의 전체적인 동작방식을 이해하기 위해 $('#jq') 와 같은 코드를 사용했다고 하자. 이 때 어떤 식으로 동작하는지 코드를 하나하나 살펴봐야 한다.

function jQuery(a,c) {
	if ( a && a.constructor == Function && jQuery.fn.ready )
		return jQuery(document).ready(a);
  ...
}

매개변수로 a 에는 '#jq' 가 넘어오며 undefined 가 아니고 생성자 함수가 Function 이 아니라 String 이기 때문에 넘어간다. 여기서 constructor 는 생성자 함수를 의미하는데 이는 프로토타입 객체에 정의되어 있으므로 여기서 프로토타입 체인을 활용한다.

a = a || jQuery.context || document;

a'#jq' 이므로 그대로 넘어간다.

if ( a.jquery )
		return $( jQuery.merge( a, [] ) );

만약 a 가 프로퍼티로 jquery 를 가질 경우 해당 메소드를 실행하라는 것인데 이는 넘어오는 인자가 jQuery 객체임을 확인하는 것이다. 그 이유는 jQuery.fn 객체에 jquery 프로퍼티가 정의되어 있기 때문이다.

if ( c && c.jquery )
		return $( c ).find(a);

위 코드 또한 방금전과 동일하게 cjQuery 객체인지 확인한다. 아니므로 넘어간다.

if ( window == this )
		return new jQuery(a,c);

생성자 함수가 아닌 일반 함수로 호출되었는지 확인하는 것으로 일반 함수로 호출되면 thiswindow 전역객체에 바인딩되기 때문에 위와 같이 검사하는 것이다. 생성자 함수로 호출되지 않았을 경우 빈 객체, 즉 새롭게 생성될 jQuery 객체를 만들어내기 위해 생성자 함수로 재호출한다. 이를 통해 jQuery.fn 객체의 프로퍼티나 메서드에도 접근할 수 있게 된다.

var m = /^[^<]*(<.+>)[^>]*$/.exec(a);
if ( m ) a = jQuery.clean( [ m[1] ] );

자바스크립트의 리터럴 정규표현식으로 ^ 는 문자열의 시작을 의미하고 $ 는 문자열의 끝을 의미한다. 또한 리터럴 정규표현식의 시작과 끝은 / 이다. 따라서 리터럴 정규식 내에서 / 를 쓰기 위해선 이스케이프 시퀀스인 \ 를 이용해 \/ 로 써야 한다. 어쨌든 exec() 메서드에 인자값을 넘겨서 인자가 HTML 태그형태인지 확인하는 것이다. 아니므로 넘어간다. (참고로 exec() 은 매칭되는 것을 찾았을 경우 배열로 반환한다.)

this.get( a.constructor == Array || a.length && !a.nodeType && a[0] != undefined && a[0].nodeType ?
		// Assume that it is an array of DOM Elements
		jQuery.merge( a, [] ) :

		// Find the matching elements and save them for later
		jQuery.find( a, c ) );

조건식이 거짓이므로 jQuery.find(a,c) 가 호출된다. 따라서 find() 가 어떻게 동작하는지 알아야 한다.

jQuery.find()

find 함수는 선택자를 인자로 받아 해당 선택자와 일치하는 DOM 객체들을 찾는 함수이다.

function( t, context ) {
  // Make sure that the context is a DOM Element
  if ( context && context.nodeType == undefined )
    context = null;

  // Set the correct context (if none is provided)
  context = context || jQuery.context || document;
  ...
}

먼저 context 변수에 대한 초기화를 진행한다. 여기서 인자로 들어오는 '#jq' 이므로 document 가 된다.

if ( t.constructor != String ) return [t];
	
if ( !t.indexOf("//") ) {
  context = context.documentElement;
  t = t.substr(2,t.length);
} else if ( !t.indexOf("/") ) {
  context = context.documentElement;
  t = t.substr(1,t.length);
  // FIX Assume the root element is right :(
  if ( t.indexOf("/") >= 1 )
    t = t.substr(t.indexOf("/"),t.length);
}

생성자 함수가 문자열이 아니라면 배열로 리턴하는데 아니니까 패스한다. 여기서 !t.indexOf("//")!t.indexOf("/")t"//""/" 로 시작하는지를 판단하며 이를 정제하는 과정이다.

var ret = [context];
var done = [];
var last = null;

while ( t.length > 0 && last != t ) {
  var r = [];
  last = t;

  t = jQuery.trim(t).replace( /^\/\//i, "" );

  var foundToken = false;

  for ( var i = 0; i < jQuery.token.length; i += 2 ) {
    var re = new RegExp("^(" + jQuery.token[i] + ")");
    var m = re.exec(t);

    if ( m ) {
      r = ret = jQuery.map( ret, jQuery.token[i+1] );
      t = jQuery.trim( t.replace( re, "" ) );
      foundToken = true;
    }
  }
  ... // while문 안 끝남

3개의 변수를 정의하고 t 의 길이가 0보다 길면 계속한다. 들어가서 trim() 으로 양쪽 공백을 제거한 뒤에 정규식으로 내부의 모든 // 문자열을 빈 문자열로 교체한다. 여기서 jQuery.token 배열에 대해 2개씩 점프하면서 반복문을 돌리는데 어떻게 생겼는지 보도록 하자.

token: [
  "\\.\\.|/\\.\\.", "a.parentNode",
  ">|/", "jQuery.sibling(a.firstChild)",
  "\\+", "jQuery.sibling(a).next",
  "~", function(a){
    var r = [];
    var s = jQuery.sibling(a);
    if ( s.n > 0 )
      for ( var i = s.n; i < s.length; i++ )
        r.push( s[i] );
    return r;
  }
]

배열이고 위의 반복문은 정규표현식에 쓸 문자열들만 본다는 것을 알 수 있다. 그렇게 각각의 요소를 가져와서 정규표현식 생성자 함수인 RegExp() 에 넣는데 이게 좀 복잡하다. token 배열의 값은 문자열인데 문자열 내에 \\ 는 정규식에서 \ 로 변환되는데 정규식에서 \ 는 이스케이프 시퀀스이고 이걸 문자열의 시작인 기호 ^ 와 동시에 괄호로 묶었으니 .. 이나 /.. 로 시작되는 문자열을 검사하는 것이라고 볼 수 있다. 따라서 이런 정규식 관련 문자열들을 돌면서 매칭되는 배열을 m 에 받는다.

여기선 mnull 이므로 그 다음을 실행한다.

  if ( !foundToken ) {
    if ( !t.indexOf(",") || !t.indexOf("|") ) {
      if ( ret[0] == context ) ret.shift();
      done = jQuery.merge( done, ret );
      r = ret = [context];
      t = " " + t.substr(1,t.length);
    } else {
      var re2 = /^([#.]?)([a-z0-9\\*_-]*)/i;
      var m = re2.exec(t);

      if ( m[1] == "#" ) {
        // Ummm, should make this work in all XML docs
        var oid = document.getElementById(m[2]);
        r = ret = oid ? [oid] : [];
        t = t.replace( re2, "" );
      } else {
        if ( !m[2] || m[1] == "." ) m[2] = "*";

        for ( var i = 0; i < ret.length; i++ )
          r = jQuery.merge( r,
                           m[2] == "*" ?
                           jQuery.getAll(ret[i]) :
                           ret[i].getElementsByTagName(m[2])
                          );
      }
    }
  }

t,| 로 시작하지 않기 때문에 else 로 넘어가서 또 다른 정규식을 본다. 해당 정규식은 문자열의 시작이 # 이나 . 로 시작될 경우 해당 문자를 캡쳐한다는 것이다. 그 이후는 문자나 숫자 또는 특수문자들을 보고 그에 맞는 패턴을 찾아서 m 에 넣는다. m 에는 인덱스 0,1,2에 각각 '#jq' , '#', 'jq' 가 들어가게 되고 이는 매칭되기 때문에 해당 if 문을 실행한다.

oid'jq' 의 DOM 객체를 찾아 넣고 이걸 배열 형태로 rret 에 넣는다. 그 다음 t 에 만든 정규식에 해당하는 문자열을 지운 결과로 초기화시킨다.

    if ( t ) {
      var val = jQuery.filter(t,r);
      ret = r = val.r;
      t = jQuery.trim(val.t);
    }
  }
	// while 문 종료
	if ( ret && ret[0] == context ) ret.shift();
	done = jQuery.merge( done, ret );
	
	return done;
}

t 가 빈 문자열이므로 while 문을 빠져나오고 ret[0]document 가 아닌 element 객체이므로 넘어가고 마지막에 done 에 중복없이 합쳐진다음(merge() 의 역할) 리턴된다.

이제 find() 의 호출이 끝났으니 이를 this.get() 의 인자로 넘겨야 한다. 이는 다음 포스팅에서 알아보자.

출처