promise规范及应用

学习目标:

  • 了解进程与线程
  • 浏览器原理
  • 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中的内存分为堆内存栈内存

  1. JS中引用类型值的大小是不固定的,因此它们会被存储到堆内存中,由系统自动分配存储空间。JS不允许直接访问堆内存中的位置,因此不能直接操作对象的堆内存空间,而是操作对象的引用。
  2. 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构造函数接收一个带有resolvereject参数的回调函数

  • 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都已经fulfilledrejected,并带有一个对象数组,每个对象都表示对应的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的错误捕获非常冗余
相关推荐
qq_390161777 分钟前
防抖函数--应用场景及示例
前端·javascript
3345543235 分钟前
element动态表头合并表格
开发语言·javascript·ecmascript
John.liu_Test36 分钟前
js下载excel示例demo
前端·javascript·excel
Yaml41 小时前
智能化健身房管理:Spring Boot与Vue的创新解决方案
前端·spring boot·后端·mysql·vue·健身房管理
PleaSure乐事1 小时前
【React.js】AntDesignPro左侧菜单栏栏目名称不显示的解决方案
前端·javascript·react.js·前端框架·webstorm·antdesignpro
哟哟耶耶1 小时前
js-将JavaScript对象或值转换为JSON字符串 JSON.stringify(this.SelectDataListCourse)
前端·javascript·json
getaxiosluo1 小时前
react jsx基本语法,脚手架,父子传参,refs等详解
前端·vue.js·react.js·前端框架·hook·jsx
理想不理想v1 小时前
vue种ref跟reactive的区别?
前端·javascript·vue.js·webpack·前端框架·node.js·ecmascript
知孤云出岫1 小时前
web 渗透学习指南——初学者防入狱篇
前端·网络安全·渗透·web
贩卖纯净水.1 小时前
Chrome调试工具(查看CSS属性)
前端·chrome