补充Promise微任务注册自我小结和Promise.finally()注意点
Promise

基础入门知识

文章目录

  • Promise出现的原因
    • 回调地狱(Callback Hell)
      • 什么是回调函数
      • 回调函数的缺点
      • 如何解决回调函数
      • 参考资料
      • 复习完再问
  • 什么是Promise
    • 代码书写比较
    • Promise 的特点
    • Promise的常用API
      • Promise.prototype.then
      • Promise.prototype.catch()
      • Promise.prototype.finally()
      • Promise.resolve(value)
      • Promise.reject(reason)
      • Promise.all()
      • Promise.race()
  • Promise的手写
  • 事件循环
      • 执行上下文和执行栈
        • 执行上下文
          • 是什么
          • 执行上下文的类型
          • 如何创建
            • 1.this绑定
            • 2.词法环境
            • 3.变量环境
          • 习题巩固
            • 题目一
            • 题目二
            • 题目三
        • 执行栈
    • 宏任务与微任务
    • EventLoop执行顺序
    • Promise中的微任务注册
      • Promise的执行顺序
      • Promise微任务注册小结
      • 重点
      • 注意

Promise出现的原因

Promise 是异步编程的一种解决方案,解决了回调地狱的问题,那么什么是回调地狱(Callback Hell)?

回调地狱(Callback Hell)

对于回调地狱,先来讲讲回调函数

什么是回调函数

MDN 的描述:回调函数是作为参数传给另一个函数的函数,然后通过在外部函数内部调用该回调函数以完成某种操作

个人理解:回调函数就是一个函数,作为另一个函数fn的参数,fn执行时会立即执行该回调函数。利用这个关系,可以在回调函数内进行处理数据等其他操作。也就是说必须触发了fn函数,才能触发回调函数,才能完成里面的一些操作。

举例了解:

const btnAdd = document.getElementById('btnAdd');btnAdd.addEventListener('click', function clickCallback(e) {// do something useless
});

必须触发了点击事件,才能执行回调函数clickCallback,向某些数据或事件添加一些功能。

回调函数的作用/出现的原因:由于javascript是单线程的,在有些需要等待的地方,需要使用回调函数。

回调函数的缺点

回调函数最大的缺点就是容易写出回调地狱

当需要发送多个异步请求,且每个请求之间需要相互依赖时,我们只能以嵌套的方式来解决:

请求1(function(请求结果1){请求2(function(请求结果2){请求3(function(请求结果3){请求4(function(请求结果4){请求5(function(请求结果5){请求6(function(请求结果3){...})})})})})
})

这就产生了回调地狱,回调地狱的主要问题

  • 代码不断缩进,可能超出编辑区,不方便维护,可维护性差

  • 可读性差

  • 只能在回调函数内处理异常

  • 容易滋生 bug

  • 代码复用性差

如何解决回调函数

es6中提出了Promise和async/await来解决回调地狱的问题。

参考资料

https://juejin.cn/post/6844903811316711437

https://juejin.cn/post/6844903625609707534

复习完再问

  1. 回答回调函数是啥?
  2. 为什么会有回调地狱?
  3. 我该如何解决?
  4. 扩展你的解决办法。

什么是Promise

Promise的出现,就是为了解决回调函数所引出的各种问题

代码书写比较

还是使用上面的网络请求例子,我们看下 Promise 的常规写法:

new Promise(请求1).then(请求2(请求结果1)).then(请求3(请求结果2)).then(请求4(请求结果3)).then(请求5(请求结果4)).catch(处理异常(异常信息))

比较一下这种写法和上面的回调式的写法。很明显Promise 的写法更为直观,并且能够在外层捕获异步函数的异常信息。

Promise 的特点

一个 Promise有三种状态:

  • pending: 进行中。
  • fulfilled: 已成功。
  • rejected: 已失败。

两个特点

  • 对象的状态不受外界影响,只有异步操作的结果,可以决定当前是哪一种状态

  • 一旦状态改变,就不会再变,任何时候都可以得到这个结果。只有两种状态改变:从pending变为fulfilled和从pending变为rejected。一旦状态变为 fulfilled/rejected 后,就不能再次改变

Promise的常用API

Promise.prototype.then

实例方法,then方法定义在原型对象Promise.prototype上。

作用:为Promise 实例添加状态改变时的回调函数。

参数:第一个参数是resolved状态的回调函数,第二个参数是rejected状态的回调函数,都可选

