整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

简析JS中Document与CSS

果你是一名前端人员,你单单的使用jq插件显然不够,js在大多时候比较搁置,但你如果前端技术想要提升,那么js的精通对你显得很重要,本文只是他到js的document与css。

1.Document与Element和TEXT是Node的子类。

Document:树形的根部节点

Element:HTML元素的节点

TEXT:文本节点

>>HtmlElement与HtmlDocument

a:HtmlElement对象表示HTML中的一个个元素。

b:HtmlDocument对象表示 HTML 文档树的根。HTMLDocument 接口对 DOM Document 接口进行了扩展,定义 HTML 专用的属性和方法。

>>HTML的DOM对象

a:DOM Event:Event 对象代表事件的状态。

b:DOM Attribute:Attr 对象表示 HTML 属性。

c:DOM Element:Element 对象表示 HTML 元素,Element 对象可以拥有类型为元素节点、文本节点、注释节点的子节点。

d:DOM Document:每个载入浏览器的 HTML 文档都会成为 Document 对象。Document 对象使我们可以从脚本中对 HTML 页面中的所有元素进行访问。Document 对象是 Window 对象的一部分,可通过 window.document 属性对其进行访问

#log span 与 #log>span的区别?

<div id="log">
<span>Span1</span>
<span>Span2</span>
<span>Span3</span>
<div>
<span>Span4</span>
<span>Span5</span>
</div>
</div>

#log span的效果:

#log>span的效果:

2.CSS选择器

与CSS选择器的标准化一起的另外一个称做"选择器API"的W3C标准定义了获取匹配一个给定选择器的元素的JavaScript方法。该API的关键是Document方法querySelectorAll()。它接收包含一个CSS选择器的字符串参数,返回一个表示文档中匹配选择器的所有元素的NodeList对象。

querySelectorAll()返回的NodeList对象并不是实时的:它包含在调用时刻选择器所匹配的元素,不包括后续的通过JavaScript更改文档的匹配元素。

querySelectorAll()强大到即使在没有其的原生支持的浏览器中依旧可以使用CSS选择器。它是一种终极的选取方法技术。

基于jQuery的Web应用程序使用一个轻便的,跨浏览器的和querySelectorAll()等效的方法,命名为$().

jQuery的CSS选择器匹配代码已经作为一个独立的标准库提出来并发布了,命名为Sizzle。

3.HTML属性作为Element的属性

表示HTML文档元素的HTMLElement对象定义了读写属性,他们映射了元素的HTML属性。

例如:

var image=document.getElementById("my_image");

var imgurl=image.src;

可以使用<img>元素的HTMLElement对象的src属性.

4.数据集属性

有时候在HTML元素上绑定一些额外的信息。HTML5提供看一种方法。

任意一”data-*“为前缀的小写的属性名字都是合法的。

5.Web浏览器很擅长解析HTML,通常设置innerHTML效率非常高。但是:对innerHTML属性使用”+=“操作符重复追加文本时效率低下,因为它既要序列化又要解析。

插入节点方法:appendChild()与insertBefore()的异同?

6.视口坐标与文档坐标

视口坐标:指的是显式文档内容的那一部分(也即我们在浏览器中能看到的那部分区域),不包括浏览器的外壳元素,比如菜单栏,工具条等。

文档坐标:指的是包含整个页面的整个部分(也即我们在浏览器中能看的那部分区域以及需要依靠滚动条来滚动查看的区域)。

该书中提供了几个实用的方法:

a:查询窗口滚动条的位置

//查询窗口滚动条的位置

