angular源碼分析:angular中jqLite的實現——你可以丟掉jQuery了


一、從function JQLite(element)函數開始。

function JQLite(element) {
  if (element instanceof JQLite) {  //情況1
    return element;
  }

  var argIsString;

  if (isString(element)) { //情況2
    element = trim(element);  //先去掉兩頭的空格、制表等字符
    argIsString = true;
  }
  if (!(this instanceof JQLite)) {
    if (argIsString && element.charAt(0) != '<') {  //判斷第一個字符,是不是'<'開動
      throw jqLiteMinErr('nosel', 'Looking up elements via selectors is not supported by jqLite! See: http://docs.angularjs.org/api/angular.element');
    }
    return new JQLite(element); //將自身作為構造函數重新調用
  }

//作為構造函數主要執行的部分
  if (argIsString) {  
    jqLiteAddNodes(this, jqLiteParseHTML(element));
  } else {
    jqLiteAddNodes(this, element);
  }
}

這段代碼分兩種情況處理:情況1,傳入的參數已經是一個JQLite對象,直接返回;情況2,傳入的是不是一個JQLite對象,若是字符串,先判斷第一個字符如果不是"<"拋出錯誤,將自己作為構造函數重新調用。

如果是字符串,先調用jqLiteParseHTML將字符串解析為一個element。

二、jqLiteParseHTML函數

function jqLiteParseHTML(html, context) {
  context = context || document;  //上面的代碼沒有傳入content,那么context = document;
  var parsed;

  if ((parsed = SINGLE_TAG_REGEXP.exec(html))) {
    return [context.createElement(parsed[1])];  //對於沒有屬性和子幾點得元素,直接調用createElement方法創建出來就行了
  }

  if ((parsed = jqLiteBuildFragment(html, context))) {
    return parsed.childNodes;
  }

  return [];
}

var SINGLE_TAG_REGEXP = /^<([\w-]+)\s*\/?>(?:<\/\1>|)$/;這個正則表達式分析,可得它將匹配一個沒有屬性的和子節點的元素,如果"< input />"或者"<div></div>"。而對於沒有屬性和子幾點得元素,直接調用createElement方法創建出來就行了。不然就只有調用jqLiteBuildFragment,開始復雜的構造了。

function jqLiteBuildFragment(html, context) {
  var tmp, tag, wrap,
      fragment = context.createDocumentFragment(),  //首先創建一個碎片元素作為載體
      nodes = [], i;

  if (jqLiteIsTextNode(html)) {  
    // Convert non-html into a text node
    nodes.push(context.createTextNode(html));
  } else {
    // Convert html into DOM nodes
    tmp = tmp || fragment.appendChild(context.createElement("div"));
    tag = (TAG_NAME_REGEXP.exec(html) || ["", ""])[1].toLowerCase();
    wrap = wrapMap[tag] || wrapMap._default;
    tmp.innerHTML = wrap[1] + html.replace(XHTML_TAG_REGEXP, "<$1></$2>") + wrap[2];//對應的元素用對應的標簽包裹起來。

    // Descend through wrappers to the right content
    i = wrap[0];
    while (i--) {
      tmp = tmp.lastChild;
    }

    nodes = concat(nodes, tmp.childNodes);

    tmp = fragment.firstChild;
    tmp.textContent = "";
  }

  // Remove wrapper from fragment
  fragment.textContent = "";
  fragment.innerHTML = ""; // Clear inner HTML
  forEach(nodes, function(node) {
    fragment.appendChild(node);
  });

  return fragment;
}

函數首先創建一個碎片元素作為載體,然后用function jqLiteIsTextNode(html) { return !HTML_REGEXP.test(html);}判斷元素是不是文本元素,如果是,加入到nodes這個臨時緩存,后面再處理。我們來分析一下var XHTML_TAG_REGEXP = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:-]+)[^>]*)\/>/gi;這個復雜的正則表達式,第一是以"<"開頭,第二是預搜索,表示接在"<"后面的不能是area、br、col、embed、hr、img、input、link、meta、param,第三是結尾以"/>"結尾。那么這個表達式將匹配第二中排除的自閉合標簽的 而寫成了自閉合標簽的元素。而html.replace(XHTML_TAG_REGEXP, "<$1></$2>"),就是按照xhtml規范,將這些標簽給改回到非自閉合的狀態。

三、函數jqLiteAddNodes

