FED实验室 - 专注WEB端开发和用户体验

浅谈querySelectorAll()

点滴Javascript 煦涵 7462℃ 0评论

一、基本介绍

querySelectorAll和querySelector方法是 W3C Selectors API规范中定义的。

引用W3C中Interface Definitions:

partial interface Document {
  Element?  querySelector(DOMString selectors);
  NodeList  querySelectorAll(DOMString selectors);
};

partial interface DocumentFragment {
  Element?  querySelector(DOMString selectors);
  NodeList  querySelectorAll(DOMString selectors);
};

partial interface Element {
  Element?  querySelector(DOMString selectors);
  NodeList  querySelectorAll(DOMString selectors);
};

二、querySelectorAll 与 getElementsByTagName

2.1 比较列表

Mthod Param Return Support
querySelectorAll() CSS Selector Static NodeLists IE8(CSS2.1),IE9+及现代浏览器
getElementsByTagName() tagName Live NodeLists IE5.5+及现代浏览器

2.2 Live NodeLists

NodeList对象 (及HTMLCollection 对象)是一个特殊类型的对象,引用DOM Level 3 spec中描述HTMLCollection对象:

NodeList and NamedNodeMap objects in the DOM are live; that is, changes to the underlying document structure are reflected in all relevant NodeList and NamedNodeMap objects. For example, if a DOM user gets a NodeList object containing the children of an Element, then subsequently adds more children to that element (or removes children, or modifies them), those changes are automatically reflected in the NodeList, without further action on the user’s part. Likewise, changes to a Node in the tree are reflected in all references to that Node in NodeList and NamedNodeMap objects.

如果运行下面示例,会出现死循环:

var i   = 0,
    div = document.getElementsByTagName("div");

while(i < div.length) {
    document.body.appendChild(document.createElement("div"));
    i ++;
}

这也就是我们平常添加节点的时候,需要先把DOM片段放到DocumentFragment类型中的原因。

2.3 Static NodeLists

querySelectorAll()方法返回一个static NodeList,引用Selectors API spec中的描述:

The NodeList object returned by the querySelectorAll() method must be static, not live ([DOM-LEVEL-3-CORE], section 1.1.1). Subsequent changes to the structure of the underlying document must not be reflected in the NodeList object. This means that the object will instead contain a list of matching Element nodes that were in the document at the time the list was created.

修改上例,将不会出现死循环:

var i   = 0,
    div = document.querySelectorAll("div");

while(i < div.length) {
    document.body.appendChild(document.createElement("div"));
    i ++;
}

2.4 为什么live nodeLists更快?

document.getElementsByTagName(),document.getElementsByName(),document.getElementsByClassName(),都是返回live nodeLists,为什么这些方法比querySelectorAll()快?
要弄懂为什么,必须研究其内部函数实现机制,getElementsByTagName和getElementsByClassName返回DynamicNodeList,意味着修改和live DOM将会自动地映射到集合中。相反,querySelectorAll返回一个充当StaticNodeList DOM的不受改变的快照。StaticNodeList必须 collect the matched elements up-front ,正是这些计算影响了querySelectorAll()的性能。

引用博文Why is getElementsByTagName() faster than querySelectorAll()?中的一段话:

The DynamicNodeList object is created by registering its existence in a cache. Essentially, the overheard to creating a new DynamicNodeList is incredibly small because it doesn’t have to do any work upfront. Whenever the DynamicNodeList is accessed, it must query the document for changes, as evidenced by the length property and the item() method (which is the same as using bracket notation).

Compare this to the StaticNodeList object, instances of which are created in another file and then populated with all of the data inside of a loop. The upfront cost to running a query on the document is much more significant than when using a DynamicNodeList instance.

If you take a look at the WebKit source code that actually creates the return value forquerySelectorAll(), you’ll see that a loop is used to get every result and build up a NodeListthat is eventually returned.

综合以上封装根据各函数长处封装函数:

(function(win){
    'use strict';

    var doc = win.document, simpleRe = /^(#?[\w-]+|\.[\w-.]+)$/, periodRe = /\./g, slice = [].slice;

    win.query = function(selector, context){
        context = context || doc;
        if(simpleRe.test(selector)){
            switch(selector.charAt(0)){
                case '#':
                    return context.getElementById(selector.substr(1));
                case '.':
                    return slice.call(context.getElementsByClassName(selector.substr(1).replace(periodRe, ' ')));
                default:
                    return slice.call(context.getElementsByTagName(selector));
            }
        }
        return slice.call(context.querySelectorAll(selector));
    };
    
})(this);

三、[].forEach.call(NodeList)

3.1 类数组转换为数组

转换类数组转换为数组参考文章:javascript类数组转换为数组。下面是其中一种实现方案:

var result = [];
[].forEach.call(document.querySelectorAll("li"), function(item, index, arr) {
    result.push(item);
});
console.log(result);

3.2 forEach方案产生的问题

1.问题一:限制重用

// cached, we can access this again
var myNodeList = document.querySelectorAll('li');

// this will only get called once
// and cannot be called again
[].forEach.call(myNodeList, function (item) {
  // :(
});

2.问题二:关注点分离
返回一个数组,可以使用slice方法。

//IE=9 返回[object Array],但是各项键值皆为undefined
var myArrayFromNodeList = [].slice.call(document.querySelectorAll('li'));

console.log(myArrayFromNodeList);
console.log(myArrayFromNodeList.prototype);

//下面为跨浏览器兼容版本

function makeArray(obj) {
    var result = [];
        
    try{
        result = [].slice.call(obj);
    }catch(e) {
        for(var i = 0, ilen = obj.length; i < ilen; i++) {
            result.push(obj[i]);
        }
    }
    return result;   
}

function getElem(str) {
    var li;
    if(document.querySelectorAll) {
        li = document.querySelectorAll(str);
    }else {
        li = document.getElementsByTagName(str);
    }
    return li;  
}
console.log(makeArray(getElem("li")));

3. 问题三:创建了不必要的数组
使用[].forEach.call()实际上创建了一个新数组,并常驻内存,为什么你要这样做?然而,可以直接使用Array.prototype.forEach。但是在一些库中卫了方便使用[]来代替。
4.问题四:非跨浏览器
forEach是ES5规范的一部分,IE8及以下版本不支持。

最后推荐的实现方式:

var forEach = function (array, callback, scope) {
  for (var i = 0; i < array.length; i++) {
    callback.call(scope, i, array[i]); 
  }
};

var myNodeList = document.querySelectorAll('li');
forEach(myNodeList, function (index, value) {
    // do something
});

四、参考链接

1.Selectors API Level 1

2.Element.querySelectorAll()

3.querySelector/querySelectorAll(浏览器支持情况)

下面是「FED实验室」的微信公众号二维码,欢迎扫描关注:

FED实验室

行文不易,如有帮助,欢迎打赏!

赞赏支持 喜欢 (3)
捐赠共勉
发表我的评论
取消评论

表情

Hi,您需要填写昵称和邮箱!

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址
(1)个小伙伴在吐槽