JS定时器,DOM操作(1)

1、 单线程、任务队列的概念

单线程

  • JavaScript是一个单线程语言,浏览器只会分配一个javascript引擎线程来执行任务,这也就意味所有任务需要排队,前一个任务结束,才会执行后一个任务。
  • 浏览器是多线程的。javascript引擎线程是浏览器多个线程中的一个,它本身是单线程的。浏览器还包括很多其他线程,如界面渲染线程,浏览器事件触发线程,Http请求线程等。

为什么是JavaScript单线程,不能有多个线程呢?
JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?
所以,为了避免复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征

单线程模型带来的问题?
单线程即任务是串行的,后一个任务需要等待前一个任务的执行,这就可能出现长时间的等待,造成浏览器失去响应(假死)。比如ajax网络请求、setTimeout时间延迟、DOM事件的用户交互等,这些任务并不消耗 CPU,是一种空等,资源浪费。
所以,浏览器为这些耗时任务开辟了另外的线程,主要包括http请求线程,浏览器定时触发器,浏览器事件触发线程,这些任务是异步的

同步任务,异步任务?

  • 同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务
  • 异步任务:
    疑惑:对于异步任务,我开始始终无法理解。所谓异步,就是做一件事的同事,也在干另一件事,两件事并发进行。如果异步任务指的是那些被加入到了任务队列中的代码块(也就是所谓的回调函数),那些代码块只是延迟了执行,并没有做到和JS主线程并行执行代码,如何能叫异步任务?
    自己的理解:
    ①我理解的异步任务指的是http请求的过程,setTimeout设置相应时间的等待的过程,onclick等待点击的过程等,这些是由浏览器的其他的线程去执行的,这些过程才和JS主线程是异步的。并不是回调函数
    举个列子:setTimeout设置等待10秒后console.log("haha"),这个等10秒的过程是浏览器的其他线程执行的,是异步的)
    ②至于回调函数,异步任务执行结束后,需要把结果,或者后续的处理交给JS主线程执行,这是通过回调函数实现的
    接着上面的列子::console.log("haha")需要JS主线程执行,就是通过回调函数的方式供JS主线程调用)
    ③那么JS主线程如何拿到异步任务的回调函数呢?JS设计了一个任务队列,异步任务会将相关回调函数添加到任务队列中,因此准确的应该是叫做callback queue(回调函数队列)。最后主线程执行这些回调函数仍然是一个一个同步执行的。所以异步任务的回调函数并没有异步执行,只是挂起,延迟了执行

任务队列
1.主线程之外,还存在一个"任务队列"(callback queue)。用于存放异步任务的回调函数。它一个先进先出的数据结构,排在前面的事件优先被主线程读取。所以对于“定时器”,虽然到了设定的时间,定时器的回调函数被加入到了任务队列中,但是前面如果还有其他的事件没执行完,此时就要等待,那么执行的时间就不一定是设定的时间了
2.回调函数放置时机:
异步操作会将相关回调函数添加到任务队列中。而不同的异步操作添加到任务队列的时机也不同,如 onclick, setTimeout, ajax 处理的方式都不同,这些异步操作是由浏览器内核的 webcore 来执行的,webcore 包含上图中的3种 webAPI,分别是 DOM Binding、network、timer模块。

  • onclick 由浏览器内核的 DOM Binding 模块来处理,当事件触发的时候,回调函数会立即添加到任务队列中。
  • setTimeout 会由浏览器内核的 timer 模块来进行延时处理,当时间到达的时候,才会将回调函数添加到任务队列中。
  • ajax 则会由浏览器内核的 network 模块来处理,在网络请求完成返回之后,才将回调添加到任务队列中。

事件循环?
主线程从"任务队列"中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)

回调函数"(callback)"
就是那些会被主线程挂起来的代码。这些被挂起来的代码会被异步任务添加到任务队列中,等到主线程中的同步代码都执行完毕,这些回调函数就会被一一执行。异步任务必须指定回调函数

图解