function jqLiteAddNodes(root, elements) {
  // THIS CODE IS VERY HOT. Don't make changes without benchmarking. //這段代碼將會被頻繁調用,沒有特別需要不要修改

  if (elements) {

    // if a Node (the most common case)
    if (elements.nodeType) {
      root[root.length++] = elements;
    } else {
      var length = elements.length;

      // if an Array or NodeList and not a Window
      if (typeof length === 'number' && elements.window !== elements) {
        if (length) {
          for (var i = 0; i < length; i++) {
            root[root.length++] = elements[i];
          }
        }
      } else {
        root[root.length++] = elements;
      }
    }
  }
}

通過上面的這段代碼,最終將dom元素轉變成了JQLite數組。

四、JQLite的原型:JQLitePrototype

1.給原型綁定函數

var JQLitePrototype = JQLite.prototype = {
  ready: function(fn) {  //定義ready函數
    var fired = false;

    function trigger() {
      if (fired) return;
      fired = true;
      fn();
    }

    // check if document is already loaded
    if (document.readyState === 'complete') {  //dom已經加載完
      setTimeout(trigger);
    } else {
      this.on('DOMContentLoaded', trigger); // works for modern browsers and IE9 //監聽dom加載完
      // we can not use jqLite since we are not done loading and jQuery could be loaded later.
      // jshint -W064
      JQLite(window).on('load', trigger); // fallback to window.onload for others
      // jshint +W064
    }
  },
  toString: function() {
    var value = [];
    forEach(this, function(e) { value.push('' + e);});
    return '[' + value.join(', ') + ']';
  },

  eq: function(index) { //定義eq
      return (index >= 0) ? jqLite(this[index]) : jqLite(this[this.length + index]);
  },

  length: 0,
  push: push,
  sort: [].sort,
  splice: [].splice
};

在這里,代碼向JQLite的原型上綁定了幾個基本的函數。集中ready用於等待dom加載完成,開始整個程序的執行。eq用於索引JQLite數組的元素。

2.向原型綁定更多的函數

