JavaScript的Promise

本文详细介绍了JavaScript中的同步和异步概念,包括同步和异步的区别,回调函数的使用,以及Promise对象在处理异步操作中的重要性。通过示例展示了Promise的创建、状态转变、then()和catch()方法,以及all()和race()方法在并发控制中的应用。文章强调了Promise在解决回调地狱和管理异步流程中的优势。
摘要由CSDN通过智能技术生成

环境

  • Ubuntu 22.04
  • Node.js 18.16.0

同步和异步

Javascript 语言的执行环境是“单线程”(single thread)。所谓“单线程”,就是指一次只能完成一件任务。如果有多个任务,就必须排队,前面一个任务完成,再执行后面一个任务。

这就引发了同步异步的问题。

同步

同步(Synchronous)就是后一个任务等待前一个任务结束,然后再执行,程序的执行顺序与任务的排列顺序是一致的、同步的。这往往用于一些简单的、快速的、不涉及 IO 读写的操作。

例1:

// task 1
while(true) {
}

// task 2
console.log('done')

本例中,task1和task2是同步的、顺序执行的。由于task1是死循环,所以task2实际上并没有机会运行。

异步

异步(Asynchronous)任务分成两段,第一段代码包含对外部数据的请求,第二段代码被写成一个回调函数,包含了对外部数据的处理。第一段代码执行完,不是立刻执行第二段代码,而是将程序的执行权交给下一个任务。等到外部数据返回了,再由系统通知执行第二段代码。所以,程序的执行顺序与任务的排列顺序是不一致的、异步的。

例2:

// task 1
setTimeout(() => {
	console.log('aaa')
}, 1000)

// task 2
console.log('bbb')

运行程序,先打印 bbb ,1秒钟之后,再打印 aaa

bbb
aaa

注:即使把定时器的延时改为0,也会先打印 bbb ,再打印 aaa ,这是因为异步任务会在当前脚本的所有同步任务执行完才会执行。显然,如果task 2包含了死循环,则异步任务就不会运行。

回调函数

在上面的例子中,setTimeout() 方法的参数就是一个回调函数(Callback)。

所谓回调函数,就是一段可执行的代码,以参数的形式传递给其它代码,在合适的时机被调用。

回调函数既可以用于异步,也可以用于同步。

下面是一个同步回调的例子。

例3:

function f1(x) {
	x += 100
	console.log(x)
}

function f2(y, fun) {
	y += 20
	fun(y)
}

f2(50, f1)

程序运行结果为 170

本例中,把 f1() 函数作为回调函数传递给了 f2() 函数。这是一个同步的回调函数。

setTimeout() 、ajax请求等,就是异步回调。

下面是一个异步回调的例子。

例4:

function f1(param1, param2, succFun, errFun) {
	setTimeout(() => {
		if (param2 == 0) {
			errFun('Divide by zero')
		} else {
			let result = param1 / param2
			succFun(result)
		}
	}, 1000)
}

function succ(data) {
	console.log('Result is ' + data)
}

function err(msg) {
	console.log('Error! Message is: ' + msg)
}

f1(100, 2, succ, err)

// other tasks
console.log('Do something...')

程序运行结果如下:

Do something...
Result is 50

如果把第二个参数改为0,则会触发 err() 函数:

Do something...
Error! Message is: Divide by zero

Promise

把例4改造成使用Promise,如下。

例5:

function f1(param1, param2, succFun, errFun) {
	setTimeout(() => {
		if (param2 == 0) {
			errFun('Divide by zero')
		} else {
			let result = param1 / param2
			succFun(result)
		}
	}, 1000)
}

function succ(data) {
	console.log('Result is ' + data)
}

function err(msg) {
	console.log('Error! Message is: ' + msg)
}

new Promise((resolve, reject) => {
	f1(100, 2, resolve, reject)
}).then(succ, err)

// other tasks
console.log('Do something...')

运行结果和例4一样。

本例和例4很像,似乎没什么差异。实际上,Promise的一个优点在于它的多重链式调用,可以避免层层嵌套回调。

(注:例5和例4只是“看上去很像”,二者还是有根本差异的。Promise对象在创建后,会立即同步运行,但是 resolve()reject() 方法会异步运行。比如,如果在 f1() 方法里,最前面加上一行代码 succFun(1234)

  • 例4结果:
Result is 1234
Do something...
Result is 50
  • 例5结果:
Do something...
Result is 1234 # 因为resolve()是异步运行的,而且resolve()只会运行一次

如果不是很明白,不要着急,看下面对Promise的讲解。)