图片来自Philip Roberts的演讲《Help, I'm stuck in an event-loop》

  • 主线程就是有虚线组成的那一部分,堆(heap)和栈(stack)共同组成了js主线程;任务队列就是callback queue ;浏览器为异步任务单独开辟的线程可以统一理解为WebAPIs
  • 函数的执行就是通过进栈和出栈实现的,比如图中有一个foo()函数,主线程把它推入栈中,在执行函数体时,发现还需要执行上面的那几个函数,所以又把这几个函数推入栈中,等到函数执行完,就让函数出栈。
  • 等到stack清空时,说明一个任务已经执行完了,这时就会从callback queue中寻找下一个(其实就是回调函数)推入栈中(这个寻找的过程,叫做event loop,因为它总是循环的查找任务队列里是否还有任务)。

2、下面这段代码输出结果是? 为什么?

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是异步执行的任务,它的回调函数会在被设定的时间到达时加入到任务队列,等待JS主线程所有代码执行完成后,才会进行Event Loop,从任务队列中读取回调函数并且执行
  • setTimeout(f,0),指定时间为0,表示的是立刻将回调函数加入到任务队列中,但是任务队列中的回调函数需要等到JS主线程的所有代码都执行完了,才会开始执行,这也就解释了为什么先输出1和3,最后在输出回调函数中的2
  • 所以setTimeout(f,0)的作用是,尽可能早地执行指定的任务,及等到JS主线程的同步任务和“任务队列”中已有的事件全都执行完后立即执行

3、下面这段代码输出结果是? 为什么?

var flag = true;
setTimeout(function(){
    flag = false;
},0)
while(flag){}
console.log(flag);

结果:死循环没有任何结果
原理:setTimeout中设定的函数,需要等到同步代码都执行完才执行,而flag的初始值是true,因此while会运行,而while循环中又没有任何内容,因此会死循环没有任何结果

4、实现一个节流函数

首先理解什么是函数节流
函数节流简单讲就是让一个函数无法在很短的时间间隔内连续执行,只有当上一次函数执行后过了你规定的时间间隔,才能进行下一次该函数的调用。
函数节流有什么用呢?
一定程度上能优化性能。例如,当调整浏览器大小的时候,onresize 事件会连续触发。在onresize 事件处理程序内部如果尝试进行DOM 操作,其高频率的更改可能会让浏览器崩溃。所以可以设置个函数节流,只有当调整窗口停下来歇会才开始触发onresize 事件。
实现原理
第一次调用函数,设置一个定时器,在指定的时间间隔之后运行代码。如果在这个时间间隔内又调用这个函数,那我们就clear掉原来的定时器,再setTimeout一个新的定时器延迟一会执行。
代码:

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属性的元素节点。如果没有发现匹配的节点,则返回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)
/*  输出结果
  <div id="box">
    <div class="color red"></div>
    <div class="color green"></div>
    <p id="content"></p>
  </div> */
  • getElementsByClassName():返回一个类似数组的对象(HTMLCollection类型的对象),包括了所有class名字符合指定条件的元素(搜索范围包括本身),元素的变化实时反映在返回结果中。任何元素节点上可以调用。一个参数,包含一个或多个类名的字符串(类名通过空格分隔,指的是一个元素同时包括多个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] 输出结果:<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)                                    //写法可以级联,box元素节点上也可以调用,结果[div.color.yellow]
  • getElementsByTagName():返回所有指定标签的元素(搜索范围包括本身)。返回值是一个HTMLCollection对象,也就是说,搜索结果是一个动态集合,任何元素的变化都会实时反映在返回的集合中。任何元素节点上可以调用
<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选择器的元素节点。如果有多个节点满足匹配条件,则返回第一个匹配的节点。如果没有发现匹配的节点,则返回null。
var el1 = document.querySelector(".myclass");
var el2 = document.querySelector('#myParent > [ng-click]');

