果你是一名前端人員,你單單的使用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) 是HTML和XML文檔的編程接口。它提供了對文檔的結構化的表述,并定義了一種方式可以使從程序中對該結構進行訪問,從而改變文檔的結構,樣式和內容。文檔對象模型 (DOM) 是對HTML文件的另一種展示,通俗地說,一個HTML 文件,我們可以用編輯器以代碼的形式展示它,也可以用瀏覽器以頁面的形式展示它,同一份文件通過不同的展示方式,就有了不一樣的表現形式。而DOM 將文檔解析為一個由節點和對象(包含屬性和方法的對象)組成的結構集合。簡言之,它會將web頁面和腳本或程序語言連接起來,我們可以使用腳本或者程序語言通過DOM 來改變或者控制web頁面。
我們可以通過JavaScript 來調用document和window元素的API來操作文檔或者獲取文檔的信息。
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這幾種類型。
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
Text表示文本節點,它包含的是純文本內容,不能包含html代碼,但可以包含轉義后的html代碼。Text有下面的特性:(1)nodeType為3(2)nodeName為#text(3)nodeValue為文本內容(4)parentNode是一個Element(5)沒有子節點
Comment表示HTML文檔中的注釋,它有下面的幾種特征:(1)nodeType為8(2)nodeName為#comment(3)nodeValue為注釋的內容(4)parentNode可能是Document或Element(5)沒有子節點
Document表示文檔,在瀏覽器中,document對象是HTMLDocument的一個實例,表示整個頁面,它同時也是window對象的一個屬性。Document有下面的特性:(1)nodeType為9(2)nodeName為#document(3)nodeValue為null(4)parentNode為null(5)子節點可能是一個DocumentType或Element
DocumentFragment是所有節點中唯一一個沒有對應標記的類型,它表示一種輕量級的文檔,可能當作一個臨時的倉庫用來保存可能會添加到文檔中的節點。DocumentFragment有下面的特性:(1)nodeType為11(2)nodeName為#document-fragment(3)nodeValue為null(4)parentNode為null
用如其名,這類API是用來創建節點的
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);
運行結果為:
createTextNode用來創建一個文本節點
語法
復制代碼 var text = document.createTextNode(data);
createTextNode接收一個參數,這個參數就是文本節點中的文本,和createElement一樣,創建后的文本節點也只是獨立的一個節點,同樣需要appendChild將其添加到HTML文檔樹中
例子:
復制代碼 var node = document.createTextNode("我是文本節點");
document.body.appendChild(node);
運行結果為:
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>
運行結果為:
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>
運行結果為:
節點創建型API主要包括createElement,createTextNode,cloneNode和createDocumentFragment四個方法,需要注意下面幾點:(1)它們創建的節點只是一個孤立的節點,要通過appendChild添加到文檔中(2)cloneNode要注意如果被復制的節點是否包含子節點以及事件綁定等問題(3)使用createDocumentFragment來解決添加大量節點時的性能問題
前面我們提到節點創建型API,它們只是創建節點,并沒有真正修改到頁面內容,而是要調用·appendChild·來將其添加到文檔樹中。我在這里將這類會修改到頁面內容歸為一類。修改頁面內容的api主要包括:appendChild,insertBefore,removeChild,replaceChild。
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>
運行結果:
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會將節點添加到子元素的末尾
刪除指定的子節點并返回
語法
復制代碼 var deletedChild = parent.removeChild(node);
deletedChild指向被刪除節點的引用,它等于node,被刪除的節點仍然存在于內存中,可以對其進行下一步操作。注意:如果被刪除的節點不是其子節點,則程序將會報錯。我們可以通過下面的方式來確??梢詣h除:
復制代碼if(node.parentNode){
node.parentNode.removeChild(node);
}
運行結果:
通過節點自己獲取節點的父節點,然后將自身刪除
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)
}
運行結果:
頁面修改型API主要是這四個接口,要注意幾個特點:(1)不管是新增還是替換節點,如果新增或替換的節點是原本存在頁面上的,則其原來位置的節點將被移除,也就是說同一個節點不能存在于頁面的多個位置(2)節點本身綁定的事件會不會消失,會一直保留著。
這個接口很簡單,根據元素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>
運行結果:
返回一個包括所有給定標簽名稱的元素的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。
運行結果:
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>
運行結果:
這個API是根據元素的class返回一個即時的HTMLCollection
語法
復制代碼 var elements = document.getElementsByClassName(names); // or:
var elements = rootElement.getElementsByClassName(names);
這個接口有下面幾點要注意:(1)返回結果是一個即時的HTMLCollection,會隨時根據文檔結構變化(2)IE9以下瀏覽器不支持(3)如果要獲取2個以上classname,可傳入多個classname,每個用空格相隔,例如
復制代碼 var elements = document.getElementsByClassName("test1 test2");
例子:
復制代碼 var elements = document.getElementsByClassName('test');
復制代碼 var elements = document.getElementsByClassName('red test');
復制代碼 var elements = document.getElementById('main').getElementsByClassName('test');
復制代碼 var testElements = document.getElementsByClassName('test');
var testDivs = Array.prototype.filter.call(testElements, function(testElement){
return testElement.nodeName === 'DIV';;
});
這兩個API很相似,通過css選擇器來查找元素,注意選擇器要符合CSS選擇器的規則
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獲取元素時,它通過深度優先搜索,拿到文檔樹前面的第三級的元素。運行結果:
語法
復制代碼 var elementList = document.querySelectorAll(selectors);
例子:
復制代碼 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以下的瀏覽器不支持。
運行結果:
在html文檔中的每個節點之間的關系都可以看成是家譜關系,包含父子關系,兄弟關系等等
每個節點都有一個parentNode屬性,它表示元素的父節點。Element的父節點可能是Element,Document或DocumentFragment
返回元素的父元素節點,與parentNode的區別在于,其父節點必須是一個Element,如果不是,則返回null
返回一個即時的NodeList,表示元素的子節點列表,子節點可能會包含文本節點,注釋節點等
一個即時的HTMLCollection,子節點都是Element,IE9以下瀏覽器不支持children屬性為只讀屬性,對象類型為HTMLCollection,你可以使用elementNodeReference.children[1].nodeName來獲取某個子元素的標簽名稱
只讀屬性返回樹中節點的第一個子節點,如果節點是無子節點,則返回 null
返回當前節點的最后一個子節點。如果父節點為一個元素節點,則子節點通常為一個元素節點,或一個文本節點,或一個注釋節點。如果沒有子節點,則返回null
返回一個布爾值,表明當前節點是否包含有子節點.
返回當前節點的前一個兄弟節點,沒有則返回nullGecko內核的瀏覽器會在源代碼中標簽內部有空白符的地方插入一個文本結點到文檔中.因此,使用諸如Node.firstChild和Node.previousSibling之類的方法可能會引用到一個空白符文本節點, 而不是使用者所預期得到的節點
previousElementSibling返回當前元素在其父元素的子元素節點中的前一個元素節點,如果該元素已經是第一個元素節點,則返回null,該屬性是只讀的。注意IE9以下瀏覽器不支持
Node.nextSibling是一個只讀屬性,返回其父節點的childNodes列表中緊跟在其后面的節點,如果指定的節點為最后一個節點,則返回nullGecko內核的瀏覽器會在源代碼中標簽內部有空白符的地方插入一個文本結點到文檔中.因此,使用諸如Node.firstChild和Node.previousSibling之類的方法可能會引用到一個空白符文本節點, 而不是使用者所預期得到的節點
nextElementSibling返回當前元素在其父元素的子元素節點中的后一個元素節點,如果該元素已經是最后一個元素節點,則返回null,該屬性是只讀的。注意IE9以下瀏覽器不支持
設置指定元素上的一個屬性值。如果屬性已經存在,則更新該值; 否則將添加一個新的屬性用指定的名稱和值
語法
復制代碼 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";
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>
運行結果:
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"的屬性,在運行之后這個屬性就被刪除了
運行結果:
Window.getComputedStyle()方法給出應用活動樣式表后的元素的所有CSS屬性的值,并解析這些值可能包含的任何基本計算假設某個元素并未設置高度而是通過其內容將其高度撐開,這時候要獲取它的高度就要用到getComputedStyle
語法
復制代碼 var style = window.getComputedStyle(element[, pseudoElt]);
element是要獲取的元素,pseudoElt指定一個偽元素進行匹配。返回的style是一個CSSStyleDeclaration對象。通過style可以訪問到元素計算后的樣式
getBoundingClientRect用來返回元素的大小以及相對于瀏覽器可視窗口的位置
語法
復制代碼 var clientRect = element.getBoundingClientRect();
clientRect是一個DOMRect對象,包含left,top,right,bottom,它是相對于可視窗口的距離,滾動位置發生改變時,它們的值是會發生變化的。除了IE9以下瀏覽器,還包含元素的height和width等數據
例子:
復制代碼 elem.style.color = 'red';
elem.style.setProperty('font-size', '16px');
elem.style.removeProperty('color');
例子:
復制代碼 var style = document.createElement('style');
style.innerHTML = 'body{color:red} #top:hover{background-color: red;color: white;}';
document.head.appendChild(style););
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或內存負載等來限流。
page view 頁面總訪問量,每刷新一次記錄一次。
unique view 客戶端主機訪問,指一天內相同IP的訪問記為1次。
query per second,即每秒訪問量。qps很大程度上代表了系統的繁忙度,沒次請求可能存在多次的磁盤io,網絡請求,多個cpu時間片,一旦qps超過了預先設置的閥值,可以考量擴容增加服務器,避免訪問量過大導致的宕機。整編:微信公眾號,搜云庫技術團隊,ID:souyunku
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
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:令牌生成速度緩慢提升直到維持在一個穩定值,平滑預熱限流) 兩種模式實現思路類似,主要區別在等待時間的計算上。
RateLimiter limiter = RateLimiter.create(5); RateLimiter.create(5)表示桶容量為5且每秒新增5個令牌,即每隔200毫秒新增一個令牌;limiter.acquire()表示消費一個令牌,如果當前桶中有足夠令牌則成功(返回值為0),如果桶中沒有令牌則暫停一段時間,比如發令牌間隔是200毫秒,則等待200毫秒后再去消費令牌,這種實現將突發請求速率平均為了固定請求速率。
RateLimiter limiter = RateLimiter.create(5,1000, TimeUnit.MILLISECONDS);
RateLimiter.create(doublepermitsPerSecond, long warmupPeriod, TimeUnit unit),permitsPerSecond表示每秒新增的令牌數,warmupPeriod表示在從冷啟動速率過渡到平均速率的時間間隔。速率是梯形上升速率的,也就是說冷啟動時會以一個比較大的速率慢慢到平均速率;然后趨于平均速率(梯形下降到平均速率)。可以通過調節warmupPeriod參數實現一開始就是平滑固定速率。整編:微信公眾號,搜云庫技術團隊,ID:souyunku
注: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(); } }}
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@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
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;
ngxhttplimitconnmodule模塊可以按照定義的鍵限定每個鍵值的連接數??梢栽O定單一 IP 來源的連接數。
并不是所有的連接都會被模塊計數;只有那些正在被處理的請求(這些請求的頭信息已被完全讀入)所在的連接才會被計數。
http { limit_conn_zone $binary_remote_addr zone=addr:10m; ... server { ... location /download/ { limit_conn addr 1; }
*請認真填寫需求信息,我們會在24小時內與您取得聯系。