预备知识
区别函数对象与实例对象
实例对象:new 函数产生的对象,称为实例对象,简称对象
函数对象:将函数作为对象使用时,简称函数对象
1 | function Fn() {} // Fn函数 |
两种类型的回调函数
同步回调
立即执行,执行完后才结束,不会放入回调队列中
例如,数组遍历相关的回调函数、Promise 的 excutor 函数
1 | const arr = [0, 3, 4] |
上面的运行结果表明,在数组的 forEach
遍历完后,才执行 console.log('forEach之后')
,说明数组的 forEach
的回调函数是同步回调。
异步回调
不会立即执行,会放入回调队列中将来执行
例如,定时器回调、ajax回调、Promise的成功/失败回调等
1 | setTimeout(() => { |
上面的结果表明,先执行完同步代码后,再执行定时器的回调函数,说明定时器的回调函数是异步回调
error
常见内置错误
SyntaxError
语法错误
1 | // 变量名错误 |
ReferenceError
引用的变量不存在
1 | // 使用一个不存在的变量 |
另一种触发场景是,将一个值分配给无法分配的对象,比如对函数的运行结果或者 this
赋值。
1 | // 等号左侧不是变量 |
上面代码对函数 console.log
的运行结果和 this
赋值,结果都引发了 ReferenceError
错误。
RangeError
数据值不在其所允许的范围内
RangeError
对象是一个值超出有效范围时发生的错误。主要有几种情况,一是数组长度为负数,二是 Number
对象的方法参数超出范围,以及函数堆栈超过最大值。
1 | // 数组长度不得为负数 |
TypeError
数据类型不正确
TypeError
对象是变量或参数不是预期类型时发生的错误。比如,对字符串、布尔值、数值等原始类型的值使用 new
命令,就会抛出这种错误,因为 new
命令的参数应该是一个构造函数。
1 | new 123 |
上面代码的第二种情况,调用对象不存在的方法,也会抛出 TypeError
错误,因为 obj.unknownMethod
的值是 undefined
,而不是一个函数。第三种情况抛出 TypeError
错误,因为 b
的值是 undefined
,而 undefined
没有 xx
的属性。
错误处理
捕获错误 try ... catch
1 | try { |
抛出错误 throw error
1 | // 抛出错误 |
Promise
Promise 的基本使用
-
Promise 构造函数:Promise(excutor) {}
- excutor 函数:执行器(resolve, reject)=> {}
- resolve 函数:内部定义成功时我们调用的函数 value => {}
- reject 函数:内部定义失败时我们调用的函数 reason => {}
说明:excutor 会在 Promise 内部立即同步回调,异步操作在执行器中执行
-
Promise.prototype.then 方法:(onResolved, onRejected) => {}
onResolved 函数:成功的回调 (value) => {}
onRejected 函数:失败的回调函数 (reason) => {}
指定用于得到成功 value 的成功回调和用于得到 reason 的失败回调返回一个新的 Promise 对象
-
Promise.prototype.catch 方法:(onRejected) => {}
onRejected 函数:失败的回调函数 (reason) => {}
then() 的语法糖,相当于:then(undefined, onRejected)、then(null, onRejected)
-
Promise.resolve 方法:(value) => {}
value: 成功的数据或 promise 对象
返回一个成功 / 失败的 promise 对象
-
Promise.reject方法:(reason) => {}
reason: 失败的原因
返回一个 失败的 promise 对象
-
Promise.all 方法:(promisees) => {}
promises:包含 n 个promise 的数组
返回一个新的 promise,只要所有的 promise 都成功才成,只要有一个失败了就直接失败
-
Promise.race 方法:(promises) => {}
promises:包含 n 个 promise 的数组
返回一个新的 promise,第一个完成的 promise 的结果状态就是最终的结果状态
详见阮大大的 Promise 对象 或者 MDN 的
Promise
Promise 与纯回调函数的对比
-
指定回调函数的方式更加灵活
纯回调函数:必须在启动异步任务前指定回调函数
Promise:启动异步任务 =》返回 Promise 对象 =》给Promise对象绑定回调函数(甚至可以在异步任务结束后指定)
1 | // 成功的回调函数 |
由上面的例子可知,第一种是用纯回调函数的方法,在给 createFileAsync
异步任务绑定成功/失败的回调函数时,须通过传参的方式传给该异步任务,即 必须在在启动异步任务前指定回调函数
,而第二种的 Promise 的方法则不需要这样,他可以通过 createFileAsync
启动异步任务后返回一个 Promise 对象,然后根据 Promise 对象的状态,通过 .then
或者 .catch
的方式指定成功或者失败的回调函数
-
支持链式调用,可以解决回调地狱的问题
什么是回调地狱:回调地狱嵌套调用,外部回调函数异步执行的结果是嵌套的回调函数执行的条件
回调地狱的缺点:不便于阅读、不便于异常处理
解决方案:Promise 链式调用
终极解决方案:async / await
回调地狱
1 | doSomething(function(result) { |
使用 Promise 的链式调用解决回调地狱
1 | // 这里的 doSomething 函数返回的是 Promise 对象 |
async / await
1 | async function request() { |
Promise 的特性
-
如何改变 Promise 的状态
- resolve(value):如果当前是 pending 就会变成 fullfilled 状态(也可说是 resolved 状态)
- reject(reason):如果当前是 pending 就会变成 rejected 状态
- 抛出异常:如果当前是 pending 就会变成 rejected
1 | const p = new Promise((resolve, reject) => { |
-
一个 promise 指定多个成功/失败回调函数,都会调用吗?
- 当promise 改变为对应状态时都会调用
1 | // 还是上面的Promise对象 p |
上面例子中的 p 绑定了两次的回调函数,并且都调用了。
-
改变 promise 状态和指定回调函数谁先谁后?
-
都有可能,正常情况下是先指定回调函数再改变状态,但也可以先改变状态再指定回调
-
如何先改变状态再指定回调?
- 在执行器中直接调用 resolve() / reject()
- 延迟更长的时间才调用 then()
-
什么时候才能得到数据
- 如果先指定的回调,那当状态发生改变时,回调函数就会调用,得到数据
- 如果先改变的状态,那当指定回调时,回调函数就会调用,得到数据
-
1 | // 1、常规:先指定回调函数,后改变的状态 |
-
promise.then() 返回的新 promise 的结果状态有什么决定
-
简单表达:有 then() 指定的回调函数执行的结果决定
-
详细表达:
- 如果抛出异常,新 promise 变成 rejected,reason 为抛出的异常
- 如果返回的是非 promise 的任意值,新 promise 变成 resolved,value 为返回的值
- 如果返回的是另一个新 promise,此 promise 的结果就会成为新的 promise 的结果
-
1 | new Promise((resolve, reject) => { |
-
promise 如何串连多个操作任务
- promise 的 then() 返回一个新的 promise,可以开成 then() 的链式调用
- 通过 then 的链式调用串连多个同步 / 异步任务
1 | new Promise((resolve, reject) => { |
-
promise 异常传透
- 当使用 promise 的 then 链式调用时,可以在最后指定失败的回调
- 前面任何操作出了异常,都会传到最后失败的回调中处理
1 | new Promise((resolve, reject) => { |
如上例可知,当 promise 的状态为 rejected 时,会将 reason 向后面 .then() 的默认的 onRejected() 回调函数( reason => { throw reason }
或者 reason => Promise.reject(reason)
,具体是函数,且看下文的源代码解析)一层一层的往后传递,直到被 .catch() 捕获 。
-
中断 promise 链
- 当使用 promise 的 then 链式调用时,在中间中断,不再调用后面的回调函数
- 办法:在回调函数中返回一个 pending 状态的 promise 对象
1 | new Promise((resolve, reject) => { |
有上例可知,不管是在 .catch(),还是在 .then() 中,只要当回调函数返回的是一个 pending 状态的 promise 对象,就会中断 promise 链
自定义 Promise
定义整体结构
1 | /** |
Promise 构造函数
-
先将Promise实例的this保存到self,否则this会根据执行环境而改变。
给promise对象指定status属性,初始值为pending
给promise对象指定一个拥有存储结果数据的属性
每个元素的结构:
{ onResolved(){}, onRejected(){}}
-
定义 resolve 函数
- 如果当前状态不是pending,直接 return
- 将状态改为 resolved
- 保存value数据
- 如果有待执行callback函数,立即异步执行回调函数;放入队列中执行所有成功的回调
-
执行 reject 函数
- 如果当前状态不是pending,直接 return
- 将状态改为 rejected
- 保存reason数据
- 如果有待执行callback函数,立即异步执行回调函数;放入队列中执行所有失败的回调
-
立即同步执行excutor
如果执行器抛出异常,使用
try...catch
捕获错误,promise对象变为rejected状态
1 | function Promise(excutor) { |
注意:先将Promise实例的this保存到self,否则this会根据执行环境而改变
1 | <!--测试--> |
then() 和 catch() 方法
-
定义一个处理执行
onResolved
或者onRejected
函数执行结果的函数handle
- 如果抛出异常,return的promise就会失败,reason就是error
- 如果回调函数返回不是promise,return的promise就会成功,value就是返回的值
- 如果回调函数返回是promise,return的promise结果就是这个promise的结果
-
当前状态还是pending状态,将回调函数保存起来
-
如果当前是resolved状态,异步执行onResolved并改变return的promise状态
-
如果当前是resolved状态,异步执行onResolved并改变return的promise状态
-
返回一个新的promise对象
-
catch() 相当于 then(undefined, onRejected)
1 | /** |
1 | // 测试代码 |
resolve 和 reject 方法
resolve
- 如果value是promise,就使用value的结果作为promise的结果
- 如果value不是promise,则使用resolve 把 promise变为成功,数据是value
- 返回一个成功/失败的promise
reject
- 使用 reject, 返回一个失败的promise
1 | /** |
测试:
1 | const p1 = Promise.resolve(2) // 如果是一般值,p1成功,value就是这个值 |
all 方法
- 定义一个用来保存所有成功value的数组
- 定义用来保存成功promise的数量
- 遍历promises获取每个promise的结果。(不直接用 p.then,而是用Promise.resolve()包裹,是因为promises可能包含不是promise对象的元素,例如,数字7)
- 如果 p 成功,将成功的value保存为values;成功的数量+1;并将value放入数组对应的位置;如果全部成功了,将return的promise改变成功
- 如果 p 失败,则直接 reject
- 返回一个promise
1 | /** |
1 | // 测试 |
race 方法
- 遍历promise获取每个promise的结果
- 一旦有成功的,将return变为成功
- 一旦有失败了,将return变为失败
- 返回一个promise
1 | /** |
1 | // 测试 |
resolveDelay()和rejectDelay() 方法
使用 setTimeout 来延迟调用 Promise.resolve 和 Promise.reject 的源代码
1 | /** |
1 | // 测试 |
Promise最终版
1 | /** |
promise 的 class 版本
1 | /** |
async 和 await
async 和 await 的基本使用
MDN:
async function
、await
阮一峰的《ECMAScript 6 入门》:async 函数
async函数
- 函数的返回值为promise对象
- promise对象的结果由async函数执行的返回值决定
1 | async function fn1() { |
await表达式
- await右侧的表达式一般为Promise对象,但也可以是其他的值
- 如果await右侧表达式是Promise对象,await返回的是promise成功的值
- 如果await右侧表达式是其他值,直接将此值作为await的返回值
-
注意:
- await必须写在async函数中,但async函数中可以没有await
- 如果await的promise失败了,就会抛出异常,需要通过try…catch来捕获处理
-
1 | function fn2() { |
js异步之宏队列与微队列
原理图
- JS中用来存储待执行回调函数的队列包含2个不同特定的队列
- 宏队列:用来保存待执行的宏任务(回调),比如:定时器回调/DOM事件回调/ajax回调
- 微队列:用来保存待执行的微任务(回调),比如:promise的回调/
MutationObserver
的回调 - JS执行时会区别这2个队列
- JS引擎首先必须执行所有的初始化同步任务代码
- 每次准备取出第一个宏任务执行前,都要将所有的微任务一个一个取出来执行
1 | // (1)、(2)、(3)等注释代表任务 |
执行过程(本文是在浏览器环境下讲解的):
- 执行同步代码(打印 ‘同步代码’),(1)、(3)加入宏队列,(4)、(5)加入微队列
- 执行(4)、(5)(打印 Promise onResolved1() 1、Promise onResolved2() 2)
- 执行(1)(打印 timeout callback1()),将(2)加入微队列
- 执行(2)(打印 Promise onResolved3() 3)
- 执行(3)(打印 timeout callback2())
练习
1 | setTimeout(() => { // (1) |
执行过程:
- (1)加入宏队列,(2)、(3)加入微队列,执行同步代码(4)(即打印 3)
- 清空微队列(即执行微队列中的所有微任务,打印 2 4)
- 执行宏队列的宏任务,每次执行宏任务前都要检查微队列是否为空,如果不为空,则先清空微队列,再执行宏任务(打印 1)
- 结果是:3 2 4 1
1 | setTimeout(() => { // (1) |
- (1)加入宏队列,执行(2),(3)加入微队列,执行(5)
- 执行(3),(4)加入微队列
- 执行(4)
- 执行(1)
1 | const first = () => (new Promise((resolve, reject) => { // (1) |
-
执行(1) 、(2),将(3)加入宏队列,将(5)、(6)加入微队列,执行(7)
-
执行(5)、(6),执行(3)
1 | setTimeout(() => { // (1) |
- 将(1)加入宏队列,执行(2),将(3)加入微队列,执行(8),将(9)加入微队列。(宏:【(1)】,微:【(3)、(9)】)(执行结果:1 7)
- 执行(3)、(4),将(5)、(7)加入微队列,执行(9)。(宏:【(1)】,微:【(5)、(7)】)(执行结果:2 3 8)
- 执行(5)、(7),将(6)加入微队列。(宏:【(1)】,微:【(6)】)(执行结果:4 6)
- 执行(6)。(宏:【(1)】,微:【(7)】)(执行结果:5)
- 执行(1)。(执行结果:0)