functon getScrollOffsets(w){

w = w || window;

var sLeft,sTop;

if(w.pageXOffset != null) {

sLeft = w.pageXOffset;

sTop = w.pageYOffset;

return {x:sLeft,y:sTop};

}

b:查询窗口的视口尺寸

//查询窗口的视口尺寸

function getViewportSize(w){

w = w || window;

var cWidth,cHeight;

if(w.innerWidth != null){

cWidth = w.innerWidht;

cHeight = w.innerHeight;

return {w:cWidth,h:w.cHeight};

}

if(document.compatMode == "CSS1Compat"){

cWidth = document.documentElement.clientWidth;

cHeight = doument.documentElement.clientHeight;

return {w:cWidth,h:w.cHeight};

}else if(document.compatMode == "BackCompat"){

cWidth = document.body.clientWidth;

cHeight = doument.body.clientHeight;

return {w:cWidth,h:w.cHeight};

}

}

7.查询元素的几何尺寸

getBoundingClientRect()方法

具体见乱炖中的这篇文章:使用getBoundingClientRect()来获取页面元素的位置

需要注意的是:getBoundingClientRect这个方法不同于getElementByTagName()这样的DOM方法返回的结果是实时的,但是getBoundingClientRect却不是,它类似于一种静态快照。用户滚动的时候,并不会去实时更新。

getBoundingClientRect()与getClientRects()的区别?

8.判断元素在某点

elementFromPoint()能够用来判断判定视口中的指定位置上有什么元素。

传递X与Y坐标(使用视口或窗口坐标而不是文档坐标)

它有一个取代者,那就是target属性。

9.滚动

Window的scrollBy()与scroll()和scrollTo()类似。

只是scrollBy的参数是相对的,并在当前滚动条的偏移量上增加。

如:

scrollIntoView()的使用?

offsetWidth()

offsetHeight()

offsetLeft()

offsetTop()

offsetParent()

clientWidth()

clientHeight()

clientLeft()

clientTop()

scrollWidth()

scrollHeight()

scrollLeft()

scrollTop()

Client他就是Web浏览器客户端-专指它定义的窗口或视口。

10.HTML表单

服务器端程序是基于表单提交动作的

客户端程序是基于事件的

JavaScript的From。

切图网(qietu.com)是一家专门从事web前端开发的公司,专注we前端开发,关注用户体验,欢迎订阅微信公众号:qietuwang

什么是DOM

文档对象模型 (DOM) 是HTML和XML文档的编程接口。它提供了对文档的结构化的表述,并定义了一种方式可以使从程序中对该结构进行访问,从而改变文档的结构,样式和内容。文档对象模型 (DOM) 是对HTML文件的另一种展示,通俗地说,一个HTML 文件,我们可以用编辑器以代码的形式展示它,也可以用浏览器以页面的形式展示它,同一份文件通过不同的展示方式,就有了不一样的表现形式。而DOM 将文档解析为一个由节点和对象(包含属性和方法的对象)组成的结构集合。简言之,它会将web页面和脚本或程序语言连接起来,我们可以使用脚本或者程序语言通过DOM 来改变或者控制web页面。

2 如何访问DOM

我们可以通过JavaScript 来调用document和window元素的API来操作文档或者获取文档的信息。

3 Node

Node 是一个接口,有许多接口都从Node 继承方法和属性:Document, Element, CharacterData (which Text, Comment, and CDATASection inherit), ProcessingInstruction, DocumentFragment, DocumentType, Notation, Entity, EntityReference。Node 有一个nodeType的属性表示Node 的类型,是一个整数,不同的值代表不同的节点类型。具体如下表所示:

节点类型常量

已弃用的节点类型常量

假设我们要判断一个Node 是不是一个元素,通过查表可知元素的nodeType属性值为1,代码可以这么写:

复制代码if(X.nodeType === 1){
  console.log('X 是一个元素');
}

在Node 类型中,比较常用的就是element,text,comment,document,document_fragment这几种类型。

3.1 Element

Element提供了对元素标签名,子节点和特性的访问,我们常用HTML元素比如div,span,a等标签就是element中的一种。Element有下面几条特性:(1)nodeType为1(2)nodeName为元素标签名,tagName也是返回标签名(3)nodeValue为null(4)parentNode可能是Document或Element(5)子节点可能是Element,Text,Comment,Processing_Instruction,CDATASection或EntityReference

3.2 Text

Text表示文本节点,它包含的是纯文本内容,不能包含html代码,但可以包含转义后的html代码。Text有下面的特性:(1)nodeType为3(2)nodeName为#text(3)nodeValue为文本内容(4)parentNode是一个Element(5)没有子节点

3.3 Comment

Comment表示HTML文档中的注释,它有下面的几种特征:(1)nodeType为8(2)nodeName为#comment(3)nodeValue为注释的内容(4)parentNode可能是Document或Element(5)没有子节点

3.4 Document

Document表示文档,在浏览器中,document对象是HTMLDocument的一个实例,表示整个页面,它同时也是window对象的一个属性。Document有下面的特性:(1)nodeType为9(2)nodeName为#document(3)nodeValue为null(4)parentNode为null(5)子节点可能是一个DocumentType或Element

3.5 DocumentFragment

DocumentFragment是所有节点中唯一一个没有对应标记的类型,它表示一种轻量级的文档,可能当作一个临时的仓库用来保存可能会添加到文档中的节点。DocumentFragment有下面的特性:(1)nodeType为11(2)nodeName为#document-fragment(3)nodeValue为null(4)parentNode为null

4 节点创建型API

用如其名,这类API是用来创建节点的

4.1 createElement

createElement通过传入指定的一个标签名来创建一个元素,如果传入的标签名是一个未知的,则会创建一个自定义的标签,注意:IE8以下浏览器不支持自定义标签。

语法

复制代码  let element = document.createElement(tagName);

使用createElement要注意:通过createElement创建的元素并不属于HTML文档,它只是创建出来,并未添加到HTML文档中,要调用appendChild或insertBefore等方法将其添加到HTML文档树中。

例子:

复制代码  let elem = document.createElement("div");
  elem.id = 'test';
  elem.style = 'color: red';
  elem.innerHTML = '我是新创建的节点';
  document.body.appendChild(elem);

运行结果为:


4.2 createTextNode

createTextNode用来创建一个文本节点

语法

复制代码  var text = document.createTextNode(data);

createTextNode接收一个参数,这个参数就是文本节点中的文本,和createElement一样,创建后的文本节点也只是独立的一个节点,同样需要appendChild将其添加到HTML文档树中

例子:

复制代码  var node = document.createTextNode("我是文本节点");
  document.body.appendChild(node);

运行结果为:


4.3 cloneNode

cloneNode返回调用该方法的节点的一个副本

语法

复制代码  var dupNode = node.cloneNode(deep);

node 将要被克隆的节点dupNode 克隆生成的副本节点deep(可选)是否采用深度克隆,如果为true,则该节点的所有后代节点也都会被克隆,如果为false,则只克隆该节点本身.

这里有几点要注意:(1)和createElement一样,cloneNode创建的节点只是游离有HTML文档外的节点,要调用appendChild方法才能添加到文档树中(2)如果复制的元素有id,则其副本同样会包含该id,由于id具有唯一性,所以在复制节点后必须要修改其id(3)调用接收的deep参数最好传入,如果不传入该参数,不同浏览器对其默认值的处理可能不同

注意如果被复制的节点绑定了事件,则副本也会跟着绑定该事件吗?这里要分情况讨论:(1)如果是通过addEventListener或者比如onclick进行绑定事件,则副本节点不会绑定该事件(2)如果是内联方式绑定比如:<div onclick="showParent()"></div>,这样的话,副本节点同样会触发事件。

例子:

复制代码<body>
  <div id="parent">
    我是父元素的文本
    <br/>
    <span>
        我是子元素
    </span>
  </div>
  <button id="btnCopy">复制</button>
</body>
<script>
  var parent = document.getElementById("parent");
  document.getElementById("btnCopy").onclick = function(){
  	var parent2 = parent.cloneNode(true);
  	parent2.id = "parent2";
  	document.body.appendChild(parent2);
  }
</script>

运行结果为:


4.4 createDocumentFragment

DocumentFragments 是DOM节点。它们不是主DOM树的一部分。通常的用例是创建文档片段,将元素附加到文档片段,然后将文档片段附加到DOM树。在DOM树中,文档片段被其所有的子元素所代替。因为文档片段存在于内存中,并不在DOM树中,所以将子元素插入到文档片段时不会引起页面回流(reflow)(对元素位置和几何上的计算)。因此,使用文档片段document fragments 通常会起到优化性能的作用。

语法

复制代码  let fragment = document.createDocumentFragment();

例子:

复制代码<body>
  <ul id="ul"></ul>
</body>
<script>
  (function()
  {
    var start = Date.now();
    var str = '', li;
    var ul = document.getElementById('ul');
    var fragment = document.createDocumentFragment();
    for(var i=0; i<1000; i++)
    {
        li = document.createElement('li');
        li.textContent = '第'+(i+1)+'个子节点';
        fragment.appendChild(li);
    }
    ul.appendChild(fragment);
  })();
</script>

运行结果为:


4.5 节点创建型API总结

节点创建型API主要包括createElement,createTextNode,cloneNode和createDocumentFragment四个方法,需要注意下面几点:(1)它们创建的节点只是一个孤立的节点,要通过appendChild添加到文档中(2)cloneNode要注意如果被复制的节点是否包含子节点以及事件绑定等问题(3)使用createDocumentFragment来解决添加大量节点时的性能问题

5 页面修改型API

前面我们提到节点创建型API,它们只是创建节点,并没有真正修改到页面内容,而是要调用·appendChild·来将其添加到文档树中。我在这里将这类会修改到页面内容归为一类。修改页面内容的api主要包括:appendChild,insertBefore,removeChild,replaceChild。

5.1 appendChild

appendChild我们在前面已经用到多次,就是将指定的节点添加到调用该方法的节点的子元素的末尾。

语法

复制代码  parent.appendChild(child);

child节点将会作为parent节点的最后一个子节点。appendChild这个方法很简单,但是还有有一点需要注意:如果被添加的节点是一个页面中存在的节点,则执行后这个节点将会添加到指定位置,其原本所在的位置将移除该节点,也就是说不会同时存在两个该节点在页面上,相当于把这个节点移动到另一个地方。如果child绑定了事件,被移动时,它依然绑定着该事件。

例子:

复制代码<body>
  <div id="child">
    要被添加的节点
  </div>
  <br/>
  <br/>
  <br/>
  <div id="parent">
    要移动的位置
  </div>		
  <input id="btnMove" type="button" value="移动节点" />
</body>
<script>
  document.getElementById("btnMove").onclick = function(){
	  var child = document.getElementById("child");
 	  document.getElementById("parent").appendChild(child);
  } 
</script>

运行结果:


5.2 insertBefore

insertBefore用来添加一个节点到一个参照节点之前

语法

复制代码  parentNode.insertBefore(newNode,refNode);

parentNode表示新节点被添加后的父节点newNode表示要添加的节点refNode表示参照节点,新节点会添加到这个节点之前

例子:

复制代码<body>
  <div id="parent">
    父节点
    <div id="child">				
        子元素
    </div>
  </div>
  <input type="button" id="insertNode" value="插入节点" />
</body>
<script>
  var parent = document.getElementById("parent");
  var child = document.getElementById("child");
  document.getElementById("insertNode").onclick = function(){
  	var newNode = document.createElement("div");
  	newNode.textContent = "新节点"
  	parent.insertBefore(newNode,child);
  }
</script>

运行结果:


关于第二个参数参照节点还有几个注意的地方:(1)refNode是必传的,如果不传该参数会报错(2)如果refNode是undefined或null,则insertBefore会将节点添加到子元素的末尾

5.3 removeChild

删除指定的子节点并返回

语法

复制代码  var deletedChild = parent.removeChild(node);

deletedChild指向被删除节点的引用,它等于node,被删除的节点仍然存在于内存中,可以对其进行下一步操作。注意:如果被删除的节点不是其子节点,则程序将会报错。我们可以通过下面的方式来确保可以删除:

复制代码if(node.parentNode){
    node.parentNode.removeChild(node);
}

运行结果:

通过节点自己获取节点的父节点,然后将自身删除


5.4 replaceChild

replaceChild用于使用一个节点替换另一个节点

语法

复制代码  parent.replaceChild(newChild,oldChild);

newChild是替换的节点,可以是新的节点,也可以是页面上的节点,如果是页面上的节点,则其将被转移到新的位置oldChild是被替换的节点

例子:

复制代码<body>
  <div id="parent">
    父节点
    <div id="child">				
        子元素
    </div>
  </div>
  <input type="button" id="insertNode" value="替换节点" />
</body>
<script>
  var parent = document.getElementById("parent");
  var child = document.getElementById("child");
  document.getElementById("insertNode").onclick = function(){
  	var newNode = document.createElement("div");
  	newNode.textContent = "新节点"
  	parent.replaceChild(newNode,child)
  }

运行结果:


5.5 页面修改型API总结

页面修改型API主要是这四个接口,要注意几个特点:(1)不管是新增还是替换节点,如果新增或替换的节点是原本存在页面上的,则其原来位置的节点将被移除,也就是说同一个节点不能存在于页面的多个位置(2)节点本身绑定的事件会不会消失,会一直保留着。

6 节点查询型API

6.1 document.getElementById

这个接口很简单,根据元素id返回元素,返回值是Element类型,如果不存在该元素,则返回null

语法

复制代码  var element = document.getElementById(id);

使用这个接口有几点要注意:(1)元素的Id是大小写敏感的,一定要写对元素的id(2)HTML文档中可能存在多个id相同的元素,则返回第一个元素(3)只从文档中进行搜索元素,如果创建了一个元素并指定id,但并没有添加到文档中,则这个元素是不会被查找到的

例子:

复制代码<body>
  <p id="para1">Some text here</p>
  <button onclick="changeColor('blue');">blue</button>
  <button onclick="changeColor('red');">red</button>
</body>
<script>
  function changeColor(newColor) {
    var elem = document.getElementById("para1");
    elem.style.color = newColor;
  }
</script>

运行结果:


6.2 document.getElementsByTagName

返回一个包括所有给定标签名称的元素的HTML集合HTMLCollection。 整个文件结构都会被搜索,包括根节点。返回的 HTML集合是动态的, 意味着它可以自动更新自己来保持和 DOM 树的同步而不用再次调用document.getElementsByTagName()

语法

复制代码  var elements = document.getElementsByTagName(name);

(1)如果要对HTMLCollection集合进行循环操作,最好将其长度缓存起来,因为每次循环都会去计算长度,暂时缓存起来可以提高效率(2)如果没有存在指定的标签,该接口返回的不是null,而是一个空的HTMLCollection(3)name是一个代表元素的名称的字符串。特殊字符 "*" 代表了所有元素。

例子:

复制代码<body>
  <div>div1</div>
  <div>div2</div>	
  <input type="button" value="显示数量" id="btnShowCount"/>
  <input type="button" value="新增div" id="btnAddDiv"/>	
</body>
<script>
  var divList = document.getElementsByTagName("div");
  document.getElementById("btnAddDiv").onclick = function(){
  	var div = document.createElement("div");
  	div.textContent ="div" + (divList.length+1);
  	document.body.appendChild(div);
  }
  document.getElementById("btnShowCount").onclick = function(){
    alert(divList.length);
  }
</script>

这段代码中有两个按钮,一个按钮是显示HTMLCollection元素的个数,另一个按钮可以新增一个div标签到文档中。前面提到HTMLCollcetion元素是即时的表示该集合是随时变化的,也就是是文档中有几个div,它会随时进行变化,当我们新增一个div后,再访问HTMLCollection时,就会包含这个新增的div。

运行结果:


6.3 document.getElementsByName

getElementsByName主要是通过指定的name属性来获取元素,它返回一个即时的NodeList对象

语法

复制代码  var elements = document.getElementsByName(name) 

使用这个接口主要要注意几点:(1)返回对象是一个即时的NodeList,它是随时变化的(2)在HTML元素中,并不是所有元素都有name属性,比如div是没有name属性的,但是如果强制设置div的name属性,它也是可以被查找到的(3)在IE中,如果id设置成某个值,然后传入getElementsByName的参数值和id值一样,则这个元素是会被找到的,所以最好不好设置同样的值给id和name

例子:

复制代码<script type="text/javascript">
  function getElements()
   {
   var x=document.getElementsByName("myInput");
   alert(x.length);
   }
</script>
<body>
  <input name="myInput" type="text" size="20" /><br />
  <input name="myInput" type="text" size="20" /><br />
  <input name="myInput" type="text" size="20" /><br />
  <br />
  <input type="button" onclick="getElements()" value="How many elements named 'myInput'?" />
</body>

运行结果:


6.4 document.getElementsByClassName

这个API是根据元素的class返回一个即时的HTMLCollection

语法

复制代码  var elements = document.getElementsByClassName(names); // or:
  var elements = rootElement.getElementsByClassName(names);
  • elements是一个实时集合,包含了找到的所有元素
  • names是一个字符串,表示要匹配的类名列表;类名通过空格分隔
  • getElementsByClassName可以在任何元素上调用,不仅仅是document。调用这个方法的元素将作为本次查找的根元素

这个接口有下面几点要注意:(1)返回结果是一个即时的HTMLCollection,会随时根据文档结构变化(2)IE9以下浏览器不支持(3)如果要获取2个以上classname,可传入多个classname,每个用空格相隔,例如

复制代码  var elements = document.getElementsByClassName("test1 test2");

例子:

  • 获取所有class为 'test' 的元素
复制代码  var elements = document.getElementsByClassName('test');
  • 获取所有class同时包括 'red' 和 'test' 的元素
复制代码  var elements = document.getElementsByClassName('red test');
  • 在id为'main'的元素的子节点中,获取所有class为'test'的元素
复制代码  var elements = document.getElementById('main').getElementsByClassName('test');
  • 我们还可以对任意的HTMLCollection 使用Array.prototype的方法,调用时传递HTMLCollection 作为方法的参数。这里我们将查找到所有class为'test'的div元素:
复制代码  var testElements = document.getElementsByClassName('test');
  var testDivs = Array.prototype.filter.call(testElements, function(testElement){
    return testElement.nodeName === 'DIV';;
  });

6.5 document.querySelector和document.querySelectorAll

这两个API很相似,通过css选择器来查找元素,注意选择器要符合CSS选择器的规则

  • 6.5.1 document.querySelector

document.querySelector返回第一个匹配的元素,如果没有匹配的元素,则返回null

语法

复制代码  var element = document.querySelector(selectors);

注意,由于返回的是第一个匹配的元素,这个api使用的深度优先搜索来获取元素。

例子:

复制代码<body>
  <div>
    <div>
      <span class="test">第三级的span</span>	
    </div>
  </div>
  <div class="test">			
    同级的第二个div
  </div>
  <input type="button" id="btnGet" value="获取test元素" />
</body>
<script>
  document.getElementById("btnGet").addEventListener("click",function(){
    var element = document.querySelector(".test");
    alert(element.textContent);
  })
</script>

两个class都包含“test”的元素,一个在文档树的前面,但是它在第三级,另一个在文档树的后面,但它在第一级,通过querySelector获取元素时,它通过深度优先搜索,拿到文档树前面的第三级的元素。运行结果:


  • 6.5.2 document.querySelectorAll返回的是所有匹配的元素,而且可以匹配多个选择符

语法

复制代码  var elementList = document.querySelectorAll(selectors);
  • elementList是一个静态的NodeList类型的对象
  • selectors是一个由逗号连接的包含一个或多个CSS选择器的字符串
  • 如果selectors参数中包含CSS伪元素,则返回一个空的elementList

例子:

复制代码  var matches = document.querySelectorAll("div.note, div.alert");

返回一个文档中所有的class为"note"或者"alert"的div元素

复制代码<body>
  <div class="test">
    class为test
  </div>
  <div id="test">
    id为test
  </div>
  <input id="btnShow" type="button" value="显示内容" />
</body>
<script>
  document.getElementById("btnShow").addEventListener("click",function(){
	var elements = document.querySelectorAll("#test,.test");	
	for(var i = 0,length = elements.length;i<length;i++){
		alert(elements[i].textContent);
	}	
  })
</script>

这段代码通过querySelectorAll,使用id选择器和class选择器选择了两个元素,并依次输出其内容。要注意两点:(1)querySelectorAll也是通过深度优先搜索,搜索的元素顺序和选择器的顺序无关(2)返回的是一个非即时的NodeList,也就是说结果不会随着文档树的变化而变化兼容性问题:querySelector和querySelectorAll在ie8以下的浏览器不支持。

运行结果:


7 节点关系型API

在html文档中的每个节点之间的关系都可以看成是家谱关系,包含父子关系,兄弟关系等等

7.1 父关系型API

7.1.1 parentNode

每个节点都有一个parentNode属性,它表示元素的父节点。Element的父节点可能是Element,Document或DocumentFragment

7.1.2 parentElement

返回元素的父元素节点,与parentNode的区别在于,其父节点必须是一个Element,如果不是,则返回null

7.2 子关系型APPI

7.2.1 childNodes

返回一个即时的NodeList,表示元素的子节点列表,子节点可能会包含文本节点,注释节点等

7.2.2 children:

一个即时的HTMLCollection,子节点都是Element,IE9以下浏览器不支持children属性为只读属性,对象类型为HTMLCollection,你可以使用elementNodeReference.children[1].nodeName来获取某个子元素的标签名称

7.2.3 firstChild

只读属性返回树中节点的第一个子节点,如果节点是无子节点,则返回 null

7.2.4 lastChild

返回当前节点的最后一个子节点。如果父节点为一个元素节点,则子节点通常为一个元素节点,或一个文本节点,或一个注释节点。如果没有子节点,则返回null

7.2.5 hasChildNodes

返回一个布尔值,表明当前节点是否包含有子节点.

7.3 兄弟关系型API

7.3.1 previousSibling

返回当前节点的前一个兄弟节点,没有则返回nullGecko内核的浏览器会在源代码中标签内部有空白符的地方插入一个文本结点到文档中.因此,使用诸如Node.firstChild和Node.previousSibling之类的方法可能会引用到一个空白符文本节点, 而不是使用者所预期得到的节点

7.3.2 previousElementSibling

previousElementSibling返回当前元素在其父元素的子元素节点中的前一个元素节点,如果该元素已经是第一个元素节点,则返回null,该属性是只读的。注意IE9以下浏览器不支持

7.3.3 nextSibling

Node.nextSibling是一个只读属性,返回其父节点的childNodes列表中紧跟在其后面的节点,如果指定的节点为最后一个节点,则返回nullGecko内核的浏览器会在源代码中标签内部有空白符的地方插入一个文本结点到文档中.因此,使用诸如Node.firstChild和Node.previousSibling之类的方法可能会引用到一个空白符文本节点, 而不是使用者所预期得到的节点

7.3.4 nextElementSibling

nextElementSibling返回当前元素在其父元素的子元素节点中的后一个元素节点,如果该元素已经是最后一个元素节点,则返回null,该属性是只读的。注意IE9以下浏览器不支持

8 元素属性型API

8.1 setAttribute

设置指定元素上的一个属性值。如果属性已经存在,则更新该值; 否则将添加一个新的属性用指定的名称和值

语法

复制代码  element.setAttribute(name, value);

其中name是特性名,value是特性值。如果元素不包含该特性,则会创建该特性并赋值。

例子:

复制代码<body>
  <div id="div1">ABC</div>
</body>
<script>  
  let div1 = document.getElementById("div1"); 
  div1.setAttribute("align", "center");
</script>

运行结果:


如果元素本身包含指定的特性名为属性,则可以世界访问属性进行赋值,比如下面两条代码是等价的:

复制代码  element.setAttribute("id","test");
  element.id = "test";

8.2 getAttribute

getAttribute()返回元素上一个指定的属性值。如果指定的属性不存在,则返回null或""(空字符串)

语法

复制代码  let attribute = element.getAttribute(attributeName);  

attribute是一个包含attributeName属性值的字符串。attributeName是你想要获取的属性值的属性名称

例子:

复制代码<body>
  <div id="div1">ABC</div>
</body>
<script>  
  let div1 = document.getElementById("div1");
  let align = div1.getAttribute("align");
  alert(align);
</script>  

运行结果:


8.3 removeAttribute

removeAttribute()从指定的元素中删除一个属性

语法

复制代码  element.removeAttribute(attrName)

attrName是一个字符串,将要从元素中删除的属性名

例子:

复制代码<body>
  <div id="div1" style="color:red" width="200px">ABC
   </div>
</body>
<script>  
  let div = document.getElementById("div1")
  div.removeAttribute("style");
</script>

在运行之前div有个style="color:red"的属性,在运行之后这个属性就被删除了

运行结果:


9 元素样式型API

9.1 window.getComputedStyle

Window.getComputedStyle()方法给出应用活动样式表后的元素的所有CSS属性的值,并解析这些值可能包含的任何基本计算假设某个元素并未设置高度而是通过其内容将其高度撑开,这时候要获取它的高度就要用到getComputedStyle

语法

复制代码  var style = window.getComputedStyle(element[, pseudoElt]);

element是要获取的元素,pseudoElt指定一个伪元素进行匹配。返回的style是一个CSSStyleDeclaration对象。通过style可以访问到元素计算后的样式

9.2 getBoundingClientRect

getBoundingClientRect用来返回元素的大小以及相对于浏览器可视窗口的位置

语法

复制代码  var clientRect = element.getBoundingClientRect();

clientRect是一个DOMRect对象,包含left,top,right,bottom,它是相对于可视窗口的距离,滚动位置发生改变时,它们的值是会发生变化的。除了IE9以下浏览器,还包含元素的height和width等数据

9.3 直接修改元素的样式

例子:

复制代码  elem.style.color = 'red';
  elem.style.setProperty('font-size', '16px');
  elem.style.removeProperty('color');

9.4 动态添加样式规则

例子:

复制代码  var style = document.createElement('style');
  style.innerHTML = 'body{color:red} #top:hover{background-color: red;color: white;}';
  document.head.appendChild(style););

