js 异步编程总结

本文总结了JavaScript异步编程的常见方式,包括回调函数、事件监听、发布/订阅模式以及Promise。重点讲解了Promise的三种状态和如何避免回调地狱,最后介绍了async/await的特性,它使得异步代码更接近同步风格。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

我们知道 JavaScript 语言的执行环境是“单线程”,代码是一行接着一行、一个函数接着一个函数的执行,不能跳着执行。倘若其中一个方法耗时很长,后面的代码都要等待,这种执行模式叫做“同步”。
但是等待的这段时间,CPU通常是空闲的,其实可以用来执行后面的代码的。于是就有了“异步”这种执行模式的诞生,典型的像代码执行到 ajax 请求时,并不等待返回结果,而是接着往下执行,等到 ajax 请求拿到结果后再执行回调中的代码。
js 异步编程有多种方式,本文总结了常用的几种方式:

回调函数

这是异步编程最基本的方式,如 ajax 请求

axios.get('xxx', (res) => {
	console.log('执行回调函数')
})

事件监听

另外一种思路是通过事件驱动,例如可以监听 ModuleA 的某一事件,当 ModuleA 触发该事件时,就执行 ModuleB 的某一方法。原理如图所示:
在这里插入图片描述
用代码表示大致如下:

ModuleA.on('eventSay', ModuleB.say); // 监听 ModuleA 的 eventSay 事件,当该事件被触发时,调用 ModuleB 的 say 方法
setTimeout(() => {
	ModuleA.trigger('eventSay'); // 触发 ModuleA 的 eventSay 事件
}, 1000)

优点:比较直观,有利于模块化
缺点:当众多模块之间相互监听时,将会形成复杂的关系网,程序的执行流程将变得混乱不行,代码也将难以维护。
在这里插入图片描述

发布/订阅

这种模式和事件监听很相似,但又做了明显的优化。模块之间不再是直接监听,而是全部通过消息中心去调度,这样模块之间就完全解耦了,代码的执行逻辑也变得清晰明了。
在这里插入图片描述
如图所示,ModuelB 监听了消息中心的两个事件 eventSay 和 eventEat,当 ModuleA 或 ModuleC 完成了自己的异步操作后,把相应的事件 push 到消息中心,此时便可以触发 ModuleB 中的方法。这样一来各个模块之间并没有直接的关系,降低了模块间的耦合度。

Promise

Promise 的本意是承诺,承诺过一段时间会给出结果,这个时间通常是指异步调用之后。
它的出现是为了解决回调场景中出现的多层回调函数嵌套的问题,我们称之为“回调地狱”。比起回调,Promise 在用法上更加优雅。

Promise 有三种状态:
  • Pending:Promise 实例在创建之初的初始状态
  • Fullfilled:成功状态
  • Rejected:失败状态

这个状态只能从 Pending -> Fullfilled,或者从 Pending -> Rejected,且不可逆。
在这里插入图片描述

用法

例如我们通常会将调用后端接口的方法写在 service 层。

function getUserInfo () {
	return new Promise((resolve, reject) => {
		ajax('/api/userinfo', (info) => {
			if (info.success === true) {
				resolve(info)
			} else {
				reject('获取用户信息错误')
			}
		})
	})
}

view 层也不再需要将处理方法传入 service

function renderUserInfo (info) {
	document.body = info.username;
}
service.getUserInfo().then(info => {
	renderUserInfo(info)
}).catch(msg => {
	alert(msg)
})

我们可以看到 Promise 是一个构造函数,参数为一个函数,这个函数接收两个参数 resolve 和 reject。真正的异步调用会被封装在这个函数内部,当异步调用成功时,调用 resolve,进入 then 函数。失败时调用 reject,进入catch。
也就是说 .then 其实就是注册异步成功时的回调,也就是 resolve 函数。
.catch 其实就是注册异步失败时的回调,也就是 reject 函数。

自己写一个 Promise

现在我们知道了 Promise 的功能之后,我们尝试自己写一个 Promise 方法