假设由于需求变化,现在要对例4做扩展,接连对数据做四次运算。

例6:

function f1(param1, param2, succFun, errFun) {
	setTimeout(() => {
		if (param2 == 0) {
			errFun('Divide by zero')
		} else {
			let result = param1 / param2
			succFun(result)
		}
	}, 1000)
}

function err(msg) {
	console.log('Error! Message is: ' + msg)
}

f1(100, 2, data1 => {
	console.log('Result is ' + data1)
	f1(200, data1, data2 => {
		console.log('Result is ' + data2)
		f1(300, data2, data3 => {
			console.log('Result is ' + data3)
			f1(400, data3, data4 => {
				console.log('Result is ' + data4)
				// ...
			}, err)
		}, err)
	}, err)
}, err)

// other tasks
console.log('Do something...')

程序运行结果如下:

Do something...
Result is 50
Result is 4
Result is 75
Result is 5.333333333333333

嵌套的调用关系使得代码的可读性和可维护性都变得很差。

使用Promise,则可以用 then() 进行链式回调,避免多重嵌套。

例7:

function f1(param1, param2, succFun, errFun) {
	setTimeout(() => {
		if (param2 == 0) {
			errFun('Divide by zero')
		} else {
			let result = param1 / param2
			succFun(result)
		}
	}, 1000)
}

function err(msg) {
	console.log('Error! Message is: ' + msg)
}

new Promise((resolve, reject) => {
	f1(100, 2, resolve, reject)
}).then(data1 => {
	console.log('Result is ' + data1)
	return new Promise((resolve, reject) => {
		f1(200, data1, resolve, reject)
	})
}).then(data2 => {
	console.log('Result is ' + data2)
	return new Promise((resolve, reject) => {
		f1(300, data2, resolve, reject)
	})
}).then(data3 => {
	console.log('Result is ' + data3)
	return new Promise((resolve, reject) => {
		f1(400, data3, resolve, reject)
	})
}).then(data4 => {
	console.log('Result is ' + data4)
	// ...
}).catch(error => {
	err(error)
})

// other tasks
console.log('Do something...')

Promise对象代表一个未完成、但预计将来会完成的操作。

注意:Promise一旦新建就会立即执行,无法取消,这是一个同步操作。

Promise对象有以下三种状态:

  • pending :初始值
  • fulfilled :代表操作成功
  • rejected :代表操作失败

Promise可以从pending转变为fulfilled,也可以从pending转变为rejected。一旦状态改变,就“凝固”了,会一直保持这个状态,不会再发生变化。

当状态发生变化, then() 绑定的函数就会被调用。

Promise构造函数有一个参数,该参数是一个函数,有两个参数resolve和reject,分别代表成功和失败的回调函数。也就是说,在异步操作成功时,应调用resolve函数,而在异步操作失败时,应调用reject函数。

例8:

var promise = new Promise((resolve, reject) => {
	// 异步操作
	......
	
	if (/* 异步操作成功 */) {
		resolve(data)
	} else {
 		/* 异步操作失败 */
 		reject(error)
	}
})

接下来,可以用 then() 方法指定resolve和reject回调函数。

promise.then(data => {
	// 处理data
	......
}, error => {
	// 处理error
	......
})

会不会出现这种情况:到了该执行回调函数的时候,却尚未还没有指定回调函数。我的理解是,不会出现这种情况,因为 then() 是同步操作,而 resolve() 是异步操作,正如前面提到的,异步操作会在同步操作之后才会运行。

例9:

let promise = new Promise((resolve, reject) => {
	console.log('start')

	let a = 1

	if (a == 1)
		resolve(111)
	else
		resolve(222)

	console.log('end')
})

// 循环会花费几秒钟时间,这是为了晚一点指定回调函数
let x = 0
for(let i = 0; i < 5000000000; i++) {
	x++;
}

console.log('x = ' + x)

promise.then(data => console.log('Result is: ' + data))

本例中,在Promise里直接调用 resolve() 方法,并没有其它异步操作,而 resolve() 方法本身是异步的,所以会在 then() 方法之后才会执行。

程序运行结果如下:

start
end
x = 5000000000
Result is: 111

