1搓幌、 單線程、任務隊列的概念
單線程:
- JavaScript是一個單線程語言,瀏覽器只會分配一個javascript引擎線程來執(zhí)行任務迅箩,這也就意味所有任務需要排隊溉愁,前一個任務結(jié)束,才會執(zhí)行后一個任務饲趋。
- 瀏覽器是多線程的拐揭。javascript引擎線程是瀏覽器多個線程中的一個,它本身是單線程的奕塑。瀏覽器還包括很多其他線程堂污,如界面渲染線程,瀏覽器事件觸發(fā)線程龄砰,Http請求線程等盟猖。
為什么是JavaScript單線程讨衣,不能有多個線程呢?
JavaScript的單線程式镐,與它的用途有關(guān)反镇。作為瀏覽器腳本語言,JavaScript的主要用途是與用戶互動娘汞,以及操作DOM歹茶。這決定了它只能是單線程,否則會帶來很復雜的同步問題你弦。比如惊豺,假定JavaScript同時有兩個線程,一個線程在某個DOM節(jié)點上添加內(nèi)容禽作,另一個線程刪除了這個節(jié)點尸昧,這時瀏覽器應該以哪個線程為準?
所以领迈,為了避免復雜性彻磁,從一誕生,JavaScript就是單線程狸捅,這已經(jīng)成了這門語言的核心特征
單線程模型帶來的問題衷蜓?
單線程即任務是串行的,后一個任務需要等待前一個任務的執(zhí)行尘喝,這就可能出現(xiàn)長時間的等待磁浇,造成瀏覽器失去響應(假死)。比如ajax網(wǎng)絡請求朽褪、setTimeout時間延遲置吓、DOM事件的用戶交互等,這些任務并不消耗 CPU缔赠,是一種空等衍锚,資源浪費。
所以嗤堰,瀏覽器為這些耗時任務開辟了另外的線程戴质,主要包括http請求線程,瀏覽器定時觸發(fā)器踢匣,瀏覽器事件觸發(fā)線程告匠,這些任務是異步的
同步任務,異步任務离唬?
- 同步任務指的是后专,在主線程上排隊執(zhí)行的任務,只有前一個任務執(zhí)行完畢输莺,才能執(zhí)行后一個任務
- 異步任務:
疑惑:對于異步任務戚哎,我開始始終無法理解裸诽。所謂異步,就是做一件事的同事建瘫,也在干另一件事崭捍,兩件事并發(fā)進行。如果異步任務指的是那些被加入到了任務隊列中的代碼塊(也就是所謂的回調(diào)函數(shù))啰脚,那些代碼塊只是延遲了執(zhí)行殷蛇,并沒有做到和JS主線程并行執(zhí)行代碼,如何能叫異步任務橄浓?
自己的理解:
①我理解的異步任務指的是http請求的過程粒梦,setTimeout設置相應時間的等待的過程,onclick等待點擊的過程等荸实,這些是由瀏覽器的其他的線程去執(zhí)行的匀们,這些過程才和JS主線程是異步的。并不是回調(diào)函數(shù)
(舉個列子:setTimeout設置等待10秒后console.log("haha")准给,這個等10秒的過程是瀏覽器的其他線程執(zhí)行的泄朴,是異步的)
②至于回調(diào)函數(shù),異步任務執(zhí)行結(jié)束后露氮,需要把結(jié)果祖灰,或者后續(xù)的處理交給JS主線程執(zhí)行,這是通過回調(diào)函數(shù)實現(xiàn)的
(接著上面的列子::console.log("haha")需要JS主線程執(zhí)行畔规,就是通過回調(diào)函數(shù)的方式供JS主線程調(diào)用)
③那么JS主線程如何拿到異步任務的回調(diào)函數(shù)呢局扶?JS設計了一個任務隊列,異步任務會將相關(guān)回調(diào)函數(shù)添加到任務隊列中叁扫,因此準確的應該是叫做callback queue(回調(diào)函數(shù)隊列)三妈。最后主線程執(zhí)行這些回調(diào)函數(shù)仍然是一個一個同步執(zhí)行的。所以異步任務的回調(diào)函數(shù)并沒有異步執(zhí)行莫绣,只是掛起畴蒲,延遲了執(zhí)行
任務隊列:
1.主線程之外,還存在一個"任務隊列"(callback queue)对室。用于存放異步任務的回調(diào)函數(shù)饿凛。它一個先進先出的數(shù)據(jù)結(jié)構(gòu),排在前面的事件優(yōu)先被主線程讀取软驰。所以對于“定時器”,雖然到了設定的時間心肪,定時器的回調(diào)函數(shù)被加入到了任務隊列中锭亏,但是前面如果還有其他的事件沒執(zhí)行完,此時就要等待硬鞍,那么執(zhí)行的時間就不一定是設定的時間了
2.回調(diào)函數(shù)放置時機:
異步操作會將相關(guān)回調(diào)函數(shù)添加到任務隊列中慧瘤。而不同的異步操作添加到任務隊列的時機也不同戴已,如 onclick, setTimeout, ajax 處理的方式都不同,這些異步操作是由瀏覽器內(nèi)核的 webcore 來執(zhí)行的锅减,webcore 包含上圖中的3種 webAPI糖儡,分別是 DOM Binding、network怔匣、timer模塊握联。
- onclick 由瀏覽器內(nèi)核的 DOM Binding 模塊來處理,當事件觸發(fā)的時候每瞒,回調(diào)函數(shù)會立即添加到任務隊列中金闽。
- setTimeout 會由瀏覽器內(nèi)核的 timer 模塊來進行延時處理,當時間到達的時候剿骨,才會將回調(diào)函數(shù)添加到任務隊列中代芜。
- ajax 則會由瀏覽器內(nèi)核的 network 模塊來處理,在網(wǎng)絡請求完成返回之后浓利,才將回調(diào)添加到任務隊列中挤庇。
事件循環(huán)?
主線程從"任務隊列"中讀取事件,這個過程是循環(huán)不斷的贷掖,所以整個的這種運行機制又稱為Event Loop(事件循環(huán))
回調(diào)函數(shù)"(callback)"
就是那些會被主線程掛起來的代碼嫡秕。這些被掛起來的代碼會被異步任務添加到任務隊列中,等到主線程中的同步代碼都執(zhí)行完畢羽资,這些回調(diào)函數(shù)就會被一一執(zhí)行淘菩。異步任務必須指定回調(diào)函數(shù)
圖解
圖片來自Philip Roberts的演講《Help, I'm stuck in an event-loop》
- 主線程就是有虛線組成的那一部分,堆(heap)和棧(stack)共同組成了js主線程屠升;任務隊列就是callback queue 潮改;瀏覽器為異步任務單獨開辟的線程可以統(tǒng)一理解為WebAPIs
- 函數(shù)的執(zhí)行就是通過進棧和出棧實現(xiàn)的,比如圖中有一個foo()函數(shù)腹暖,主線程把它推入棧中汇在,在執(zhí)行函數(shù)體時,發(fā)現(xiàn)還需要執(zhí)行上面的那幾個函數(shù)脏答,所以又把這幾個函數(shù)推入棧中糕殉,等到函數(shù)執(zhí)行完,就讓函數(shù)出棧殖告。
- 等到stack清空時阿蝶,說明一個任務已經(jīng)執(zhí)行完了,這時就會從callback queue中尋找下一個(其實就是回調(diào)函數(shù))推入棧中(這個尋找的過程黄绩,叫做event loop羡洁,因為它總是循環(huán)的查找任務隊列里是否還有任務)。
2爽丹、下面這段代碼輸出結(jié)果是? 為什么?
var a = 1;
setTimeout(function(){
a = 2;
console.log(a);
}, 0);
var a ;
console.log(a);
a = 3;
console.log(a);
輸出:
1
3
2
原理:
- setTimeout是異步執(zhí)行的任務筑煮,它的回調(diào)函數(shù)會在被設定的時間到達時加入到任務隊列辛蚊,等待JS主線程所有代碼執(zhí)行完成后,才會進行Event Loop真仲,從任務隊列中讀取回調(diào)函數(shù)并且執(zhí)行
- setTimeout(f,0)袋马,指定時間為0,表示的是立刻將回調(diào)函數(shù)加入到任務隊列中秸应,但是任務隊列中的回調(diào)函數(shù)需要等到JS主線程的所有代碼都執(zhí)行完了虑凛,才會開始執(zhí)行,這也就解釋了為什么先輸出1和3灸眼,最后在輸出回調(diào)函數(shù)中的2
- 所以setTimeout(f,0)的作用是卧檐,盡可能早地執(zhí)行指定的任務,及等到JS主線程的同步任務和“任務隊列”中已有的事件全都執(zhí)行完后立即執(zhí)行
3焰宣、下面這段代碼輸出結(jié)果是? 為什么?
var flag = true;
setTimeout(function(){
flag = false;
},0)
while(flag){}
console.log(flag);
結(jié)果:死循環(huán)沒有任何結(jié)果
原理:setTimeout中設定的函數(shù)霉囚,需要等到同步代碼都執(zhí)行完才執(zhí)行,而flag的初始值是true匕积,因此while會運行盈罐,而while循環(huán)中又沒有任何內(nèi)容,因此會死循環(huán)沒有任何結(jié)果
4闪唆、實現(xiàn)一個節(jié)流函數(shù)
首先理解什么是函數(shù)節(jié)流
函數(shù)節(jié)流簡單講就是讓一個函數(shù)無法在很短的時間間隔內(nèi)連續(xù)執(zhí)行盅粪,只有當上一次函數(shù)執(zhí)行后過了你規(guī)定的時間間隔,才能進行下一次該函數(shù)的調(diào)用悄蕾。
函數(shù)節(jié)流有什么用呢票顾?
一定程度上能優(yōu)化性能。例如帆调,當調(diào)整瀏覽器大小的時候奠骄,onresize 事件會連續(xù)觸發(fā)。在onresize 事件處理程序內(nèi)部如果嘗試進行DOM 操作番刊,其高頻率的更改可能會讓瀏覽器崩潰含鳞。所以可以設置個函數(shù)節(jié)流,只有當調(diào)整窗口停下來歇會才開始觸發(fā)onresize 事件芹务。
實現(xiàn)原理
第一次調(diào)用函數(shù)蝉绷,設置一個定時器,在指定的時間間隔之后運行代碼枣抱。如果在這個時間間隔內(nèi)又調(diào)用這個函數(shù)熔吗,那我們就clear掉原來的定時器,再setTimeout一個新的定時器延遲一會執(zhí)行佳晶。
代碼:
function throttle(fn, delay) {
var timer = null
return function() {
clearTimeout(timer)
timer = setTimeout(function() {
fn()
}, delay)
}
}
function hiFrequency() {
console.log("do something")
}
var result = throttle(hiFrequency, 3000)
result()
result()
result()
5磁滚、列出DOM 元素選取的 API
- getElementById():返回匹配指定ID屬性的元素節(jié)點。如果沒有發(fā)現(xiàn)匹配的節(jié)點,則返回null
<div id="box">
<div class="color red"></div>
<div class="color green"></div>
<p id="content"></p>
</div>
var elem = document.getElementById("box");
console.log(elem)
/* 輸出結(jié)果
<div id="box">
<div class="color red"></div>
<div class="color green"></div>
<p id="content"></p>
</div> */
- getElementsByClassName():返回一個類似數(shù)組的對象(HTMLCollection類型的對象)垂攘,包括了所有class名字符合指定條件的元素(搜索范圍包括本身),元素的變化實時反映在返回結(jié)果中淤刃。任何元素節(jié)點上可以調(diào)用晒他。一個參數(shù),包含一個或多個類名的字符串(類名通過空格分隔逸贾,指的是一個元素同時包括多個class)陨仅。
<div id="box">
<div class="color red"></div>
<div class="color green"></div>
<div class="color blue"></div>
<div class="color yellow"></div>
<div class="color pink"></div>
<p id="content"></p>
</div>
var elements = document.getElementsByClassName('color');
console.log(elements) //[div.color.red, div.color.green, div.color.blue, div.color.yellow, div.color.pink]
console.log(document.getElementsByClassName('color')[0]) //前面取出來的是個HTMLCollection類型的對象,想要獲取元素還需要這樣索引一下或者elements[x] 輸出結(jié)果:<div class="color red"></div>
var elements2 = document.getElementsByClassName('red color');
console.log(elements2) // [div.color.blue]
var elements3 = document.getElementById('box').getElementsByClassName('yellow');
console.log(elements3) //寫法可以級聯(lián)铝侵,box元素節(jié)點上也可以調(diào)用灼伤,結(jié)果[div.color.yellow]
- getElementsByTagName():返回所有指定標簽的元素(搜索范圍包括本身)。返回值是一個HTMLCollection對象咪鲜,也就是說狐赡,搜索結(jié)果是一個動態(tài)集合,任何元素的變化都會實時反映在返回的集合中疟丙。任何元素節(jié)點上可以調(diào)用
<div id="box">
<div class="color red"></div>
<p id="content"></p>
<p></p>
</div>
var paras = document.getElementsByTagName("p");
console.log(paras[0]) // <p id="content"></p>
- getElementsByName():用于選擇擁有name屬性的HTML元素颖侄,比如form、img享郊、frame览祖、embed和object,返回一個NodeList格式的對象炊琉,不會實時反映元素的變化展蒂。
// 假定有一個表單是<form name="x"></form>
var forms = document.getElementsByName("x");
console.log(forms[0]) // <form name="x"></form>
console.log(forms[0].tagName) // "FORM"
注:在IE瀏覽器使用這個方法,會將沒有name屬性苔咪、但有同名id屬性的元素也返回锰悼,所以name和id屬性最好設為不一樣的值。
- querySelector():ES5的元素選擇方法悼泌。querySelector方法返回匹配指定的CSS選擇器的元素節(jié)點松捉。如果有多個節(jié)點滿足匹配條件,則返回第一個匹配的節(jié)點馆里。如果沒有發(fā)現(xiàn)匹配的節(jié)點隘世,則返回null。
var el1 = document.querySelector(".myclass");
var el2 = document.querySelector('#myParent > [ng-click]');
注:參數(shù)的寫法和css寫法一致鸠踪。querySelector方法無法選中CSS偽元素丙者。
- querySelectorAll():ES5的元素選擇方法。querySelectorAll方法返回匹配指定的CSS選擇器的所有節(jié)點营密,返回的是NodeList類型的對象械媒。NodeList對象不是動態(tài)集合,所以元素節(jié)點的變化無法實時反映在返回結(jié)果中。
elementList = document.querySelectorAll(selectors);
querySelectorAll方法的參數(shù)纷捞,可以是逗號分隔的多個CSS選擇器痢虹。
var matches = document.querySelectorAll("div.note, div.alert");
6、創(chuàng)建元素主儡、添加元素
創(chuàng)建元素
- createElement():生成HTML元素節(jié)點奖唯。生成的節(jié)點是存在于內(nèi)存中的,還沒如被加入到DOM中糜值。
var newDiv = document.createElement("div");
createElement方法的參數(shù)為元素的標簽名丰捷,即元素節(jié)點的tagName屬性。如果傳入大寫的標簽名寂汇,會被轉(zhuǎn)為小寫病往。如果參數(shù)帶有尖括號(即<和>)或者是null,會報錯骄瓣。
- createTextNode():生成文本節(jié)點停巷,參數(shù)為所要生成的文本節(jié)點的內(nèi)容。
var newContent = document.createTextNode("Hello");
-
createDocumentFragment():生成一個DocumentFragment對象累贤。DocumentFragment對象是一個存在于內(nèi)存的DOM片段叠穆,但是不屬于當前文檔,常常用來生成較復雜的DOM結(jié)構(gòu)臼膏,然后插入當前文檔硼被。這樣做的好處在于,因為DocumentFragment不屬于當前文檔渗磅,對它的任何改動嚷硫,都不會引發(fā)網(wǎng)頁的重新渲染,比直接修改當前文檔的DOM有更好的性能表現(xiàn)始鱼。
有什么用呢仔掸?
舉個列子:向ul中添加5個li
<ul class="navbar"></ul>
//方法一,這個方法最差,相當于操作了5次DOM
var navbarNode = document.querySelector(".navbar")
for(var i = 0; i < 5; i++){
var child = document.createElement("li")
var text = document.createTextNode("hello" + i)
child.appendChild(text)
navbarNode.appendChild(child)
}
//方法二医清,先將li全部放入一個div中起暮,最后一次性加入到DOM節(jié)點中,這個雖然只和DOM交互了一次会烙,但是不符合初衷负懦,外層多了一個div
var navbarNode = document.querySelector(".navbar")
var container = document.createElement("div")
for(var i = 0; i < 5; i++){
var child = document.createElement("li")
var text = document.createTextNode("hello" + i)
child.appendChild(text)
container.appendChild(child)
}
navbarNode.appendChild(container)
//方法三,最優(yōu)的方法柏腻。先將li全部放入一個fragment對象中纸厉,最后一次性添加進相應的DOM節(jié)點中,fragment相當于一個隱形的元素五嫂,不會顯示在DOM中
var navbarNode = document.querySelector(".navbar")
var fragment = document.createDocumentFragment()
for(var i = 0; i < 5; i++){
var child = document.createElement("li")
var text = document.createTextNode("hello" + i)
child.appendChild(text)
fragment.appendChild(child)
}
navbarNode.appendChild(fragment)
innerHTML也可以添加元素颗品,不需要通過創(chuàng)建節(jié)點肯尺,在appendChild的方式添加到DOM中,只需要HTML結(jié)構(gòu)的字符串就可以添加
<ul class="navbar"></ul>
var navData = [1, 2, 3]
var html = ""
navData.forEach(function(item){
html += "<li>" + item + "</li>"
}) //html結(jié)果:"<li>1</li><li>2</li><li>3</li>"
document.querySelector(".navbar").innerHTML = html
因此也可以用 document.querySelector(".navbar").innerHTML直接獲取某個節(jié)點中的HTML結(jié)構(gòu)的字符串
和innerText的區(qū)別躯枢?
var navData = [1, 2, 3]
var html = ""
navData.forEach(function(item){
html += "<li>" + item + "</li>"
}) //html結(jié)果:"<li>1</li><li>2</li><li>3</li>"
document.querySelector(".navbar").innerText = html
相當于在類名為navbar的ul中添加了<li>1</li><li>2</li><li>3</li>這行文字则吟,不會轉(zhuǎn)換為HTML結(jié)構(gòu),因此會在頁面中顯示這行文字
所以innerText也可以用來獲取元素內(nèi)包含的文本內(nèi)容锄蹂,在多層次的時候會按照元素由淺到深的順序拼接其內(nèi)容
Ex:
<div>
<p>
123
<span>456</span>
</p>
</div>
外層div的innerText返回內(nèi)容是 "123456"
注意:讓用戶輸入的內(nèi)容可以用innerText逾滥,不要用innerHTML,因為如果用戶輸入的html結(jié)構(gòu)的字符串中包含惡意的JS代碼败匹,innerHTML會執(zhí)行,容易招受攻擊
修改元素
- appendChild():在元素末尾添加元素
var child = document.createElement("div")
var Text = document.createTextNode("哈哈")
child.appendChild(Text)
document.body.appendChild(child)
- insertBefore():在當前節(jié)點的某個子節(jié)點之前再插入一個子節(jié)點讥巡。
<ul id="menu">
<li id="item"></li>
</ul>
var item1 = document.createElement("li")
var item2 = document.getElementById("item2")
var menu = item2.parentNode
menu.insertBefore(item1, item2)
注:想要插入到某個子節(jié)點之后掀亩,沒有 insertAfter方法』肚辏可以使用 insertBefore方法和 nextSibling來模擬它槽棍。
var item3 = document.createElement("li")
var item2 = document.getElementById("item2")
var menu = item2.parentNode
menu.insertBefore(item3, item2.nextSibling)
- replaceChild():用指定的節(jié)點替換當前節(jié)點的一個子節(jié)點,并返回被替換掉的節(jié)點抬驴。
replacedNode = parentNode.replaceChild(newChild, oldChild);
//newChild 用來替換 oldChild 的新節(jié)點炼七。如果該節(jié)點已經(jīng)存在于DOM樹中,則它會被從原始位置刪除布持。
//replacedNode 和oldChild相等豌拙。
- removeChild():刪除元素
parentNode.removeChild(childNode);
- cloneNode():克隆元素,方法有一個布爾值參數(shù)题暖,傳入true的時候會深復制按傅,也就是會復制元素及其子元素(IE還會復制其事件),false的時候只復制元素本身
node.cloneNode(true);