class Promise {
	constructor (exector) {
	    this.value = null;
	    this.reason = null;
	    this.status = 'pending';
		this.resolveCallback = [];
		this.rejectCallback = [];
	    try {
	    	// 传入 Promise 的函数是需要立即执行的
	    	exector(this.resolve.bind(this), this.reject.bind(this))
	    } catch (e) {
	      console.log(e)
	    }
	}
	then (successFunc, errorFunc) {
	    if (this.status === 'fullfilled') {
	    	// 如果 exector 内部没有异步调用,则会先执行到 resolve 函数,状态变成 fullfiled,再执行 then 函数。这时候就不是把 successFunc 注册到 resolveCallback 队列里了,而是需要直接执行
	    	successFunc(this.value)
	    }
	    if (this.status === 'rejected') {
	    	// 道理同上
	    	errorFunc(this.reason)
	    }
	    if (this.status === 'pending') {
	    // exector 里有异步调用的话,则会先走到这里
	      	this.resolveCallback.push(successFunc)
	      	this.rejectCallback.push(errorFunc)
	    }
  }
  catch (func) {
    	this.rejectCallback.push(func)
  }
  resolve (res) {
	  if (this.status === 'pending') {
		this.status = 'fullfilled';
		this.value = res;
		while(this.resolveCallback.length) {
			// 之所以用 shift,是为了把内存释放,因为一个 Promise 实例生命周期内只会执行一次 resolve 方法,执行完之后这个 Promise 实例也就没用了,加入这个时候 resolveCallback 还存在着外部一个函数的引用,那么这个 Promise 实例就不能够被释放,空耗内存。
			this.resolveCallback.shift()()
		}
	  }
  }
  reject (e) {
    if (this.status === 'pending') {
      this.status = 'rejected';
      this.reason = e;
      while (this.rejectCallback.length) {
      	// 道理同上
      	this.rejectCallback.shift()()
      }
    }

  }
}

好了,这就基本完成了一个简易的 Promise 类了。可是我们看到 Promise 是可以链式调用的,then 之后还能 then,所以可以确定的是,then 函数里需要返回一个 Promise 实例,让我们来改造一下这个 then

then (successFunc, errorFunc) {
	return new Promise((resolve, reject) => {
		if (this.status === 'fullfilled') {
	    	x = successFunc(this.value)
	    	if (x instanceof Promise) {
	    		x.then(v => resolve(v))
	    	} else {
	    		resolve(x)
	    	}
	    }
	     if (this.status === 'rejected') {
	    	x = errorFunc(this.reason)
	    	if (x instanceof Promise) {
	    		x.then(v => resolve(v))
	    	} else {
	    		reject(x)
	    	}
	    }
	    if (this.status === 'pending') {
	    	// exector 里有异步调用的话,则会先走到这里
	      	this.resolveCallback.push(() => {
	      		x = successFunc(this.value)
		    	if (x instanceof Promise) {
		    		x.then(v => resolve(v))
		    	} else {
		    		resolve(x)
		    	}
			})
	      	...
	    }
	})
  }

在 then 方法中返回一个全新的 Promise 实例,这样就可以完成我们想要的链式调用。
先判断一下前一个 then 函数里注册的回调函数执行结果是不是一个 Promise 实例,如果是,那么等到这个 Promise 的状态改变后,再去执行下一个 then 函数注册的回调函数。
如此一来,原先需要相互一来的一个异步调用的“回调地狱”写法,就可以写成 promise.then().then().then()…这样的链式调用,代码是不是优雅了很多呢!
此外,Promise 还有另外一个原型链上的方法也一并贴在这里

class Promise {
...
// 等所有的异步任务都成功时,才算成功
static all (list) {
	return new Promise((resolve, reject) => {
		let count = 0;
		let res = []
		list.forEach((item,index) => {
			item.then((res) => {
				count++;
				res[index] = res;
				if (count === list.length) {
					resolve(res)
				}
				// 只要错了一个就结束 Promise.all,并进去它的 catch
			}, e=> reject(e))
		})
	})
}
// 竞赛模式,只要有一个异步执行结束,就算结束
static race (list) {
	return new Promise((resolve, reject) => {
		list.forEach(item=> {
			item.then(res => {
				resolve(res)
			})
		})
	})
}
// 不管是走到成功 resolve 还是走到失败 reject,都要执行这个方法
static finally (callback) {
	// 所以我们只要在成功回调队列和失败回调队列都注册一下callback就行了
	return this.then(value=> {
		callback()
	}, reason => {
		callback()
	})
}
static resolve (value) {
	if (value instanceof Promise) {
		return value;
	} else {
		return new Promise((resolve, reject) => {
			resolve(value)
		})
	}
}
...
}

这样就完成了一个功能完整的 Promise,你学废了吗?

async/await

async/await 有以下几个特点:

  • 基于普通的 promise 实现,它不能用于普通的回调函数
  • 与 promise 一样,是非阻塞式的
  • 它使得异步代码看起来更像同步代码,这正是它的魔力所在
let fs = require('fs')
function read(file) {
  return new Promise(function(resolve, reject) {
    fs.readFile(file, 'utf8', function(err, data) {
      if (err) reject(err)
      resolve(data)
    })
  })
}
async function readResult(params) {
  try {
    let p1 = await read(params, 'utf8')//await后面跟的是一个Promise实例
    let p2 = await read(p1, 'utf8')
    let p3 = await read(p2, 'utf8')
    console.log('p1', p1)
    console.log('p2', p2)
    console.log('p3', p3)
    return p3
  } catch (error) {
    console.log(error)
  }
}
readResult('1.txt').then( // async函数返回的也是个promise
  data => {
    console.log(data)
  },
  err => console.log(err)
)
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值