10 总结

JavaScript中的API太多了,将这些API记住并熟练使用对JavaScript的学习是有很大的帮助

作者:yyzclyang

链接:https://juejin.cn/post/6844903604445249543

开发分布式高并发系统时有三把利器用来保护系统:缓存、降级、限流

缓存

缓存的目的是提升系统访问速度和增大系统处理容量

降级

降级是当服务出现问题或者影响到核心流程时,需要暂时屏蔽掉,待高峰或者问题解决后再打开

限流

限流的目的是通过对并发访问/请求进行限速,或者对一个时间窗口内的请求进行限速来保护系统,一旦达到限制速率则可以拒绝服务、排队或等待、降级等处理

问题描述

1、某天A君突然发现自己的接口请求量突然涨到之前的10倍,没多久该接口几乎不可使用,并引发连锁反应导致整个系统崩溃。如何应对这种情况呢?生活给了我们答案:比如老式电闸都安装了保险丝,一旦有人使用超大功率的设备,保险丝就会烧断以保护各个电器不被强电流给烧坏。同理我们的接口也需要安装上“保险丝”,以防止非预期的请求对系统压力过大而引起的系统瘫痪,当流量过大时,可以采取拒绝或者引流等机制。整编:微信公众号,搜云库技术团队,ID:souyunku

