本系列将从以下专题去总结:
暂时会对以上四个专题去总结,现在开始Part4: 事件对象与事件机制。下图是我这篇的大纲。
同步(Synchronous):你在做一件事情,不能同时去做另外一件事。
异步(Asynchronous):你在做一件事情,这件事可能会耗时很久,而此时你可以在等待的过程中,去做另外一件事。
比如煮开水这件事吧…在这过程,你担心水沸了而不去做其它事情,就等到水沸腾,那就是同步。
而你觉得这过程耗时蛮久,可以先去做其它事情,比如去扫地,直到水沸腾。这就是异步。
1.进程(process): 程序的一次执行, 它占有一片独有的内存空间。可以通过windows
任务管理器查看进程。进程负责为程序的运行提供必备的环境,相当于工厂的车间。
2.线程(thread): 是进程内的一个独立执行单元。 是CPU的最小的调度单元。 是程序执行的一个完整流程。线程负责执行进程中的程序,相当于工厂工作的工人。
3.图解进程、线程和程序的关系:
一个程序A有多个进程,那么程序A 就是多进程的程序。程序B是只有一个进程,那么程序B就是单进程的程序。
如果一个进程有一个线程,那么这个程序就是单线程的。如果一个进程有多个线程,那么这个程序就是多线程的。单和多 线程是针对进程而言的。比如,我一个程序有两个进程,这两个进程分别有一个线程,那么这个程序还是单线程的程序。
4.进程与线程
5.何为多进程与多线程?
JS是单线程运行的
在JS设计的本意只是对一些简单的操作而已,比如提交表单用户名和密码之类的。当没有JS时,那么这些数据就会提交到服务器中,那么这个数据处理将会特别大,首先假设有1000个人同时注册,那么这些请求就会到服务器上,服务器的加载量就会很大,而且,用户体验也不好,可能会延迟返回请求信息。这是如果这些操作在浏览器端来操作,那么就会简单很多。所以,JS当时设计的初衷也就单线程了,因为不需要太多的操作。单线程足矣应付,而且不占太多的内存。当然后面会说道,因为他的功能(DOM操作等)也决定了它只能单线程。
但使用H5中的 Web Workers可以多线程运行(主线程只有一个,要做其他的事可以启动分线程)
8.浏览器运行是单进程还是多进程?
9.如何查看浏览器是否是多进程运行的呢 ?
10.浏览器运行是单线程还是多线程?
浏览器内核:支撑浏览器运行的最核心的程序。
webkit
内核Gecko
内核Trident
内核Trident + webkit
(双核,嘻嘻,给你一个眼神~)主线程
分线程
1.如何证明js执行是单线程的?
setTimeout()
的回调函数是在主线程执行的2.为什么js要用单线程模式, 而不用多线程模式?
3.代码的分类:
4.js引擎执行代码的基本流程
<script type="text/javascript">
setTimeout(function () {
console.log('timeout 2222')
}, 2000)
setTimeout(function () {
console.log('timeout 1111')
}, 1000)
function fn() {
console.log('fn()')
}
fn()
console.log('alert()之前')
alert('------') //暂停当前主线程的执行, 同时暂停定时器的计时, 点击确定后, 恢复程序执行和计时。
console.log('alert()之后')
</script>
5.js是单线程执行的(回调函数也是在主线程)
6.H5提出了实现多线程的方案: Web Workers
7.只能是主线程更新界面
1.定时器真是定时执行的吗?
2.定时器回调函数是在分线程执行的吗?
3.定时器是如何实现的?
<button id="btn">启动定时器</button>
<script type="text/javascript">
document.getElementById('btn').onclick = function () {
var start = Date.now()
console.log('启动定时器前...')
setTimeout(function () {
console.log('定时器执行了', Date.now()-start)
}, 200)
console.log('启动定时器后...')
// 做一个长时间的工作
for (var i = 0; i < 1000000000; i++) {
}
}
</script>
dom
事件监听, 设置定时器, 发送ajax
请求的代码dom
事件监听, 设置定时器, 发送ajax
请求的各自的回调函数)以下这张图就是event-driven interaction model(事件驱动模型)。
另外简单的说一下request-response model(事件响应模型),这个就相当于浏览器去服务器请求一些数据,服务器接收到这些请求,去处理这些请求,紧接着返回给浏览器的请求数据,浏览器接收到数据解析到页面上的一个过程。
现在我们主要是看一下event-driven interaction model:
首先,这个图分为三个部分:JS引擎等主线程、浏览器内核的分线程、任务队列。
在第一部分中:(堆内存和栈内存)
在第二部分中:
这一块主要是交给浏览器的分线程处理。以setTimeout
定时器为比较,他会拿到回调函数和延迟时间1000,当延迟时间过了之后,就会把回调函数推入队列中。
在第三部分中:
临时保存着回调函数,当执行栈为空时,就会依次将其回调函数压入执行栈中。
这个部分叫做callback queue
。也叫任务队列(task queue)、消息队列(message queue)、事件队列(event queue)。指的都是同一个。
刚刚以定时器介绍了这个过程。我们再以AJAX为例看看是如何执行这些过程的?
上图以AJAX异步请求为例,发起异步任务后,由AJAX线程执行耗时的异步操作,而JS引擎线程继续执行堆中的其他同步任务,直到堆中的所有异步任务执行完毕。然后,从消息队列中依次按照顺序取出消息作为一个同步任务在JS引擎线程中执行,那么AJAX的回调函数就会在某一时刻被调用执行。
另外一点,我们看到事件机制模型图有事件轮询(event loop),就是从任务队列中循环取出回调函数放入执行栈中处理(一个接一个)。JS引擎线程用来执行栈中的同步任务(初始化代码),当所有同步任务(初始化代码)执行完毕后,栈被清空,然后读取消息队列中的一个待处理任务,并把相关回调函数压入栈中,单线程开始执行新的同步任务。JS引擎线程从消息队列中读取任务是不断循环的,每次栈被清空后,都会在消息队列中读取新的任务,如果没有新的任务,就会等待,直到有新的任务,这就叫事件轮询。
宏任务队列
微任务队列
promise
对象的成功的回调和progress.nextTick()
function fun() {
console.log('程序开始执行', 11111111);
setTimeout(function () {
console.log('定时器开始执行',666666);
}, 0)
new Promise(function (resolve, reject) {
console.log('promise对象开始执行', 2222222);
for (var i = 0; i < 5; i++) {
console.log(i, 33333333);
}
resolve();
})
.then(() => {
console.log('promise对象成功的回调执行', 555555);
})
.then(() => {
console.log('promise对象失败的回调执行', 555555);
});
console.log('程序执行完毕', 444444444444);
}
fun();
//以上程序执行顺序结构就是上述的数字123456.
Web Workers 是 HTML5 提供的一个javascript多线程解决方案,我们可以将一些大计算量的代码交由web Worker运行而不冻结用户界面,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。
实现一个斐波那契数列,在页面input
中输入数列项的值,得到相应的数列值。
<input type="text" placeholder="数值" id="number">
<button id="btn">计算</button>
<script type="text/javascript">
//斐波那契数列: 1 1 2 3 5 8 f(n) = f(n-1) + f(n-2)
function fibonacci(n) {
return n<=2 ? 1 : fibonacci(n-1) + fibonacci(n-2)
//递归调用(效率很低,时间复杂度很大)
}
var input = document.getElementById('number')
document.getElementById('btn').onclick = function () {
var number = input.value;
var result = fibonacci(number);
//主线程会一直在处理这个递归调用。导致冻结了用户界面,也就是不能操作界面了。
alert(result)
}
</script>
以上操作会在js引擎的主线程中,在计算的过程中,会冻结用户界面,达到不佳的用户体验。
H5规范提供了js分线程的实现,取名为: Web Workers。它支持JavaScript多线程的操作。
相关API
Worker
: 构造函数, 加载分线程执行的js文件Worker.prototype.onmessage
: 用于接收另一个线程的回调函数Worker.prototype.postMessage
: 向另一个线程发送消息使用步骤
步骤1:创建在分线程执行的js文件
//workers.js文件
function fibonacci(n) {
return n<=2 ? 1 : fibonacci(n-1) + fibonacci(n-2) //递归调用
}
console.log(this)
//当接受到主线程的数据时
this.onmessage = function (event) {
var number = event.data
console.log('分线程接收到主线程发送的数据: '+number)
//计算(目的:让复杂的、耗时的运算放在分线程中处理)
var result = fibonacci(number)
postMessage(result) //正因为可以直接使用这个方法,是因为在全局对象中有这个方法
console.log('分线程向主线程返回数据: '+result)
// alert(result) alert是window的方法, 在分线程不能调用。
// 分线程中的全局对象不再是window, 所以在分线程中不可能更新界面
}
步骤2:在主线程中的js中发消息并设置回调
//主线程
<input type="text" placeholder="数值" id="number">
<button id="btn">计算</button>
<script type="text/javascript">
var input = document.getElementById('number')
document.getElementById('btn').onclick = function () {
var number = input.value
//创建一个Worker对象
var worker = new Worker('worker.js')
// 绑定接收消息的监听(这个位置与向分线程发送消息的代码位置可交换)
worker.onmessage = function (event) {
console.log('主线程接收分线程返回的数据: '+event.data)
alert(event.data)
}
// 向分线程发送消息
worker.postMessage(number)
console.log('主线程向分线程发送数据: '+number)
}
// console.log(this) // window
</script>
回顾4.6.2的案例引入,我们可知,那个是完全在主线程中操作,带来的弊端就是冻结了用户界面。而使用Workers在分线程中处理耗时的运算,在主线程去接受计算好的数据,就可以解决直接使用主线程的冻结用户界面的弊端,这个时候不会冻结用户界面,但是子线程完全受主线程控制,且子线程不得操作DOM,因为其this
不是window
。
案例1
console.log("1");
setTimeout(function(){
console.log("2");
},1000);
console.log("3");
setTimeout(function(){
console.log("4");
},0);
输出结果: 1->3->4->2.
案例1分析:
案例2
//同步code1
var t = true;
//异步code2
window.setTimeout(function (){
t = false;
},1000);
//同步code2
while (t){}
//同步code3
alert('end');
案例2分析:
同步code1
-> 同步code2
。同步code2
时while(true){}
,进入死循环,说明这个时候栈中的同步代码永远不会执行完,也就栈永远不会清空出来,那么任务队列中的代码就不会执行。也就是任务队列中的异步的代码就无法执行。案例3
//只有用户触发点击事件才会被推入队列中(如果点击时间小于定时器指定的时间,则先于定时器推入,否则反之)
document.querySelector("#box").onclick = function(){
console.log("click");
};
//第一个推入队列中
setTimeout(function(){
console.log("1");
},0);
//第三个推入队列中
setTimeout(function(){
console.log("2");
},1000);
//第二个推入队列中
setTimeout(function(){
console.log("3");
},0);
执行结果:如上面代码段中的分析。
案例3分析:
以上都是异步代码,包括onclick
那个。一定要分清哪些是异步的代码。异步代码中的回调函数都会定义在heap
中,也就是在右边的堆分配一块内存给他们,这个时候根据他们指定的时候结束后,把他们的回调函数放到任务队列等待执行。
setTimeout
的作用是在间隔一定的时间后,将回调函数插入消息队列中,等栈中的同步任务都执行完毕后,再执行。因为栈中的同步任务也会耗时,所以间隔的时间一般会大于等于指定的时间(指定的时间就是回调函数后面一个参数的毫秒值)。
setTimeout(fn, 0)
的意思是,将回调函数fn立刻插入消息队列,等待执行,而不是立即执行。只有等待同步任务全部执行完,然后js引擎(js虚拟机)就去从任务队列中拿出来去执行。
案例4
setTimeout(function() {
console.log("a")
}, 0)
for(let i=0; i<10000; i++) {}
console.log("b")
执行结果:先输出b 再输出a
案例4分析:
这个与案例3就差不多了。先执行for
循环的同步代码。定时器是异步代码,先等线程的同步代码执行结束后在从任务队列中去拿这些异步代码段执行。
案例5
执行下面这段代码,执行后,在 5s 内点击两下,过一段时间(>5s)后,再点击两下,整个过程的输出结果是什么?
//异步代码
setTimeout(function(){
for(var i = 0; i < 100000000; i++){}
console.log('timer a');
}, 0)
//同步代码
for(var j = 0; j < 5; j++){
console.log(j);
}
//异步代码
setTimeout(function(){
console.log('timer b');
}, 0)
//函数
function waitFiveSeconds(){
var now = (new Date()).getTime();
while(((new Date()).getTime() - now) < 5000){}
console.log('finished waiting');
}
//异步代码
document.addEventListener('click', function(){
console.log('click');
})
//同步代码
console.log('click begin');
//同步代码,调用函数,执行函数体
waitFiveSeconds();
案例5分析:
首先,先执行同步任务。其中waitFiveSeconds
是耗时操作,持续执行长达5s。
0
1
2
3
4
click begin
finished waiting
然后,在JS引擎线程执行的时候,timer a
对应的定时器产生的回调、 timer b
对应的定时器产生的回调和两次click
对应的回调被先后放入消息队列。由于JS引擎线程空闲后,会先查看是否有事件可执行,接着再处理其他异步任务。因此会产生 下面的输出顺序。
click
click
timer a
timer b
最后,5s 后的两次 click 事件被放入消息队列,由于此时JS引擎线程空闲,便被立即执行了。
click
click
案例6
<script>
for (var i = 0; i < 5; i++){
var btn = document.createElement('button');
btn.appendChild(document.createTextNode('Button ' + i));
btn.addEventListener('click', function (){
console.log(i);
});
document.body.appendChild(btn);
}
// 1、点击 Button 4,会在控制台输出什么? 5
/*An:不管点击哪个button都是输出5.*/
// 2. 给出一种预期的实现方式
/* 将for循环中的var 变成 let 或者 用对象.属性保存起来i的值 */
</script>
此文档为吕涯原创,可任意转载,但请保留原链接,标明出处。
文章只在CSDN和掘金第一时间发布:
CSDN主页:https://blog.csdn.net/LY_code
掘金主页:https://juejin.im/user/5b220d93e51d4558e03cb948
若有错误,及时提出,一起学习,共同进步。谢谢。