链式的then:前一个then必须return返回一个值或一个Promise对象让下一个then回调接收,最后一个then的话则不需要;下一个then必须等到上一个then回调结束Promise对象的状态发生变化,才会被调用。也就是存在前后依赖关系。

Promise.prototype.catch()

实例方法。

作用:一般放在最后用于捕获异常。

注意点:

  1. then()方法指定的回调函数运行中抛出throw的错误,也会被捕获。
  2. Promise 在resolve语句后面,再抛出错误,不会被捕获。因为 Promise 的状态一旦改变,就不会再变了。
const promise = new Promise(function(resolve, reject) {resolve('ok');throw new Error('test');
});
promise.then(function(value) { console.log(value) }).catch(function(error) { console.log(error) });
// ok
  1. Promise 对象的错误具有“冒泡”性质,会一直向后传递,直到被捕获为止。也就是说,可以捕获前面产生所有的错误。
getJSON('/post/1.json').then(function(post) {return getJSON(post.commentURL);
}).then(function(comments) {// some code
}).catch(function(error) {// 处理前面三个Promise产生的错误
});
  1. 如果catch()方法返回的还是一个 Promise 对象,后面还可以接着调用then()方法。
  2. 如果catch()方法后面的then()报错,则与前面的catch()无关。
  3. catch()方法之中,还能再抛出错误

Promise.prototype.finally()

实例方法

作用:无论Promise是什么状态,都会执行里面的回调函数,本质上是then方法的特例。类似try…catch…finally

注意点:

1.finally()的回调函数不接受任何参数,就算有输出也是undefined

2.finally()的返回值如果在没有抛出错误的情况下,默认是上一个Promise的返回值

Promise.resolve(value)

类方法

作用:根据不同的value返回不同的Promise对象

value的四种情况:

  1. 是一个 Promise 对象

不做任何修改、原封不动地返回这个对象,即返回原对象

  1. 是一个thenable对象

thenable对象指的是具有then方法的对象,会将该对象转为 Promise 对象,然后就立即执行thenable对象的then()方法,即返回

如 jQuery.ajax的返回值就是 thenable 对象

举例:典型thenable对象——jQuery.ajax的返回值是 thenable 对象

let promise = Promise.resolve($.ajax('/test/test.json'));// => promise对象
promise.then(function(value){console.log(value);
});
  1. 是一个原始值,除1、2情况的参数

返回一个为resolved状态的新 Promise 对象。

举例:

let p = Promise.resolve('Hello');p.then(function (s) {console.log(s)  // Hello
});
  1. 没有任何参数

直接返回一个resolved状态的 Promise 对象。

Promise.reject(reason)

与 resolve 类似,唯一的不同是返回的 promise 对象的状态为 rejected。

Promise.all()

作用:并行执行多个 Promise 任务。往往参数是由多个 Promise 任务组成的数组[p1,p2,p3]

返回值:如果全部成功执行,则以数组的方式返回所有 Promise 任务的执行结果;只要有一个 Promise 任务执行失败,则只返回 rejected 任务的结果。

Promise.race()

与all方法类似,但返回的是最先执行结束的 Promise 任务的结果,无论成功还是失败状态。顾名思义,race 赛跑,哪个任务先执行结束,就返回哪个任务的结果。

Promise的手写

看链接文章,写的很详细

https://juejin.cn/post/6945319439772434469

事件循环

先牢记一点:JS 是一门单线程语言,在执行过程中永远只能同时执行一个任务,任何异步的调用都只是在模拟这个过程,或者说可以直接认为在 JS 中的异步就是延迟执行的同步代码。

执行 JS 代码就是往执行栈里 push 函数,那么遇到异步代码会这么做呢?

先补充下知识点:js执行上下文和执行栈及宏任务与微任务

执行上下文和执行栈

参考资料:

https://juejin.cn/post/6844903682283143181

https://segmentfault.com/a/1190000017350739

执行上下文

是什么

执行上下文(Execution context 简称EC,也称为执行环境)是JavaScript代码在被解析和运行时环境的抽象概念。JavaScript 任何代码的运行是在执行上下文环境中运行的。

执行上下文的类型