2、缓存的目的是提升系统访问速度和增大系统能处理的容量,可谓是抗高并发流量的银弹;而降级是当服务出问题或者影响到核心流程的性能则需要暂时屏蔽掉,待高峰或者问题解决后再打开;而有些场景并不能用缓存和降级来解决,比如稀缺资源(秒杀、抢购)、写服务(如评论、下单)、频繁的复杂查询(评论的最后几页),因此需有一种手段来限制这些场景的并发/请求量,即限流。

3、系统在设计之初就会有一个预估容量,长时间超过系统能承受的TPS/QPS阈值,系统可能会被压垮,最终导致整个服务不够用。为了避免这种情况,我们就需要对接口请求进行限流。

4、限流的目的是通过对并发访问请求进行限速或者一个时间窗口内的的请求数量进行限速来保护系统,一旦达到限制速率则可以拒绝服务、排队或等待。

5、一般开发高并发系统常见的限流模式有控制并发和控制速率,一个是限制并发的总数量(比如数据库连接池、线程池),一个是限制并发访问的速率(如nginx的limitconn模块,用来限制瞬时并发连接数),另外还可以限制单位时间窗口内的请求数量(如Guava的RateLimiter、nginx的limitreq模块,限制每秒的平均速率)。其他还有如限制远程接口调用速率、限制MQ的消费速率。另外还可以根据网络连接数、网络流量、CPU或内存负载等来限流。