注意: startend 是立刻输出的,因为它们都是同步操作(前面提到,Promise构造函数是同步运行的),然后过了几秒钟,才输出 x = 5000000000 (这也是同步操作),最后输出 Result is: 111 (这是异步操作)。

then()catch()

then() 方法有两个参数,分别为Promise从 pending 变为 fulfilledrejected 时的回调函数。这两个函数都接受Promise对象传出的值作为参数。 then() 方法返回一个新的Promise对象。

简单来说, then() 方法就是定义resolve和reject函数的。

更直白的讲:

  • 在Promise里合适的时机调用 resolve() 回调(在同步操作之后才会执行);
  • then() 方法里指定 resolve() 回调;

then() 方法也可以只有一个参数,即resolve回调函数。

catch() 方法是 then(undefined, onRejected) 的别名,用于指定发生错误时的回调函数。

例10:

let promise = new Promise((resolve, reject) => {
	if (2 == 1)
		resolve(123)
	else
		reject(456)
})

// 方法 1
promise.then(data => {
        console.log('Result is: ' + data)
}, error => {
        console.log('Error: ' + error)
})

// 方法 2
promise.then(data => {
	console.log('Result is: ' + data)
}).catch(error => {
	console.log('Error: ' + error)
})

// 方法 3
promise.then(data => {
        console.log('Result is: ' + data)
}).then(undefined, error => {
        console.log('Error: ' + error)
})

注意:方法1里,如果 resolve() 回调里报错,不会被 reject() 回调捕获。

reject(xxx) 相当于 throw new Error(xxx)

但是,二者有一个重要的差异: reject() 是异步操作。

例11:

let promise = new Promise((resolve, reject) =>{
	console.log('start')

	// 方法 1
	reject(456)

	// 方法 2
	//throw new Error(456)

	console.log('end')
})

promise.then(data => {
	console.log('Result is: ' + data)
}).catch(error => {
	console.log('Error: ' + error)
})

方法1输出结果如下:

start
end
Error: 456

方法2输出结果如下:

start
Error: Error: 456

原因很简单, reject() 是异步操作,而 throw 是同步操作。

then() 方法指定的回调如果报错,会一直向后传递,直到被捕获。参见例7,如果把2改成0,则四个 then() 方法的回调都不会执行,而是直接执行 catch() 方法的回调了。

Promise状态一旦改变就会凝固,不会再改变。比如,promise一旦resolve了,再reject,也不会变为rejected,也不会被catch。

例12:

let promise = new Promise((resolve, reject) =>{
	console.log('start')

	resolve(123)

	reject(456)
	//throw new Error(456)

	console.log('end')
})

promise.then(data => {
	console.log('Result is: ' + data)
}).catch(error => {
	console.log('Error: ' + error)
})

输出结果如下:

start
end
Result is: 123

本例中, reject() 不起作用。

但是,如果把 reject() 换成 throw ,结果会是什么呢?

start
Result is: 123

解析: resolve() 是异步操作,在同步操作之后执行,但是它会立刻(同步)把Promise状态变为 fulfilled ,状态一旦改变就会凝固,因此,后面再抛异常,也不会触发状态改变,也就不会触发 catch() ,相当于异常被“默默的压抑住了”。当然,由于抛出异常,后面的 console.log('end') 也不会再执行。

换句话说, resolve() 会立即把Promise状态变成 fulfilled ,并且把resolve回调准备好,等同步操作完成后就会执行。

但是这里面有一个问题:

例13:

let promise = new Promise((resolve, reject) =>{
	console.log('start')

	resolve(123)

	throw new Error(456)

	console.log('end')
})

promise.then(data => {
	console.log('Result is: ' + data)
}).catch(error => {
	console.log('Error: ' + error)
})

throw new Error(789)

运行结果如下:

start
/home/ding/temp/temp0606/test13.js:17
throw new Error(789)
^

Error: 789
    at Object.<anonymous> (/home/ding/temp/temp0606/test13.js:17:7)
    at Module._compile (node:internal/modules/cjs/loader:1254:14)
    at Module._extensions..js (node:internal/modules/cjs/loader:1308:10)
    at Module.load (node:internal/modules/cjs/loader:1117:32)
    at Module._load (node:internal/modules/cjs/loader:958:12)
    at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:81:12)
    at node:internal/main/run_main_module:23:47