JavaScript 中有三种执行上下文类型。

  • 全局执行上下文 — 默认的,任何不在函数内部的代码都在全局上下文中。它会执行两件事:创建一个全局的 window 对象(浏览器的情况下),设置 this 的值等于这个全局对象即设置this=window。一个程序中只会有一个全局执行上下文。
  • 函数执行上下文 — 每当函数被调用时, 会为该函数创建一个新的上下文,即每个函数都有它自己的执行上下文。
  • Eval 函数执行上下文 — 执行 eval 函数内部的代码时会产生自己的上下文,由于开发不常用 eval,就不讨论了
如何创建

执行上下文包括三个阶段:创建——执行——销毁,主要讨论创建阶段。

创建环节会执行三件事情:

1.this绑定

在全局执行上下文中,this 指向全局对象(浏览器中,this指向Window )。

在函数执行上下文中,谁调用this 指向谁。如果调用者是一个引用对象如obj对象,则指向该对象,否则 this 指向全局对象(window,非严格模式)或者 undefined(严格模式下函数的this)

let foo = {baz: function() {console.log(this);}
}foo.baz();   // 'this' 指向 'foo', 因为 'baz' 被对象 'foo' 调用let bar = foo.baz;
bar();       // 'this' 指向全局 window 对象,因为没有指定引用对象
2.词法环境

1)是什么

词法环境是一种持有标识符(函数名、变量名)—变量(对实际对象或原始数据的引用)映射的结构。

2)结构

内部由两个组件组成:环境记录器外部环境的引用

环境记录器

  • 全局环境中,环境记录器是对象环境记录器,定义出现在全局上下文中的变量和函数的关系。

  • 函数环境中,环境记录器是声明式环境记录器,存储变量、函数(包括arguments 对象和参数的长度length)和参数。

外部环境的引用

  • 可以访问其父级词法环境(作用域)
3.变量环境

也是一个词法环境,其环境记录器持有变量声明语句在执行上下文中创建的绑定关系。

与词法环境类似,不同点是词法环境用来存储函数声明和变量(letconst)绑定,而变量环境只用来存储 var 变量绑定

也就是说在创建阶段时,var 变量最初设置为 undefinedletconst 变量最初设置为未初始化< uninitialized >。也就是变量提升。

小结变量声明

1.函数声明的是整个函数体( 因为函数声明不存在赋值操作),而且优先级高于同名的变量,会跳过该变量的声明

2.变量提升并非物理意义上的顺序改变,只是变量声明发生在执行上下文的创建阶段,而变量赋值却发生在执行阶段。

习题巩固
题目一
var foo = 1;
function bar () {console.log(foo);var foo = 10;console.log(foo);
}bar();

bar函数内部变量提升,根据作用域链规则,因此输出 undefined 10

题目二
var foo = 1;
function bar () {console.log(foo);foo = 2;
}
bar();
console.log(foo);

bar函数内的foo在函数作用域内没有定义,因此操作的是外部foo的全局变量,输出1 2

题目三
var foo = 1;
function bar (foo) {console.log(foo);foo = 234;
}
bar(123);
console.log(foo);

运行 bar 函数的时候将 123 数字作为实参传入,操作的是函数作用域内的 foo,输出123 1

执行栈

1.是什么

执行栈,也就是数据结构的栈,具有先进先出的特点。

2.作用

存储代码运行时创建的所有执行上下文。

3.怎么做

当JavaScript引擎第一次遇到 JS 代码时,会产生一个全局执行上下文并压入执行栈。

每遇到一个函数调用,就会创建一个新的函数执行上下文并压入栈。

引擎执行栈顶的函数,执行完毕,弹出当前执行上下文。控制流程到达当前栈中的下一个上下文。

全局执行上下文在浏览器关闭时出栈。

举例说明:

function foo() {console.log('1');bar();console.log('3');
}function bar() {console.log('2');
}foo();

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IyKCWMXz-1620485338434)(C:\Users\Daii\AppData\Roaming\Typora\typora-user-images\image-20210508164044693.png)]

首先代码在浏览器中加载时,JavaScript 引擎创建了一个全局执行上下文(Global EC)并把它压入当前执行栈。

当 foo() 函数被调用,创建 foo 函数的执行上下文并压入执行栈,接着执行输出 ‘1’;接着 bar() 函数被调用,创建 bar 函数的执行上下文压入执行栈,接着执行输出 ‘2’,bar() 执行完毕,被弹出执行栈;foo() 函数接着执行,输出 ‘3’。

foo() 函数执行完毕,被弹出执行栈。一旦所有代码执行完毕,JavaScript 引擎从当前栈中移除全局执行上下文。

4.习题巩固

