学习目标:
- 了解进程与线程
- 浏览器原理
- EVENT-LOOP(事件循环)
- Promise
计算机原理-进程、线程
- 进程:
cpu资源分配的最小单位
。(拥有独立的资源库,两个进程间是不会共享资源的,是相互不影响的)。 - 线程:
cpu调度的最小单位
。(同一进程下的两个线程,是资源共享的,共同使用进程内的资源)。 - 例子:比如浏览器和音乐播放器是两个进程,浏览器内的两个tab页面也是进程;一个进程包含多个线程,每个线程
并行
执行不同的任务。其中一个线程崩溃了,那么整个进程也就崩溃了。线程之间可以相互通信。
浏览器原理
GUI渲染线程、JS引擎线程、定时器的触发线程、事件触发线程、异步http请求线程
白屏原因:
- GUI线程在等JS引擎线程执行完毕,JS引擎线程执行不完GUI线程不空闲,它俩是相互排斥的。
- js报错捕获住,CUI线程执行完。
CUI渲染线程
- 解析HTML、CSS构建DOM树 -> 布局 -> 绘制
- 与JS引擎线程互斥,当执行JS引擎线程时,CUI渲染会被挂起,当任务队列空闲时,主线程才会执行GUI
- 这也是某个JS线程中出错,线程卡住了,会出现白屏状态的原因
JS引擎线程
- 处理JS,解析执行脚本
- 分配、处理、执行了待执行脚本同时,处理待执行事件,维护事件队列
- 阻塞GUI渲染。JS为何会阻塞GUI渲染,因为网页是单线程的,JS执行的时候,渲染会被挂起。
定时器触发线程
- 异步定时器的处理和执行: setTimeout / setInterval
- 接收JS引擎分配的定时器任务,并执行
- 处理完成后交于事件触发线程
事件触发线程
- 接收所有来源的事件
- 将回调的事件依次加入到任务队列的队尾,交给JS引擎执行
异步HTTP请求线程
- 异步执行请求类操作
- 接收JS引起线程异步请求操作
- 监听回调,交给事件触发线程做处理
EVENT-LOOP
为什么JS是单线程的?
- JS是一门
单线程
语言,也就是说同一时间只能做一件事。这是因为JS作为浏览器脚本语言,主要用来处理与用户的交互、网络以及操作DOM。这决定了它只能是单线程的,否则会带来很复杂的同步问题。 - 假设JS有两个线程,一个线程在某个DOM节点上添加内容,另外一个线程删除了这个节点,这时浏览器应该以哪个线程为准?
- JS的任务也要一个接一个的执行,如果某个任务(比如加载高清图片)是个耗时任务,那浏览器岂不是一直卡着?为了防止主线程的阻塞,JS有了
同步
和异步
的概念。
同步和异步
- 同步:如果在一个函数返回的时候,调用者就能够得到预期的结果,那么这个函数就是同步的。也就是说同步方法调用一旦开始,调用者必须等到该函数调用返回后,才能继续后续的行为。
js
//下面这段段代码首先会弹出 alert 框,如果你不点击 `确定` 按钮,所有的页面交互都被锁死,并且后续的 console 语句不会被打印出来。
alert('Yancey');
console.log('is');
console.log('the');
console.log('best');
- 异步:如果在函数返回的时候,调用者还不能够得到预期结果,从而需要在将来通过一定的手段得到,那么这个函数就是异步的。
比如说发一个网络请求,告诉主程序等到接收到数据后在通知,然后就可以去做其他事情了。当异步完成后,会通知,但是此时可能程序正在做其他的事情,所以即使异步完成了也需要在一旁等待,等到程序空闲下来才有时间去看哪些异步已经完成了,在去执行。
js
//这也就是定时器并不能精确在指定时间后输出回调函数结果的原因。
setTimeout(() => {
console.log('yancey');
}, 1000);
for (let i = 0; i < 100000000; i += 1) {
// todo
}
执行栈和任务队列
数据结构:栈、堆、队列
- 栈(stack):栈是遵循
后进先出
原则的有序集合,新添加或待删除的元素都保存在同一端,称为栈顶,另一端叫做栈底。在栈里,新元素都靠近栈顶,旧元素都接近栈底。栈在编程语言的编译器和内存中存储基本数据类型和对象的指针、方法调用等。 - 堆(heap):堆是基于树抽象数据类型的一种特殊的数据结构。
- 队列(queue):对垒是遵循
先进先出
原则的有序集合,队列在尾部添加新元素,并在顶部移除元素,最新添加的元素必须在队列的末尾。在计算机学中,最常见的例子就是打印队列。
如上图所示,JS中的内存分为堆内存
和栈内存
- JS中引用类型值的大小是不固定的,因此它们会被存储到
堆内存
中,由系统自动分配存储空间。JS不允许直接访问堆内存中的位置,因此不能直接操作对象的堆内存空间,而是操作对象的引用。 - JS中的基础数据类型都有固定的大小,因此它们被存储到
栈内存
中。可以直接操作保存在栈内存空间的值,因此基础数据类型都是按值访问。因此,栈内存还会存储,对象的引用以及函数执行时的运行空间。
执行栈
当调用一个方法的时候,JS会生成一个与这个方法对应的执行环境,又叫执行上下文(context)。这个执行环境中保存着该方法的私有作用域、上层作用域(作用域链)、方法的参数,以及这个作用域中定义的变量和this的指向,而当一系列方法被依次调用的时候,由于JS是单线程的,这些方法就会按顺序被排列在一个单独的地方,这个地方就是所谓执行栈。
任务队列
事件队列是一个存储着
异步任务
的队列,其中的任务严格按照时间先后顺序执行,排在队头的任务将会率先执行,而排在队尾的任务会最后执行。事件队列每次仅执行一个任务,在该任务执行完毕之后,在执行下一个任务,执行栈则是一个类似于函数调用栈的运行容器,当执行栈为空时,JS引擎便检查事件队列,如果事件队列不为空的话,事件队列便将第一个任务压入执行栈中运行。
事件循环(Event Loop)
在异步代码完成后仍有可能要在一旁等待,因此此时程序可能在做其他的事情,等到程序空闲下来才有时间去看哪些异步已经完成了。所以JS有一套机制去处理同步和异步操作,就是
事件循环
(Event Loop)。
解析:*
- 所有同步任务都在主线程上执行,形成一个执行栈(Execution Context Stack)。
- 而异步任务会被放置到Task Table,也就是上图中的
异步处理模块
,当异步任务有了运行结果,就将该函数移入任务队列。 - 一旦执行栈中的所有同步任务执行完毕,JS引擎就会读取任务队列,然后将任务队列中的第一个任务压入执行栈中运行。
- 主线程不断重复第三步,也就是只要主线程空了,就会去读取任务队列,该过程不断重复,这就是所谓的事件循环。
宏任务和微任务
异步任务分为
宏任务(macrotask)
与微任务(microtask)
。宏任务会进入一个队列,而微任务会进入到另外一个不同的队列,且微任务要优于宏任务执行。
常见的宏任务和微任务
- 宏任务:script(整体代码)、setTimeout、setInterval、I/O、事件、postMessage、MessageChannel、setImmediate(Node.js)。
- 微任务:Promise.then、MutaionObserver、process.nextTick(Node.js)
总结: 任务队列中的任务分为宏任务和微任务,当执行栈清空后,会先检查任务队列中是否有微任务,如果有就按照先进先出的原则,压入执行栈中执行。微任务中产生了新的微任务不会推迟到下一循环中,而是在当前循环中继续执行。当执行这一轮的微任务完毕后,开启下一轮循环,执行任务队列中的宏任务。
执行顺序
- 执行宏任务中的同步代码,遇到宏任务或微任务,分别放入对应的任务队列,等待执行。
- 当所有同步任务执行完毕后,执行栈为空,
首先执行微任务队列中的任务
。 - 微任务执行完毕后,检查这次执行中是否产生新的微任务,如果存在,重复执行步骤,直到微任务执行完毕。
- 开始下一轮
Event Loop
,执行宏任务中的代码。
Event Loop遇到async/await
async/await仅仅是生成器的语法糖,所以只要把它转换成Promise的形式即可。
js
下面这段代码是 async/await 函数的经典形式。
async function foo() {
// await 前面的代码
await bar();
// await 后面的代码
}
async function bar() {
// do something...
}
foo();
其中 await 前面的代码是同步的,调用此函数时会直接执行;
而 await bar()--> 这句可以被转换成 Promise.resolve(bar());
await 后面的代码则会被放到 Promise 的 then() 方法里。
因此上面的代码可以被转换成如下形式,这样是不是就很清晰了?
function foo() {
// await 前面的代码
Promise.resolve(bar()).then(() => {
// await 后面的代码
});
}
function bar() {
// do something...
}
foo();
js
setTimeout(() => {
console.log('timeout'); // 5. 宏任务2
}, 0)
new Promise(resolve => {
console.log('new Promise'); // 1. 属于同步进入主线程 宏任务1
resolve();
}).then(() => {
console.log('Promise then'); // 3. 微任务 1
}).then(() => {
console.log('Promise then then'); // 4. 微任务 2
})
console.log('hi'); // 2. 同步 + 宏任务1
js
async function async1() {
console.log('async1 start');//2
await async2();
console.log('async1 end');//6
}
async function async2() {
console.log('async2');//3
}
console.log('script start'); //1
setTimeout(function() {
console.log('setTimeout');//8
}, 0);
async1();
new Promise(function(resolve) {
console.log('promise1'); //4
resolve();
}).then(function() {
console.log('promise2');//7
});
console.log('script end');//5
Promise
promise的理解
Promise是异步编程的一种解决方案,将异步操作以同步操作的流程表达出来,避免了地狱回调。
Promise实例对象的三种状态
- Pending(初始状态)
- Fulfilled(成功状态)
- Rejected(失败状态)
Promise的实例有两个过程
- pending------>fulfilled:Resolved(已完成)
- pending------>rejected:Rejected(已拒绝)
- 注意:一旦从进行状态变成为其他状态就永远不能更改状态了,其过程是不可逆的。
Promise构造函数接收一个带有resolve
和reject
参数的回调函数
- resolve的作用是将Promise状态从pending变为fulfilled,在异步操作成功时调用,并将异步结果返回,作为参数传递出去
- reject的作用是将Promise状态从pending变为rejected,在异步操作失败后,将异步操作错误的结果,作为参数传递出去
Promise的缺点
- 无法取消Promise,一旦新建它就会立即执行,无法中途取消。
- 如果不设置回调函数,Promise内部抛出错误,不会反应到外部。
- 当处于pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。
Promise方法
Promise.then()
对应resolve
成功的处理Promise.catch()
对应reject
失败的处理Promise.all()
可以完成并行任务,将多个Promise
实例数组,包装成一个新的Promise
实例,返回的实例就是普通的Promise
。有一个失败,代表Promise失败,当所有的子Promise
完成,返回值是全部值得数组Promise.race()
类似于Promise.all()
,区别在于由第一个改变状态的promise对象决定最终返回的结果Promise.allSettled()
返回一个在所有给定的Promise
都已经fulfilled
或rejected
,并带有一个对象数组,每个对象都表示对应的Promise
结果。
async/await的理解
async/await
其实是Generator
的语法糖,它能够实现的效果都能用then
链来实现,它是为优化then链而开发来的。通过
async
关键字声明一个异步函数,await
用于等待一个异步方法执行完成,并且会阻塞执行。
async
函数返回的是一个Promise
对象,如果在函数中return
一个变量,async
会把这个直接量通过Promise.resolve()
封装成Promise
对象,如果没有返回值,返回Promise.resolve(undefined)
async/await对比Promise的优势
- 代码可读性高,Promise虽然摆脱了回调地狱,但自身的链式调用会影响可读性
- 相对promise更优雅,传值更方便
- 对错误处理友好,可以通过try/catch捕获,promise的错误捕获非常冗余