请注意:Promise里面的 throw new Error(456) 并不起作用(因为是在resolve之后),但是主程序里的 throw new Error(789) 还是起作用的。由于抛出异常,resolve回调并没有执行。

两个throw操作都是同步操作,但显然Promise内部的throw行为有所不同,这一点要格外注意。

另一点要注意的是,Promise里异步操作的异常不会被Promise的 catch() 捕获。

例13.A:

console.log('start')

setTimeout(() => {
	throw new Error(123)
}, 1000)

console.log('end')

运行结果如下:

start
end
/home/ding/temp/temp0606/test23.js:4
	throw new Error(123)
	^

Error: 123
    at Timeout._onTimeout (/home/ding/temp/temp0606/test23.js:4:8)
    at listOnTimeout (node:internal/timers:569:17)
    at process.processTimers (node:internal/timers:512:7)

Node.js v18.16.0

例13.B:

let promise = new Promise((resolve, reject) => {
	console.log('start')
	
	setTimeout(() => {
		throw new Error(123)
	}, 1000)
	
	console.log('end')
})

promise.then(data => {
	console.log('Result is: ' + data)
}).catch(error => {
	console.log('Error: ' + error)
})

运行结果如下:

start
end
/home/ding/temp/temp0606/test24.js:5
		throw new Error(123)
		^

Error: 123
    at Timeout._onTimeout (/home/ding/temp/temp0606/test24.js:5:9)
    at listOnTimeout (node:internal/timers:569:17)
    at process.processTimers (node:internal/timers:512:7)

Node.js v18.16.0

可见,Promise里异步操作中的异常,不会被Promise的 catch() 方法所捕获。

all()race()

语法:Promise.all(iterable)

比如: Promise.all([p1, p2, p3]) ,将多个Promise实例,包装成一个新的Promise实例。当p1、p2、p3的状态都变成 fulfilled 时,该Promise的状态才变成 fulfilled

例14:

console.log('start')

let p1 = new Promise((resolve, reject) => {
	setTimeout(() => {
	resolve(100)}, 1000)
})

let p2 = new Promise((resolve, reject) => {
        setTimeout(() => {
        resolve(200)}, 5000)
})

let p3 = new Promise((resolve, reject) => {
        setTimeout(() => {
        resolve(50)}, 3000)
})

let promise = Promise.all([p1, p2, p3])

promise.then(data => {
	console.log('Result is: ' + data)
})

console.log('end')

运行结果如下:

start
end
Result is: 100,200,50

程序立即输出 startend ,大约5秒钟之后,输出 Result is: 100,200,50 。注意其顺序,是按位置排列,不是按resolve的时间顺序。

若内部多个Promise中任意一个状态变成 rejected ,则外部Promise立刻变成 rejected

例15:

console.log('start')

let p1 = new Promise((resolve, reject) => {
	setTimeout(() => {
	resolve(100)}, 1000)
})

let p2 = new Promise((resolve, reject) => {
        setTimeout(() => {
        resolve(200)}, 5000)
})

let p3 = new Promise((resolve, reject) => {
        setTimeout(() => {
        reject('p3')}, 3000)
})

let promise = Promise.all([p1, p2, p3])

promise.then(data => {
	console.log('Result is: ' + data)
}).catch(error => {
	console.log('Error: ' + error)
})

console.log('end')

运行结果如下:

start
end
Error: p3

程序立即输出 startend ,大约3秒钟之后,输出 Error: p3 ,然后又过了大约2秒钟,程序结束。

race() 方法和 all() 方法很类似,只不过是当多个内部Promise的任意一个状态发生变化( fulfilled 或者 rejected)时,外部Promise的状态就随之变化。

例16:

console.log('start')

let p1 = new Promise((resolve, reject) => {
	setTimeout(() => {
	resolve(100)}, 1000)
})

let p2 = new Promise((resolve, reject) => {
        setTimeout(() => {
        resolve(200)}, 5000)
})

let p3 = new Promise((resolve, reject) => {
        setTimeout(() => {
        resolve(50)}, 3000)
})

let promise = Promise.race([p1, p2, p3])

promise.then(data => {
	console.log('Result is: ' + data)
})

console.log('end')

运行结果如下:

start
end
Result is: 100

程序立即输出 startend ,大约1秒钟之后,输出 Result is: 100 ,然后又过了大约4秒钟,程序结束。