var count = 0;
function foo(count) {count += 1;console.log(count);
}
foo(count);
foo(count);

用执行栈来理解,第一个foo函数被调用,产生新的执行上下文并被压入执行栈,执行完毕后就会被弹出执行栈;第二个foo函数被调用,产生新的执行上下文并被压入执行栈,执行完毕后就会被弹出执行栈。

每个foo函数的执行上下文里的count是传入的形参count=0,因此每次调用结果都为1。

var count = 0;
function foo() {count += 1;console.log(count);
}
foo(count);
foo(count);

与上面不同的是foo函数没有指定参数,那么每次调用foo函数产生的执行上下文里的count根据作用域链找到的是全局变量var声明的count,因此结果分别为1 2。

  • 函数执行上下文注意点

函数的形参也属于函数执行上下文,如果指定了形参,它会随着函数被调用而新建,随着函数销毁而销毁。如果没有指定形参,会沿着作用域链找到全局变量 count。

宏任务与微任务

Js 有两种任务的执行模式:同步模式(Synchronous)和异步模式(Asynchronous)

异步任务主要分为宏任务与微任务两种。

1.是什么

ES6 规范中,宏任务(Macrotask)由宿主(浏览器、Node)发起,被称为Task。

微任务(Microtask)由 JS 自身发起,被称为 Jobs。

2.创建方式

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kFg4PaRj-1620485338441)(C:\Users\Daii\AppData\Roaming\Typora\typora-user-images\image-20210508212538637.png)]

Promise 中只有涉及到状态变更后才需要被执行的回调才算是微任务,比如说 thencatchfinally ,其他所有的代码执行都是同步执行。

EventLoop执行顺序

Event Loop 执行顺序如下:

  • 一开始整个脚本script作为一个宏任务执行

  • 执行过程中同步代码直接执行,遇到宏任务放入宏任务队列,遇到微任务放入微任务队列

  • 执行完所有同步代码后且执行栈为空,判断是否有微任务需要执行

  • 有则执行最早进入队列的任务之后继续检查微任务队列空不空,即依次执行所有的微任务,直到全部执行完

  • 执行浏览器UI线程的渲染工作

  • 执行完本轮的宏任务,执行下一轮宏任务(第二步),依此循环,直到宏任务和微任务队列都为空。

Promise中的微任务注册

具体看:
https://juejin.cn/post/6844903987183894535
https://juejin.cn/post/6869573288478113799
看完后的习题巩固:
Promise习题
补充自我小结:

Promise的执行顺序

  • Promise 新建后就会立即执行,创建方式有两种,都会返回一个 Promise 对象。

    • new Promise(fn)
    • Promise.resolve(fn)
  • 调用resolvereject不会终结 Promise 的参数函数的执行

Promise微任务注册小结

1.链式调用中微任务的注册是前后依赖的

then中的回调有return,后一个then的注册需要等待前一个then的return之后的结果

  • return一个error对象不会抛出错误,不会被后续的 .catch 捕获。
  • return不能是promise 本身,会死循环。
  • return一个新Promise对象(不妨命名为promise2),
    • 如果是 return new Promise(fn)会生成一个新微任务,内容是调用 promise3.then(resolvePromise2, rejectPromise2),将 promise2和 promise3相关联,调用then得到promise2的状态。
    • 如果是return Promise.resolve(fn)会产生两个微任务,得到状态为fulfilled,因此第二个微任务内容是resolvePromise2(4)
    • 具体可阅读:https://juejin.cn/post/6950093219153575972

then中的回调没有return,后一个then的注册需要等待前一个then的同步代码执行完成(即直至遇到下一个异步任务)

2.如果是变量定义的方式调用then,则进行同步注册,此时并不是链式调用,如p.thenvar p = new Promise都是同步执行的。