相关概念:

PV:

page view 页面总访问量,每刷新一次记录一次。

UV:

unique view 客户端主机访问,指一天内相同IP的访问记为1次。

QPS:

query per second,即每秒访问量。qps很大程度上代表了系统的繁忙度,没次请求可能存在多次的磁盘io,网络请求,多个cpu时间片,一旦qps超过了预先设置的阀值,可以考量扩容增加服务器,避免访问量过大导致的宕机。整编:微信公众号,搜云库技术团队,ID:souyunku

RT:

response time,每次请求的响应时间,直接决定用户体验性。

本文主要介绍应用级限流方法,分布式限流、流量入口限流(接入层如NGINX limitconn和limitreq 模块)。

应用级限流

一、控制并发数量

属于一种较常见的限流手段,在实际应用中可以通过信号量机制(如Java中的Semaphore)来实现。操作系统的信号量是个很重要的概念,Java 并发库 的Semaphore 可以很轻松完成信号量控制,Semaphore可以控制某个资源可被同时访问的个数,通过 acquire() 获取一个许可,如果没有就等待,而 release() 释放一个许可。

举个例子,我们对外提供一个服务接口,允许最大并发数为10,代码实现如下:

public class DubboService {
    private final Semaphore permit = new Semaphore(10, true);
    public void process(){
        try{            permit.acquire();            //业务逻辑处理
        } catch (InterruptedException e) {            e.printStackTrace();        } finally {            permit.release();        }    }}