注:参数的写法和css写法一致。querySelector方法无法选中CSS伪元素。

  • querySelectorAll():ES5的元素选择方法。querySelectorAll方法返回匹配指定的CSS选择器的所有节点,返回的是NodeList类型的对象。NodeList对象不是动态集合,所以元素节点的变化无法实时反映在返回结果中
elementList = document.querySelectorAll(selectors);

querySelectorAll方法的参数,可以是逗号分隔的多个CSS选择器。

var matches = document.querySelectorAll("div.note, div.alert"); 

6、创建元素、添加元素

创建元素

  • createElement():生成HTML元素节点。生成的节点是存在于内存中的,还没如被加入到DOM中。
var newDiv = document.createElement("div");

createElement方法的参数为元素的标签名,即元素节点的tagName属性。如果传入大写的标签名,会被转为小写。如果参数带有尖括号(即<和>)或者是null,会报错。

  • createTextNode():生成文本节点,参数为所要生成的文本节点的内容。
var newContent = document.createTextNode("Hello");
  • createDocumentFragment():生成一个DocumentFragment对象。DocumentFragment对象是一个存在于内存的DOM片段,但是不属于当前文档,常常用来生成较复杂的DOM结构,然后插入当前文档。这样做的好处在于,因为DocumentFragment不属于当前文档,对它的任何改动,都不会引发网页的重新渲染,比直接修改当前文档的DOM有更好的性能表现。
    有什么用呢?
    举个列子:向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节点中,这个虽然只和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)
//方法三,最优的方法。先将li全部放入一个fragment对象中,最后一次性添加进相应的DOM节点中,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也可以添加元素,不需要通过创建节点,在appendChild的方式添加到DOM中,只需要HTML结构的字符串就可以添加

<ul class="navbar"></ul>
  var navData = [1, 2, 3]
  var html = ""
  navData.forEach(function(item){
    html += "<li>" + item + "</li>"
  })                               //html结果:"<li>1</li><li>2</li><li>3</li>"
  document.querySelector(".navbar").innerHTML = html

因此也可以用 document.querySelector(".navbar").innerHTML直接获取某个节点中的HTML结构的字符串
和innerText的区别?

  var navData = [1, 2, 3]
  var html = ""
  navData.forEach(function(item){
    html += "<li>" + item + "</li>"
  })                               //html结果:"<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>这行文字,不会转换为HTML结构,因此会在页面中显示这行文字
所以innerText也可以用来获取元素内包含的文本内容,在多层次的时候会按照元素由浅到深的顺序拼接其内容
Ex:

<div>
    <p>
        123
        <span>456</span>
    </p>
</div>

外层div的innerText返回内容是 "123456"
注意:让用户输入的内容可以用innerText,不要用innerHTML,因为如果用户输入的html结构的字符串中包含恶意的JS代码,innerHTML会执行,容易招受攻击

修改元素

  • appendChild():在元素末尾添加元素
var child = document.createElement("div")
var Text = document.createTextNode("哈哈")
child.appendChild(Text)                             
document.body.appendChild(child)
  • insertBefore():在当前节点的某个子节点之前再插入一个子节点。
  <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)

注:想要插入到某个子节点之后,没有 insertAfter方法。可以使用 insertBefore方法和 nextSibling来模拟它。

var item3 = document.createElement("li")
var item2 = document.getElementById("item2")
var menu = item2.parentNode
menu.insertBefore(item3, item2.nextSibling)
  • replaceChild():用指定的节点替换当前节点的一个子节点,并返回被替换掉的节点。
replacedNode = parentNode.replaceChild(newChild, oldChild);
//newChild 用来替换 oldChild 的新节点。如果该节点已经存在于DOM树中,则它会被从原始位置删除。
//replacedNode 和oldChild相等。
  • removeChild():删除元素
parentNode.removeChild(childNode);
  • cloneNode():克隆元素,方法有一个布尔值参数,传入true的时候会深复制,也就是会复制元素及其子元素(IE还会复制其事件),false的时候只复制元素本身
node.cloneNode(true);

参考
js的单线程和异步
JavaScript 运行机制详解:再谈Event Loop

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容