forEach({
  data: jqLiteData,
  inheritedData: jqLiteInheritedData,

  scope: function(element) {...},

  isolateScope: function(element) {...},

  controller: jqLiteController,

  injector: function(element) {...},

  removeAttr: function(element, name) {...},

  hasClass: jqLiteHasClass,

  css: function(element, name, value) {...},

  attr: function(element, name, value) {...},

  prop: function(element, name, value) {...},

  text: (function() {...},

  html: function(element, value) {...},

  empty: jqLiteEmpty
}, function(fn, name) {
  /**
   * Properties: writes return selection, reads return first value
   */
  JQLite.prototype[name] = function(arg1, arg2) {
    var i, key;
    var nodeCount = this.length;

    // jqLiteHasClass has only two arguments, but is a getter-only fn, so we need to special-case it
    // in a way that survives minification.
    // jqLiteEmpty takes no arguments but is a setter.
    if (fn !== jqLiteEmpty &&
        (isUndefined((fn.length == 2 && (fn !== jqLiteHasClass && fn !== jqLiteController)) ? arg1 : arg2))) {
      if (isObject(arg1)) {

        // we are a write, but the object properties are the key/values
        for (i = 0; i < nodeCount; i++) {
          if (fn === jqLiteData) {
            // data() takes the whole object in jQuery
            fn(this[i], arg1);
          } else {
            for (key in arg1) {
              fn(this[i], key, arg1[key]);
            }
          }
        }
        // return self for chaining
        return this;
      } else {
        // we are a read, so read the first child.
        // TODO: do we still need this?
        var value = fn.$dv;
        // Only if we have $dv do we iterate over all, otherwise it is just the first element.
        var jj = (isUndefined(value)) ? Math.min(nodeCount, 1) : nodeCount;
        for (var j = 0; j < jj; j++) {
          var nodeValue = fn(this[j], arg1, arg2);
          value = value ? value + nodeValue : nodeValue;
        }
        return value;
      }
    } else {
      // we are a write, so apply to all children
      for (i = 0; i < nodeCount; i++) {
        fn(this[i], arg1, arg2);
      }
      // return self for chaining
      return this;
    }
  };
});

3.繼續綁定

forEach({
  removeData: jqLiteRemoveData,
  on: function jqLiteOn(element, type, fn, unsupported) {...},

  off: jqLiteOff,

  one: function(element, type, fn) {...},

  replaceWith: function(element, replaceNode) {...},

  children: function(element) {...},

  contents: function(element) {...},

  append: function(element, node) {...},

  prepend: function(element, node) {...},

  wrap: function(element, wrapNode) {...},

  remove: jqLiteRemove,

  detach: function(element) {...},

  after: function(element, newElement) {...},

  addClass: jqLiteAddClass,
  removeClass: jqLiteRemoveClass,

  toggleClass: function(element, selector, condition) {...},

  parent: function(element) {...},

  next: function(element) {...},

  find: function(element, selector) {...},

  clone: jqLiteClone,

  triggerHandler: function(element, event, extraParameters) {...}
}, function(fn, name) {
  /**
   * chaining functions
   */
  JQLite.prototype[name] = function(arg1, arg2, arg3) {
    var value;

    for (var i = 0, ii = this.length; i < ii; i++) {
      if (isUndefined(value)) {
        value = fn(this[i], arg1, arg2, arg3);
        if (isDefined(value)) {
          // any function which returns a value needs to be wrapped
          value = jqLite(value);
        }
      } else {
        jqLiteAddNodes(value, fn(this[i], arg1, arg2, arg3));
      }
    }
    return isDefined(value) ? value : this;
  };

  // bind legacy bind/unbind to on/off
  JQLite.prototype.bind = JQLite.prototype.on;
  JQLite.prototype.unbind = JQLite.prototype.off;
});

五、$$jqLite service

// Provider for private $$jqLite service
function $$jqLiteProvider() {
  this.$get = function $$jqLite() {
    return extend(JQLite, {
      hasClass: function(node, classes) {
        if (node.attr) node = node[0];
        return jqLiteHasClass(node, classes);
      },
      addClass: function(node, classes) {
        if (node.attr) node = node[0];
        return jqLiteAddClass(node, classes);
      },
      removeClass: function(node, classes) {
        if (node.attr) node = node[0];
        return jqLiteRemoveClass(node, classes);
      }
    });
  };
}

六、jqLiteClone、HTML5、IE8加載一起的坑

function jqLiteClone(element) {
  return element.cloneNode(true);
}

這里可以看到,它直接調用了element.cloneNode。而在ie8下這個方法在復制H5新元素(section,footer,header,em等)時,會自動變成“:element”(即:section,:footer,:header,:em),而angular中ng-if,ng-repeat等都使用了jqLiteClone。這就會導致css選擇器失敗,樣式就變得不堪入目了。筆者閱讀了jQuery的源碼,結果發現它依然是一個坑,一層h5元素的情況處理了,多層的確沒有處理。並且這個bug官方也貌似沒打算修復。不得已,寫了一個修復文件: ie8_ele_clone.js,並且把angular的jqLiteClone函數改了。

//修復ie8上的clone html5 錯誤問題
'use strict';

function ie8_ele_clone(element){
    function createSafeFragment( document ) {
        var list = nodeNames.split( "|" ),
            safeFrag = document.createDocumentFragment();

        if ( safeFrag.createElement ) {
            while ( list.length ) {
                safeFrag.createElement(
                    list.pop()
                );
            }
        }
        return safeFrag;
    }

    var html5Clone =
        document.createElement( "nav" ).cloneNode( true ).outerHTML !== "<:nav></:nav>",
        nodeNames = "abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|" +
                "header|hgroup|mark|meter|nav|output|progress|section|summary|time|video",
        rnoshimcache = new RegExp("<(?:" + nodeNames + ")[\\s/>]", "i"),
        safeFragment = createSafeFragment( document ),
        fragmentDiv = safeFragment.appendChild( document.createElement("div") );

    if(html5Clone){
        return element.cloneNode(true);
    }

    function copy(elem){
        var clone;


        if(rnoshimcache.test( "<" + elem.nodeName + ">" )){
            fragmentDiv.innerHTML = elem.outerHTML;
            fragmentDiv.removeChild( clone = fragmentDiv.firstChild );
        }
        else
        {
            clone = elem.cloneNode(true);
        }

        for(var i = 0; i < elem.children.length ; i ++){
            var tmp_node = elem.children[i];
            if(tmp_node.children.length == 0 && !rnoshimcache.test( "<" + tmp_node.nodeName + ">" ))continue;
            var copy_node = copy(tmp_node);

            var clone_replace = clone.children[i];
            clone.insertBefore(copy_node,clone_replace);
            clone.removeChild(clone_replace);
        }
        return clone;
    }

    return copy(element);
};

改后的jqLiteClone函數:

function jqLiteClone(element) {
  if(typeof ie8_ele_clone == 'function'){
    return ie8_ele_clone(element);
  }
  else
  {
    return element.cloneNode(true);
  }  
}

上一期:angular源碼分析:angular的源代碼目錄結構說明
下一期:angular源碼分析:injector.js文件分析——angular中的依賴注入式如何實現的(續)
ps,在《angular源碼分析:injector.js文件分析——angular中的依賴注入式如何實現的(續)》中,我們補充講解了《angular中的依賴注入式如何實現的》中沒有講到的部分,還有provider的各種語法糖。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM