[js基础篇]Promise是什么和EventLoop
补充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
复习完再问
- 回答回调函数是啥?
- 为什么会有回调地狱?
- 我该如何解决?
- 扩展你的解决办法。
什么是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()
实例方法。
作用:一般放在最后用于捕获异常。
注意点:
- then()方法指定的回调函数运行中抛出throw的错误,也会被捕获。
- 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
- Promise 对象的错误具有“冒泡”性质,会一直向后传递,直到被捕获为止。也就是说,可以捕获前面产生所有的错误。
getJSON('/post/1.json').then(function(post) {return getJSON(post.commentURL);
}).then(function(comments) {// some code
}).catch(function(error) {// 处理前面三个Promise产生的错误
});
- 如果
catch()
方法返回的还是一个 Promise 对象,后面还可以接着调用then()
方法。 - 如果
catch()
方法后面的then()
报错,则与前面的catch()
无关。 catch()
方法之中,还能再抛出错误
Promise.prototype.finally()
实例方法
作用:无论Promise是什么状态,都会执行里面的回调函数,本质上是then方法的特例。类似try…catch…finally
注意点:
1.finally()的回调函数不接受任何参数,就算有输出也是undefined
2.finally()的返回值如果在没有抛出错误的情况下,默认是上一个Promise的返回值
Promise.resolve(value)
类方法
作用:根据不同的value返回不同的Promise对象
value的四种情况:
- 是一个 Promise 对象
不做任何修改、原封不动地返回这个对象,即返回原对象
- 是一个
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、2情况的参数
返回一个为
resolved
状态的新 Promise 对象。
举例:
let p = Promise.resolve('Hello');p.then(function (s) {console.log(s) // Hello
});
- 没有任何参数
直接返回一个
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.变量环境
也是一个词法环境,其环境记录器持有变量声明语句在执行上下文中创建的绑定关系。
与词法环境类似,不同点是词法环境用来存储函数声明和变量(let
和 const
)绑定,而变量环境只用来存储 var
变量绑定。
也就是说在创建阶段时,var
变量最初设置为 undefined
,let
和 const
变量最初设置为未初始化< 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 中只有涉及到状态变更后才需要被执行的回调才算是微任务,比如说 then
、 catch
、finally
,其他所有的代码执行都是同步执行。
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)
- 调用
resolve
或reject
并不会终结 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.then
和 var 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相关推荐
- cytoscape.js基础篇
cytoscape.js基础篇 cytoscape.js 包引用 版本信息 Citation Funding 基础篇 cytoscape.js变量描述 位置 Elements JSON 节点属性说明 ...
- JS基础篇--HTML DOM classList 属性
页面DOM里的每个节点上都有一个classList对象,程序员可以使用里面的方法新增.删除.修改节点上的CSS类.使用classList,程序员还可以用它来判断某个节点是否被赋予了某个CSS类. 添加 ...
- ie9无法获取未定义或 null 引用的属性“indexof”_前端JS基础篇(二)JS基本数据类型和引用数据类型及检测数据类型方法...
JS中的数据类型 (一).基本数据类型(值类型) 1.number:数字 -12.12.5.-12.5 0这些数字都是number: js中增加了一个number类型的数据:'NaN' typeof ...
- 前端面试题汇总(JS 基础篇)
前端面试题汇总(JS 基础篇)** 1.javascript 的 typeof 返回哪些数据类型** object number function boolean underfind stringty ...
- (一)JS 基础篇—基础知识总结
⛺️ 欢迎大家拜访我的:个人博客 ⛽️ 前端加油站之[JavaScript]⛽️ 内容 地址 (一)JS 基础篇-基础知识总结 ⛳️ [快来点点我 ~] (二)JS 基础篇-函数与作用域 ⛳️ [快来 ...
- 前端面试题目汇总摘录(JS 基础篇 —— 2018.11.01更新)
温故而知新,保持空杯心态 JS 基础 JavaScript 的 typeof 返回那些数据类型 object number function boolean undefined string type ...
- 前端面试题目汇总摘录(JS 基础篇)
温故而知新,保持空杯心态 JS 基础 JavaScript 的 typeof 返回那些数据类型 object number function boolean undefined string type ...
- 前端工程化----Node.js基础篇
文章目录 1.认识Node.js Node.js是什么 Node.js应用场景 2.Node.js安装和版本管理 Node.js安装 Node.js版本工具 3.Node.js执行文件 4.Node. ...
- 前端学习笔记(CSS、JS基础篇)
CSS篇 注意:css注释使用/ /,而不是<!-- -->或者//,否则很容易导致不明错误!!! div padding:内边距.盒子内容与盒子边框的距离设置,相当于给盒子加了厚度,使用 ...
最新文章
- 说一说MVC的CompressActionFilterAttrubute(五)
- Leetcode每日必刷题库第5题,如何实现最长回文子串?
- 随手记:IDAPro蛮强大
- 《江南百景图》游戏设计小思考:留边占角“小烦恼”
- this.$nextTick()的使用场景
- [mysql] linux下使用yum安装mysql
- 是Excel的图,不!是R的图
- python怎么另起一行阅读答案_使用Python+Dlib构建人脸识别系统(在Nvidia Jetson Nano 2GB开发板上)...
- SQL Server 中 GO 的用法
- 【hdu 6396】Swordsman
- shell - 查看天气
- python魅力_Python逐渐失去魅力
- 启动virt-manager报错:Failed to open /var/lib/dbus/machine-id
- 360度测试:KAFKA会丢数据么?其高可用是否满足需求?
- cad计算机绘图等级考试,国家CAD等级考试介绍完整版.doc
- ubantu上adb调试fastboot下载
- [hdu6148][Valley Numer]
- 虚拟串口工具VSPD简单使用
- shell-定时备份数据库发送至邮箱
- 电气控制电路图——(3)设计