3.Promise resolve 后,跟着的 then 中的回调会马上进入微任务队列Promise.resolve().then(()

重点

弄清then的回调进入队列的时机,注册的不是then方法而是里面的回调。

  • then是同步执行的,但里面的回调是异步执行的,
  • 回调是否在那一刻进入队列,取决于前面的promise的状态是否更改,如果是pending状态,那么回调不会进入队列,等待Promise对象状态改变后才进入队列。也就是说,不是每次遇到 then 时都需要把它的回调丢入微任务队列中,而是等待 then 的回调执行完毕后再根据情况执行对应操作。

注意

打印promise对象是打印promise的状态及状态进行回调的值,对象的键值对形式,如Promise{<resolved>: 'resolve1'}

看完后再阅读https://juejin.cn/post/6945319439772434469思路会更清晰

[js基础篇]Promise是什么和EventLoop相关推荐

  1. cytoscape.js基础篇

    cytoscape.js基础篇 cytoscape.js 包引用 版本信息 Citation Funding 基础篇 cytoscape.js变量描述 位置 Elements JSON 节点属性说明 ...

  2. JS基础篇--HTML DOM classList 属性

    页面DOM里的每个节点上都有一个classList对象,程序员可以使用里面的方法新增.删除.修改节点上的CSS类.使用classList,程序员还可以用它来判断某个节点是否被赋予了某个CSS类. 添加 ...

  3. ie9无法获取未定义或 null 引用的属性“indexof”_前端JS基础篇(二)JS基本数据类型和引用数据类型及检测数据类型方法...

    JS中的数据类型 (一).基本数据类型(值类型) 1.number:数字 -12.12.5.-12.5 0这些数字都是number: js中增加了一个number类型的数据:'NaN' typeof ...

  4. 前端面试题汇总(JS 基础篇)

    前端面试题汇总(JS 基础篇)** 1.javascript 的 typeof 返回哪些数据类型** object number function boolean underfind stringty ...

  5. (一)JS 基础篇—基础知识总结

    ⛺️ 欢迎大家拜访我的:个人博客 ⛽️ 前端加油站之[JavaScript]⛽️ 内容 地址 (一)JS 基础篇-基础知识总结 ⛳️ [快来点点我 ~] (二)JS 基础篇-函数与作用域 ⛳️ [快来 ...

  6. 前端面试题目汇总摘录(JS 基础篇 —— 2018.11.01更新)

    温故而知新,保持空杯心态 JS 基础 JavaScript 的 typeof 返回那些数据类型 object number function boolean undefined string type ...

  7. 前端面试题目汇总摘录(JS 基础篇)

    温故而知新,保持空杯心态 JS 基础 JavaScript 的 typeof 返回那些数据类型 object number function boolean undefined string type ...

  8. 前端工程化----Node.js基础篇

    文章目录 1.认识Node.js Node.js是什么 Node.js应用场景 2.Node.js安装和版本管理 Node.js安装 Node.js版本工具 3.Node.js执行文件 4.Node. ...

  9. 前端学习笔记(CSS、JS基础篇)

    CSS篇 注意:css注释使用/ /,而不是<!-- -->或者//,否则很容易导致不明错误!!! div padding:内边距.盒子内容与盒子边框的距离设置,相当于给盒子加了厚度,使用 ...

最新文章

  1. 说一说MVC的CompressActionFilterAttrubute(五)
  2. Leetcode每日必刷题库第5题,如何实现最长回文子串?
  3. 随手记:IDAPro蛮强大
  4. 《江南百景图》游戏设计小思考:留边占角“小烦恼”
  5. this.$nextTick()的使用场景
  6. [mysql] linux下使用yum安装mysql
  7. 是Excel的图,不!是R的图
  8. python怎么另起一行阅读答案_使用Python+Dlib构建人脸识别系统(在Nvidia Jetson Nano 2GB开发板上)...
  9. SQL Server 中 GO 的用法
  10. 【hdu 6396】Swordsman
  11. shell - 查看天气
  12. python魅力_Python逐渐失去魅力
  13. 启动virt-manager报错:Failed to open /var/lib/dbus/machine-id
  14. 360度测试:KAFKA会丢数据么?其高可用是否满足需求?
  15. cad计算机绘图等级考试,国家CAD等级考试介绍完整版.doc
  16. ubantu上adb调试fastboot下载
  17. [hdu6148][Valley Numer]
  18. 虚拟串口工具VSPD简单使用
  19. shell-定时备份数据库发送至邮箱
  20. 电气控制电路图——(3)设计

热门文章

  1. 电子采购订单与手动采购订单有什么区别?
  2. iframe使用总结:
  3. python 实现文章中词汇的频率统计并进行显示(针对英文文章)
  4. Qt 文件按时间先后排序 删除
  5. 网卡丢包,rx_missed_errors 大于0,是什么问题导致的?
  6. 安装super-gradients包时遇到的一些错误
  7. 操作系统CPU调度算法
  8. 安装Django教程
  9. LoRaWAN介绍8 QoS
  10. 学习app.config连接数据库的配置