例17:

console.log('start')

let p1 = new Promise((resolve, reject) => {
	setTimeout(() => {
	resolve(100)}, 1000)
})

let p2 = new Promise((resolve, reject) => {
        setTimeout(() => {
        resolve(200)}, 5000)
})

let p3 = new Promise((resolve, reject) => {
        setTimeout(() => {
        reject('p3')}, 3000)
})

let promise = Promise.race([p1, p2, p3])

promise.then(data => {
	console.log('Result is: ' + data)
}).catch(error => {
	console.log('Error: ' + error)
})

console.log('end')

运行结果如下:

start
end
Result is: 100

程序立即输出 startend ,大约1秒钟之后,输出 Result is: 100 ,然后又过了大约4秒钟,程序结束。

虽然在p3里,3秒钟时抛出了异常,但是为时已晚,因为p1在1秒钟时resolve了,所以promise就随之resolve了。

resolve()reject()

Promise.resolve(xxx) 相当于 new Promise(resolve => {resolve(xxx)})

Promise.reject(xxx) 相当于 new Promise((resolve, reject) => {reject(xxx)})

Promise.resolve() 的另一个作用就是将thenable对象(即带有 then() 方法的对象)转换为Promise对象。

例18:

let promise = Promise.resolve({
	then: (resolve, reject) => {
		resolve(123)
	}
})

console.log(promise instanceof Promise)

promise.then(data => {
	console.log('Result is: ' + data)
})

运行结果如下:

true
Result is: 123

Promise.resolve() 还可以resolve一个Promise对象。

例19:

let p1 = new Promise((resolve, reject) => {
	console.log('start')
	resolve(123)
	console.log('end')
})

let p2 = Promise.resolve(p1)

p2.then(data => {
	console.log('Result is: ' + data)
})

运行结果如下:

start
end
Result is: 123

then() 会返回一个新创建的Promise对象(不是原来的那个对象)。比较下面两个例子:

例20:

let promise = new Promise((resolve, reject) => {
	console.log('start')
	resolve(123)
	console.log('end')
})

promise.then(data => {
	console.log('p1: ' + data)
	return data + 1000
})

promise.then(data => {
        console.log('p2: ' + data)
	return data + 10000
})

promise.then(data => {
        console.log('p3: ' + data)
})

运行结果如下:

start
end
p1: 123
p2: 123
p3: 123

例21:

let promise = new Promise((resolve, reject) => {
	console.log('start')
	resolve(123)
	console.log('end')
})

promise.then(data => {
	console.log('p1: ' + data)
	return data + 1000
})
.then(data => {
        console.log('p2: ' + data)
	return data + 10000
})
.then(data => {
        console.log('p3: ' + data)
})

运行结果如下:

start
end
p1: 123
p2: 1123
p3: 11123

思考题

1

下面代码的输出结果是什么?

例22:

function f() {
	let promise = new Promise((resolve, reject) => {
		console.log('start')
		resolve(123)
		console.log('end')
	})

	promise.then(data => {
		console.log('Inside result: ' + data)
		return data + 1000
	})

	return promise
}

f().then(data => {
	console.log('Outside result: ' + data)
})

运行结果如下:

start
end
Inside result: 123
Outside result: 123

解析:参见例20。如果想要将外部结果变成1123,则在 f() 里不能返回promise,而应该返回 promise.then()

2

下面代码的输出结果是什么?

例23:

console.log('start')

let promise = new Promise((resolve, reject) => {
    reject(123)
    setTimeout(() => {
        resolve()
    }, 0)
})

promise.then(data => {
    console.log('Result is: ' + data)
}, error => {
    console.log('Error: ' + error)
})

console.log('end')

运行结果如下:

start
end
Error: 123

解析:Promise构造函数是同步执行的, reject() 虽然是异步操作,但是会立即(同步)把Promise状态设置为 rejected ,而定时器是异步操作,即使时间设置为0,也会在同步操作都运行完以后才会运行。因此,Promise状态会被设置为 rejected ,之后就不会再发生变化。在同步操作运行完之后,就会执行 then() 方法里指定的onRejected回调函数。

参考

  • https://www.cnblogs.com/le220/p/10381920.html
  • https://www.w3cschool.cn/javascript_guide/javascript_guide-a9jb269c.html
  • https://www.runoob.com/js/js-promise.html
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值