在理解事件循环之前我们先了解一下浏览器进程和线程二三事

进程和线程

进程: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函数中有多个resolvereject调用时,只会执行最前面的一个,剩下的不会执行
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

zhouing
4 声望2 粉丝