在以上代码中,虽然有30个线程在执行,但是只允许10个并发的执行。Semaphore的构造方法Semaphore(int permits) 接受一个整型的数字,表示可用的许可证数量。Semaphore(10)表示允许10个线程获取许可证,也就是最大并发数是10。Semaphore的用法也很简单,首先线程使用Semaphore的acquire()获取一个许可证,使用完之后调用release()归还许可证,还可以用tryAcquire()方法尝试获取许可证,信号量的本质是控制某个资源可被同时访问的个数,在一定程度上可以控制某资源的访问频率,但不能精确控制,控制访问频率的模式见下文描述。

二、控制访问速率

在工程实践中,常见的是使用令牌桶算法来实现这种模式,常用的限流算法有两种:漏桶算法和令牌桶算法。

漏桶算法

漏桶算法思路很简单,水(请求)先进入到漏桶里,漏桶以一定的速度出水,当水流入速度过大会直接溢出,可以看出漏桶算法能强行限制数据的传输速率。

对于很多应用场景来说,除了要求能够限制数据的平均传输速率外,还要求允许某种程度的突发传输。这时候漏桶算法可能就不合适了,令牌桶算法更为适合。

令牌桶算法

如图所示,令牌桶算法的原理是系统会以一个恒定的速度往桶里放入令牌,而如果请求需要被处理,则需要先从桶里获取一个令牌,当桶里没有令牌可取时,则拒绝服务,令牌桶算法通过发放令牌,根据令牌的rate频率做请求频率限制,容量限制等。整编:微信公众号,搜云库技术团队,ID:souyunku

在Wikipedia上,令牌桶算法是这么描述的:

1、每过1/r秒桶中增加一个令牌。

