在理解事件循环之前我们先了解一下浏览器进程和线程二三事
进程和线程
进程:cpu资源分配的最小单位(是能拥有资源和独立运行的最小单位)
线程:进程中执行的每一个任务指的就是线程,系统不会为其分配内存资源,各个线程共享进程拥有的内存资源。cpu调度的最小单位(线程是建立在进程的基础上的一次程序运行单位)
两者的关系:
1、一个程序至少有一个进程,一个进程至少有一个线程
2、线程不能脱离进程而独立运行
3、线程没有地址空间,线程包含在进程的地址空间中。线程上下文只包含一个堆栈、一个寄存器、一个优先权
4、进程内的任何线程都被看做是同位体,且处于相同的级别
5、进程中任何线程都可以通过销毁主线程来销毁进程,销毁主线程将导致该进程的销毁,对主线程的修改可能影响所有的线程。
浏览器的进程
浏览器是多线程的,一般来说每打开一个标签页就算是创建了一个独立的进程
除了标签进程完,浏览器进程还有:
1、Browser进程:浏览器主进程,负责协调、主控
负责浏览器界面显示,与用户交互,如前进、后退等
负责各个页面的管理,创建和销毁其他进程
将Renderer进程得到的内存中的Bitmap,绘制到用户界面上
网络资源的管理,下载等
2、CPU进程
用于硬件加速图形绘制
3、渲染进程(也称浏览器内核或Renderer进程)多线程
用于解析页面,渲染页面,执行脚本,处理事件等等
4、第三方插件进程
每种类型的插件对应一个进程,仅当使用该插件时才创建
渲染进程
里面有多个线程,靠着这些现成共同完成渲染任务
1、图形用户界面GUI渲染线程
负责渲染浏览器界面,解析HTML,CSS,构建DOM树和RenderObject树,布局和绘制等
当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时
2、JS引擎线程
负责处理Javascript脚本程序。(例如V8引擎)
浏览器无论什么时候都只有一个JS引擎在运行JS程序
为什么是单线程?
因为js主要用途是与用户互动与操作dom,所有决定了它只能是单线程,否则会带来复杂的同步问题,例如:如果是多线程,一个线程在dom上添加内容,一个线程删除了这个dom,这时候浏览器应该以哪一个为准就比较复杂了
但是为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许js脚本创建多个线程,但是子线程受主线程控制,且不能操作dom
注意:GUI渲染线程与JS引擎线程是互斥的
3、事件触发线程
归属于渲染(浏览器内核)进程,不受JS引擎线程控制
用于控制事件(例如鼠标,键盘等事件),当该事件被触发时候,事件触发线程就会把该事件的处理函数添加进任务队列中,等待JS引擎线程空闲后执行
4、定时触发器线程
setInterval与setTimeout所在线程
浏览器定时计数器并不是由JavaScript引擎计数的,(因为JavaScript引擎是单线程的, 如果处于阻塞线程状态就会影响记计时的准确)
因此通过单独线程来计时并触发定时(计时完毕后,添加到事件队列中,等待JS引擎空闲后执行)
注意:W3C的HTML标准中规定,setTimeout中低与4ms的时间间隔算为4ms
5、异步HTTP请求线程
在XMLHttpRequest在连接后是通过浏览器新开一个线程请求
将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将这个回调再放入事件队列中。再由JavaScript引擎执行。
总结:需要知道的是浏览器中js线程只有一个,如果发起一个异步IO请求,在等待响应的这段时间后面的代码会被阻塞。由于js主线程和GUI渲染线程是相互阻塞的,所以就造成了浏览器假死。
什么是事件循环
事件循环本质是用来做调度的,决定何时将资源分配给谁
V8只是负责Js代码的解析和执行,而事件循环决定了V8什么时候执行什么代码。
基本概念
堆:用以表示一个大部分非结构化的内存区域
对象、数组被存放在堆中
栈:js中的调用栈(代码执行的地方),是一种后进先出的数组结构
JavaScript 是一门单线程的语言,这意味着它只有一个调用栈,因此,它同一时间只能做一件事。
每调用一个函数,解释器就会把该函数添加进调用栈并开始执行
正在调用栈中执行的函数还调用了其它函数,那么新函数也将会被添加进调用栈,一旦这个函数被调用,便会立即执行。
当前函数执行完毕后,解释器将其从栈顶移出调用栈,继续执行当前执行环境下的剩余的代码
当分配的调用栈空间被占满时,会引发“堆栈溢出”。
注:这里的堆栈,是数据结构的堆栈,不是内存中的堆栈(内存中的堆栈,堆存放引用类型的数据,栈存放基本类型的数据)
同步任务:调用立即得到结果的任务
在主线程排队执行的任务,只能前一个任务执行完后才能执行下一个任务
异步任务:调用无法立即得到结果,需要额外的操作才能预期结果的任务
异步任务是不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。
异步任务运行机制:
1、所有同步任务都在主线程上执行,形成一个执行栈
2、主线程之外,还存在一个"任务队列"。只要异步操作执行完成,就到任务队列中排队
3、一旦执行栈中的所有同步任务执行完毕,系统就会按次序读取任务队列中的异步任务,于是被读取的异步任务结束等待状态,进入执行栈,开始执行
4、主线程不断重复上面的第三步。
主线程从"任务队列"中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)
任务队列:是一种先进先出的一种数据结构
task(任务)
在事件循环中,每进行一次循环操作称为tick
分为宏任务(macrotask )和微任务(microtask )
并且每个宏任务结束后, 都要清空所有的微任务,这里的 Macro Task也是我们常说的 task ,
宏任务
包括:script( 整体代码)、setTimeout、setInterval、ajax、dom操作、I/O、UI 交互事件、setImmediate(Node.js 环境)
微任务(特殊的任务)
包括:process.nextTick(Node.js 环境)、Promise、MutaionObserver(DOM变化监听)
执行顺序:
JS 引擎会将所有任务按照类别分到这两个队列中
1、在宏任务的队列中取出第一个任务(一般最初始,宏任务队列中,只有一个 script(整体代码)任务)
2、执行完毕后取出微任务队列中的所有任务顺序执行
3、再取宏任务,周而复始,直至两个队列的任务都取完
tip: 由于microtask 优先于 task 执行,所以如果有需要优先执行的逻辑,放入microtask 队列会比 task 更早的被执行
setTimeOut、setInterval
如果第二个参数为0或不设置,意味着不是立即执行,而是在主线程最早可得的空闲时间执行
所以说,第二个参数设置的时间并不是绝对的,它需要根据当前代码最终执行的时间来确定的,例:如果当前代码执行的时间(如执行200ms)超出了推迟执行(setTimeout(fn, 100))或反复执行的时间(setInterval(fn, 100)),那么setTimeout(fn, 100) 和 setTimeout(fn, 0) 也就没有区别了,setInterval(fn, 100) 和 setInterval(fn, 0) 也就没有区别了。
Promise
回调函数是立即执行的;.then()是在执行栈之后任务队列之前执行,属于微任务
process.nextTick
在执行栈尾部,总是发生在所有异步任务之前
setImmediate
Node.js提供的一个与任务队列有关的方法,追加在任务队列的尾部,它和 setTimeout(fn, 0) 很像,但优先级都是 setTimeout 优先于 setImmediate。
同步代码(宏任务) > process.nextTick > Promise(微任务)> setTimeout(fn)、setInterval(fn)(宏任务)> setImmediate(宏任务)> setTimeout(fn, time)、setInterval(fn, time),其中time>0
异步编程
回调函数
优点是简单、容易理解和部署
缺点是容易产生回调地狱
事件监听
采用事件驱动模式
任务的执行不取决于代码的顺序,而取决于某个事件是否发生。
function f1(){
setTimeout(function () {
// f1的任务代码
f1.trigger('done'); // 执行完成后,立即触发done事件,从而开始执行f2
}, 1000);
}
f1.on('done', f2); // 当f1发生done事件,就执行f2
优点:可以绑定多个事件,每个事件可以指定多个回调函数,而且可以"去耦合"(Decoupling),有利于实现模块化
缺点: 整个程序都要变成事件驱动型,运行流程会变得很不清晰。
发布/订阅(观察者模式)
jQuery.subscribe("done", f2);
function f1(){
setTimeout(function () {
// f1的任务代码
jQuery.publish("done") ; // f1执行完成后,向"信号中心"jQuery发布"done"信号,从而引发f2的执行。
}, 1000);
}
jQuery.unsubscribe("done", f2); // f2完成执行后,也可以取消订阅(unsubscribe)
Promise
对象的状态不受外界影响
一旦状态改变,就不会再变,任何时候都可以得到这个结果
Promise对象的状态改变,只有两种可能:从Pending变为Resolved和从Pending变为Rejected。
优点:将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。此外,Promise对象提供统一的接口,使得控制异步操作更加容易
缺点:
1、无法取消Promise,一旦新建它就会立即执行,无法中途取消;
2、如果不设置回调函数,Promise内部抛出的错误,不会反应到外部
3、当处于Pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)
注意以下几点:
1、在使用new
创建Promise(...)
时,里面的代码是自动执行的,Promise
参数中的匿名函数与主线程同步执行,只有then
里面的东西才是异步执行的
2、同一个Promise
函数中有多个resolve
或reject
调用时,只会执行最前面的一个,剩下的不会执行
new Promise((res, rej) => {
res(1)
rej(2)
res(3)
}).then((res) => {
console.log('then:', res)
}).catch(res =>{
console.log('catch:', res)
})
// then:1
3、一旦promise的内部状态发生变化并获得一个值,则后面对.then
或.catch
的每次调用都直接获取该值
const prom = new Promise((res,rej) => {
setTimeout(() => {
console.log(111)
res(222)
}, 1000)
})
const start = Date.now()
prom.then(res => {
console.log(res, Date.now() - start, '333')
})
prom.then(res => {
console.log(res, Date.now() - start, '444')
})
// 111
// 222 1054 333
// 222 1054 444 注意时间是一样的
// prom的`.then`和`.catch`可以被多次调用,但是上面的例子中Promise构造函数只执行了一次,所以每次调用都直接获取值
4、.then
或.catch
的返回值不能是promise
本身,否则会导致无限循环
5、在.then
或.catch
中返回错误对象不会引发错误,后续的.catch
不会捕获该错误对象
Promisr.resolve()
.then(() => {
return new Error('error') // 返回任何非promise的值都将包装到一个Promise对象中,相当于 Promise.resolve(new Error('error'))
// 为了能在后续catch中捕获,可以修改为return Promise.reject(new Error('error))
}).then(res => {
console.log('then', res)
}).catch(err =>{
console.log('catch', err)
})
// then Error: error
6、.then
和.catch
的参数应该为函数,如果是非函数则会忽略值得结果
Promise.resolve(1)
.then(2) // 非函数,被忽略
.then(Promise.resolve(3)) // 非函数,被忽略
.then(console.log)
// 1
7、.then
接收两个参数,第一个是处理成功的函数,第二个是处理失败的函数,但是第二个处理失败的函数不能捕获第一个成功函数抛出的错误,.catch
是第二个处理失败函数的便捷方法,捕获前面的错误。
Promise.resolve()
.then((res) => {
throw new Error('Error in success')
}, (err) => {
console.log('error', err)
})
// .then((res) => {
// },(e) => {
// console.log('catch error', e)
// })
.catch(err => {
console.log('catch error', erro)
})
// catch error Error: Error in success
Generator函数
Generator函数返回的遍历器对象,yield语句暂停,调用next方法恢复执行,如果没遇到新的yeild,一直运行到return语句为止,return 后面表达式的值作为返回对象的value值,如果没有return语句,一直运行到结束,返回对象的value为undefined。
async/await
async函数就是Generator函数的语法糖。
async函数返回一个 Promise对象,可以使用then方法添加回调函数
关键字await使async函数一直等待(执行栈当然不可能停下来等待的,await将其后面的内容包装成promise交给Web APIs后,执行栈会跳出async函数继续执行),直到promise执行完并返回结果。
await只在async函数函数里面奏效。
await关键字后面的函数里面的同步代码和主线程同步执行
await语句后面的语句就相当于是await XXX promise resolved后then里面的东西
Async/Await 通过同步的方式实现异步
第一个请求的返回值作为后面一个请求的参数,其中每一个参数都是一个promise对象
(async () => {
var a = await A();
var b = await B(a);
var c = await C(b);
var d = await D(c);
})();
Promise、async/await和setTimeout的执行顺序
async function async1() {
console.log('async1 start');
await async2();
console.log('asnyc1 end');
}
async function async2() {
console.log('async2');
}
console.log('script start');
setTimeout(() => {
console.log('setTimeOut');
}, 0);
async1();
new Promise(function (reslove) {
console.log('promise1');
reslove();
}).then(function () {
console.log('promise2');
})
console.log('script end');
执行结果:
script start
async1 start
async2
promise1
script end
asnyc1 end
promise2
setTimeOut
理解:
1、new Promise是同步的任务,会被放到主进程中去立即执行。而.then()函数是异步任务会放到异步队列中去,那什么时候放到异步队列中去呢?当你的promise状态结束的时候,就会立即放进异步队列中去了
2、带async关键字的函数会返回一个promise对象,如果里面没有await,执行起来等同于普通函数,和主线程同步执行
3、如果带await,此时的await会让出线程,阻塞async内后续的代码,先去执行async外的代码。等外面的同步代码执行完毕,才会执行里面的后续代码。就算await的不是promise对象,是一个同步函数,也会等这样操作
注:setTimeOut并不是直接的把你的回掉函数放进上述的异步队列中去,而是在定时器的时间到了之后,把回掉函数放到执行异步队列中去。如果此时这个队列已经有很多任务了,那就排在他们的后面。这也就解释了为什么setTimeOut为什么不能精准的执行的问题了。
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2() {
//async2做出如下更改:
new Promise(function(resolve) {
console.log('promise1');
resolve();
}).then(function() {
console.log('promise2');
});
}
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0)
async1();
new Promise(function(resolve) {
console.log('promise3');
resolve();
}).then(function() {
console.log('promise4');
});
console.log('script end');
输出结果:
script start
async1 start
promise1
promise3
script end
promise2
async1 end
promise4
setTimeout
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。