2、桶中最多存放b个令牌,如果桶满了,新放入的令牌会被丢弃。

3、当一个n字节的数据包到达时,消耗n个令牌,然后发送该数据包。

4、如果桶中可用令牌小于n,则该数据包将被缓存或丢弃。

令牌桶控制的是一个时间窗口内通过的数据量,在API层面我们常说的QPS、TPS,正好是一个时间窗口内的请求量或者事务量,只不过时间窗口限定在1s罢了。以一个恒定的速度往桶里放入令牌,而如果请求需要被处理,则需要先从桶里获取一个令牌,当桶里没有令牌可取时,则拒绝服务。令牌桶的另外一个好处是可以方便的改变速度,一旦需要提高速率,则按需提高放入桶中的令牌的速率。

在我们的工程实践中,通常使用Google开源工具包Guava提供的限流工具类RateLimiter来实现控制速率,该类基于令牌桶算法来完成限流,非常易于使用,而且非常高效。如我们不希望每秒的任务提交超过1个

public static void main(String[] args) {    String start = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());    RateLimiter limiter = RateLimiter.create(1.0); // 这里的1表示每秒允许处理的量为1个    for (int i = 1; i <= 10; i++) {        double waitTime = limiter.acquire(i); // 请求RateLimiter, 超过permits会被阻塞        System.out.println("cutTime=" + System.currentTimeMillis() + " call execute:" + i + " waitTime:" + waitTime);    }    String end = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());    System.out.println("start time:" + start);    System.out.println("end time:" + end);}

首先通过RateLimiter.create(1.0);创建一个限流器,参数代表每秒生成的令牌数,通过limiter.acquire(i);来以阻塞的方式获取令牌,令牌桶算法允许一定程度的突发(允许消费未来的令牌),所以可以一次性消费i个令牌;当然也可以通过tryAcquire(int permits, long timeout, TimeUnit unit)来设置等待超时时间的方式获取令牌,如果超timeout为0,则代表非阻塞,获取不到立即返回,支持阻塞或可超时的令牌消费。

从输出来看,RateLimiter支持预消费,比如在acquire(5)时,等待时间是4秒,是上一个获取令牌时预消费了3个两排,固需要等待3*1秒,然后又预消费了5个令牌,以此类推。

RateLimiter通过限制后面请求的等待时间,来支持一定程度的突发请求(预消费),在使用过程中需要注意这一点,Guava有两种限流模式,一种为稳定模式(SmoothBursty:令牌生成速度恒定,平滑突发限流),一种为渐进模式(SmoothWarmingUp:令牌生成速度缓慢提升直到维持在一个稳定值,平滑预热限流) 两种模式实现思路类似,主要区别在等待时间的计算上。

SmoothBursty 模式:

RateLimiter limiter = RateLimiter.create(5); RateLimiter.create(5)表示桶容量为5且每秒新增5个令牌,即每隔200毫秒新增一个令牌;limiter.acquire()表示消费一个令牌,如果当前桶中有足够令牌则成功(返回值为0),如果桶中没有令牌则暂停一段时间,比如发令牌间隔是200毫秒,则等待200毫秒后再去消费令牌,这种实现将突发请求速率平均为了固定请求速率。

SmoothWarmingUp模式:

RateLimiter limiter = RateLimiter.create(5,1000, TimeUnit.MILLISECONDS);

创建方式:

RateLimiter.create(doublepermitsPerSecond, long warmupPeriod, TimeUnit unit),permitsPerSecond表示每秒新增的令牌数,warmupPeriod表示在从冷启动速率过渡到平均速率的时间间隔。速率是梯形上升速率的,也就是说冷启动时会以一个比较大的速率慢慢到平均速率;然后趋于平均速率(梯形下降到平均速率)。可以通过调节warmupPeriod参数实现一开始就是平滑固定速率。整编:微信公众号,搜云库技术团队,ID:souyunku

放在Controller中用Jemter压测

注:RateLimiter控制的是速率,Samephore控制的是并发量。RateLimiter的原理就是令牌桶,它主要由许可发出的速率来定义,如果没有额外的配置,许可证将按每秒许可证规定的固定速度分配,许可将被平滑地分发,若请求超过permitsPerSecond则RateLimiter按照每秒 1/permitsPerSecond 的速率释放许可。注意:RateLimiter适用于单体应用,且RateLimiter不保证公平性访问。

使用上述方式使用RateLimiter的方式不够优雅,自定义注解+AOP的方式实现(适用于单体应用),详细见下面代码:

自定义注解:

import java.lang.annotation.*;
/** * 自定义注解可以不包含属性,成为一个标识注解 */@Inherited@Documented@Target({ElementType.METHOD, ElementType.FIELD, ElementType.TYPE})@Retention(RetentionPolicy.RUNTIME)public @interface RateLimitAspect {
}

自定义切面类

import com.google.common.util.concurrent.RateLimiter;import com.test.cn.springbootdemo.util.ResultUtil;import net.sf.json.JSONObject;import org.aspectj.lang.ProceedingJoinPoint;import org.aspectj.lang.annotation.Around;import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.Pointcut;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.context.annotation.Scope;import org.springframework.stereotype.Component;
import javax.servlet.ServletOutputStream;import javax.servlet.http.HttpServletResponse;import java.io.IOException;
@Component@Scope@Aspectpublic class RateLimitAop {
    @Autowired    private HttpServletResponse response;
    private RateLimiter rateLimiter = RateLimiter.create(5.0); //比如说,我这里设置"并发数"为5
    @Pointcut("@annotation(com.test.cn.springbootdemo.aspect.RateLimitAspect)")    public void serviceLimit() {
    }
    @Around("serviceLimit()")    public Object around(ProceedingJoinPoint joinPoint) {        Boolean flag = rateLimiter.tryAcquire();        Object obj = null;        try {            if (flag) {                obj = joinPoint.proceed();            }else{                String result = JSONObject.fromObject(ResultUtil.success1(100, "failure")).toString();                output(response, result);            }        } catch (Throwable e) {            e.printStackTrace();        }        System.out.println("flag=" + flag + ",obj=" + obj);        return obj;    }
    public void output(HttpServletResponse response, String msg) throws IOException {        response.setContentType("application/json;charset=UTF-8");        ServletOutputStream outputStream = null;        try {            outputStream = response.getOutputStream();            outputStream.write(msg.getBytes("UTF-8"));        } catch (IOException e) {            e.printStackTrace();        } finally {            outputStream.flush();            outputStream.close();        }    }}

测试controller

import com.test.cn.springbootdemo.aspect.RateLimitAspect;import com.test.cn.springbootdemo.util.ResultUtil;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.ResponseBody;
@Controllerpublic class TestController {
    @ResponseBody    @RateLimitAspect    @RequestMapping("/test")    public String test(){        return ResultUtil.success1(1001, "success").toString();    }

压测结果:

三、控制单位时间窗口内请求数

某些场景下,我们想限制某个接口或服务 每秒/每分钟/每天 的请求次数或调用次数。例如限制服务每秒的调用次数为50,实现如下:

private LoadingCache < Long, AtomicLong > counter = CacheBuilder.newBuilder().expireAfterWrite(2, TimeUnit.SECONDS).build(new CacheLoader < Long, AtomicLong > () {@    Override    public AtomicLong load(Long seconds) throws Exception {        return new AtomicLong(0);    }});public static long permit = 50;public ResponseEntity getData() throws ExecutionException {    //得到当前秒    long currentSeconds = System.currentTimeMillis() / 1000;    if (counter.get(currentSeconds).incrementAndGet() > permit) {        return ResponseEntity.builder().code(404).msg("访问速率过快").build();    }    //业务处理}

到此应用级限流的一些方法就介绍完了。假设将应用部署到多台机器,应用级限流方式只是单应用内的请求限流,不能进行全局限流。因此我们需要分布式限流和接入层限流来解决这个问题。

分布式限流

自定义注解+拦截器+Redis实现限流 (单体和分布式均适用,全局限流)

自定义注解:

@Inherited@Documented@Target({ElementType.FIELD,ElementType.TYPE,ElementType.METHOD})@Retention(RetentionPolicy.RUNTIME)public @interface AccessLimit {
    int limit() default 5;  
    int sec() default 5;}

拦截器:

public class AccessLimitInterceptor implements HandlerInterceptor {
    @Autowired    private RedisTemplate<String, Integer> redisTemplate;  //使用RedisTemplate操作redis
    @Override    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {        if (handler instanceof HandlerMethod) {            HandlerMethod handlerMethod = (HandlerMethod) handler;            Method method = handlerMethod.getMethod();            if (!method.isAnnotationPresent(AccessLimit.class)) {                return true;            }            AccessLimit accessLimit = method.getAnnotation(AccessLimit.class);            if (accessLimit == null) {                return true;            }            int limit = accessLimit.limit();            int sec = accessLimit.sec();            String key = IPUtil.getIpAddr(request) + request.getRequestURI();            Integer maxLimit = redisTemplate.opsForValue().get(key);            if (maxLimit == null) {                redisTemplate.opsForValue().set(key, 1, sec, TimeUnit.SECONDS);  //set时一定要加过期时间            } else if (maxLimit < limit) {                redisTemplate.opsForValue().set(key, maxLimit + 1, sec, TimeUnit.SECONDS);            } else {                output(response, "请求太频繁!");                return false;            }        }        return true;    }
    public void output(HttpServletResponse response, String msg) throws IOException {        response.setContentType("application/json;charset=UTF-8");        ServletOutputStream outputStream = null;        try {            outputStream = response.getOutputStream();            outputStream.write(msg.getBytes("UTF-8"));        } catch (IOException e) {            e.printStackTrace();        } finally {            outputStream.flush();            outputStream.close();        }    }
    @Override    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
    }
    @Override    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
    }}

controller:

@Controller@RequestMapping("/activity")public class AopController {    @ResponseBody    @RequestMapping("/seckill")    @AccessLimit(limit = 4,sec = 10)  //加上自定义注解即可    public String test (HttpServletRequest request,@RequestParam(value = "username",required = false) String userName){        //TODO somethings……        return   "hello world !";    }}

配置文件:

/*springmvc的配置文件中加入自定义拦截器*/<mvc:interceptors>   <mvc:interceptor>      <mvc:mapping path="/**"/>      <bean class="com.pptv.activityapi.controller.pointsmall.AccessLimitInterceptor"/>   </mvc:interceptor></mvc:interceptors>

访问效果如下,10s内访问接口超过4次以上就过滤请求,原理和计数器算法类似:

接入层限流

主要介绍nginx 限流,采用漏桶算法。

限制原理:可一句话概括为:“根据客户端特征,限制其访问频率”,客户端特征主要指IP、UserAgent等。使用IP比UserAgent更可靠,因为IP无法造假,UserAgent可随意伪造。整编:微信公众号,搜云库技术团队,ID:souyunku

用limit_req模块来限制基于IP请求的访问频率:

http://nginx.org/en/docs/http/ngxhttplimitreqmodule.html

也可以用tengine中的增强版:

http://tengine.taobao.org/documentcn/httplimitreqcn.html

1、并发数和连接数控制的配置:

nginx http配置:    #请求数量控制,每秒20个    limit_req_zone $binary_remote_addr zone=one:10m rate=20r/s;    #并发限制30个    limit_conn_zone $binary_remote_addr zone=addr:10m;
    server块配置    limit_req zone=one burst=5;    limit_conn addr 30;

2、ngxhttplimitconnmodule 可以用来限制单个IP的连接数:

ngxhttplimitconnmodule模块可以按照定义的键限定每个键值的连接数。可以设定单一 IP 来源的连接数。

并不是所有的连接都会被模块计数;只有那些正在被处理的请求(这些请求的头信息已被完全读入)所在的连接才会被计数。

http {    limit_conn_zone $binary_remote_addr zone=addr:10m;    ...    server {        ...        location /download/ {            limit_conn addr 1;        }