8000 参透 JavaScript —— 异步编程与Promise · Issue #9 · bryqiu/Blog · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

参透 JavaScript —— 异步编程与Promise #9

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
bryqiu opened this issue Mar 8, 2025 · 0 comments
Open

参透 JavaScript —— 异步编程与Promise #9

bryqiu opened this issue Mar 8, 2025 · 0 comments
Labels
参透JavaScript系列 参透 JavaScript 核心技术

Comments

@bryqiu
Copy link
Owner
bryqiu commented Mar 8, 2025

前言

现代 JavaScript 开发中,异步编程是一个很重要的话题,Promise 的出现为 JavaScript 异步编程提供了优雅的标准解决方案

本篇文章我们将循序渐进理解 Promise 核心知识

同步和异步的概念理解

我们都知道,JavaScript 是单线程的,如果把线程想象成一个快递员(镇上唯一的快递员),那么

同步模式:你必须按顺序挨家挨户的送货,每送到一户,都要等待收件人签收,在当前收件人完成签收前,你无法前往下一户

基于这样的情况,不难看出同步模式的特点在于:

  • 阻塞式执行:必须要等待收件人签收,才能送下一单
  • 执行顺序是可预测的:某一个快递是否签收?这样的问题可以预测或解答
  • 利用率低:如果某个快递消耗时间长(比如开箱检查),快递员必须等待

异步模式:快递员将包裹统统放在驿站,每个包裹都有一个取件码,收件人凭取件码取件,它们之前互不影响

同样的,异步模式的特点在于:

  • 非阻塞式执行:快递员把快递存放驿站,取件/签收由收件人自行完成
  • 并发:可以处理多个快递的投放状态

记住,同步与异步的本质区别在于:是否会阻塞当前执行流程

JavaScript 中的异步概念,更多指的是单线程 + 任务队列管理异步任务的执行顺序

注意,JavaScript 是单线程的,要避免将异步和多线程混淆,异步是同时处理多个任务,就好比一个人在边吃饭边看电视,而不是两个人吃饭看电视

Promise 之前的回调地狱(Callback Hell)

回调地狱是在异步操作中因多层嵌套的回调函数导致的代码结构问题,它的表现方式就像是俄罗斯套娃,一层一层往下套,特别是在处理多个顺序依赖的异步操作时,也就是下一层的操作,需要基于上一层的数据

function double(value,successCallback){
    setTimeout(()=>{
        successCallback(value * 2)
    },1000)
}

// 回调地狱
double(10,(firstResult)=>{
  console.log(firstResult);// 20
  double(firstResult,(secondResult)=>{
    console.log(secondResult); // 40
    double(secondResult,(thirdResult)=>{
      console.log(thirdResult); // 80
    })
  })
})

上面这段代码,在一秒后将打印出数字 20,再一秒后打印 40,最后是 80

回调地狱的核心问题在于其代码的可读性、可维护性难以言喻,就如同打了个死结!

Promise 对象

ECMAScript 6 依据社区广泛流行的 Promise/A+ 规范并加以完善支持,在此基础上推出了 Promise 对象,为异步编程模式提供了标准方案

Promise 的基本用法

Promise 对象是一个构造函数,可以通过 new 操作符来创建它的实例,它接收一个执行器函数(executor)作为参数,比如:

const p1 = new Promise(()=>{})
console.log(p1);

在控制台打印 p1 实例,你会发现存在一个 [[PromiseState]] 属性,这个属性的值范围可以是三种状态:

  • pending(待定) :初始状态,既没有完成,也没有被拒绝
  • fulfilled(已完成):意味着操作成功完成
  • rejected(已拒绝):意味着操作失败

这三种状态中,pending 是最初始的状态,也就是没有做任何影响状态的操作

但要注意的是,一个 Promise 对象的状态一旦发生改变,比如从 pending 转为 fulfilledpending 转为 rejected 后,就已经落定,是不可逆的

并且,Promise 状态不能通过 JS 检测到,也无法进行修改

通过执行器函数(executor)来切换状态

上面说到,Promise 实例化时接收一个执行器函数,而这个执行器函数又接收两个函数作为参数,一般是 resolvereject

在 TypeScript 中,表现为:

interface PromiseConstructor {
	//...
	new <T>(executor: (resolve: (value: T | PromiseLike<T>) => void, reject: (reason?: any) => void) => void): Promise<T>;
}

resolve 翻译为解决的意思,可以将 Promise 对象的状态从 pending(待定) 转变到 fulfilled(已完成),并将结果(value)放在参数中传递出去

const p1 = new Promise((resolve,reject)=>{
    resolve('成功!')
})
console.log(p1);

相应的,reject 翻译为拒绝的意思,可以将 promise 状态从pending(待定) 转变到 rejected(已拒绝),并将理由(reason)放在参数中传递出去

const p1 = new Promise((resolve,reject)=>{
  reject('失败')
})
console.log(p1);

Promise 的实例方法

Promise 的实例上可以访问三种方法,这些方法是在 Primise.prototype 上定义的

Promise.prototype.then

先聊一聊重要的 then() 方法,它为 Promise 对象的 fulfilled(完成) 状态、rejected(拒绝) 状态提供处理程序,接收两个参数:

  • onFulfilled:当状态为 fulfilled 时执行此回调函数,接收一个参数,参数值来于 resolve(value) 中传递的 value
  • onRejected:当状态为 rejected 时执行此回调函数,接收一个参数,参数值来于 reject(reason) 调用时传递的 reason(原因,理由)

onFulfilledonRejected 这两个参数在 then() 中是可选的,只写单个参数也可以

它的 TypeScript 类型表现为:

interface Promise<T> {
 	//...
	then<TResult1 = T, TResult2 = never>(onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | undefined | null, onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | undefined | null): Promise<TResult1 | TResult2>;
}

理论了解后,来看看下面这个例子:

const p1 = new Promise((resolve,reject)=>{
    if(/**条件 */){
        resolve('成功值') // 触发 then 的 onFulfilled
    }else{
        reject(new Error('失败')) // 触发 then 的 onRejected
    }
})

p1.then(
  (res)=>{
    console.log(res); // 成功值
},
  (err)=>{
    console.log(err); // Error 对象
}
)

then() 方法会返回一个新的 Promise 实例,这意味着 then() 可以形成链式调用,也就是在 then() 之后再接 then()

并且,上一个 then() 可以通过 return 将值传递给下一个 then() ,但请注意,这里有一些规则:

  • 如果没有返回,则下一个 then() 的参数是 undefined
  • 如果返回的是一个普通值,那么将立即触发后续的 then(),并将值传递
  • 如果返回的是一个 Promise 实例,那么等到此 Promise 状态为 fulfilled(完成)rejected(拒绝)时将值/理由传递

看下面的例子:

const p1 = new Promise((resolve,reject)=>{
  resolve('成功值') // 触发 then 的 onFulfilled
})
.then(res=>{
  console.log('首次then接收:', res) // 首次then接收: 成功值
  return new Promise((resolve,reject)=>{
    setTimeout(()=>{
      resolve('链式') // 异步改变状态为fulfilled(1秒后)
    },1000)
  })
})
.then(res=>{
   // 前序Promise解决后才执行(等待1秒)
   console.log('异步then接收:', res) // 异步then接收: 链式
  return '第二个then的值' // 同步
})
.then(res=>{
  console.log(res); // 第二个then的值
})

Promise.prototype.catch

catch() 方法用于执行 Promise 状态为 rejected(拒绝) 的回调函数

它的 TypeScript 类型表现为:

interface Promise<T> {
    //...
	catch<TResult = never>(onrejected?: ((reason: any) => TResult | PromiseLike<TResult>) | undefined | null): Promise<T | TResult>;
}

在上文介绍的 then() 方法中,我们说它接收 onFulfilledonRejected 两个参数

但在一般的实践中,我们通常使用 catch() 来充当 onRejected 处理程序的角色,因为 catch() 方法实际上是 then() 第二个参数 onRejected 程序的语法糖

并且,catch()then() 一样,返回一个 Promise 对象

const p1 = new Promise((resolve,reject)=>{
  reject(new Error('失败值'))
})

p1
.then(res=>{
  // 当状态为fulfilled时
  console.log(res);
})
.catch(err=>{
  // 当状态为rejected时
  console.log(err);
})

Promise.prototype.finally

finally() 方法的特点是不管 Promise 状态如何变化,都会执行的操作

它接收一个参数:

  • onFinally:在状态为 fulfilled(成功)rejected(拒绝) 时都会触发的回调函数,但要注意,这个回调函数不接收任何参数值

TypeScript类型表现为:

interface Promise<T> {
    //...
    finally(onfinally?: (() => void) | undefined | null): Promise<T>;
}

和前面的两个方法一样,finally() 方法也返回一个 Promise

Promise 解决回调地狱

还记得在回调地狱章节的示例代码吗?现在,有了 Promise 后,我们有了更棒的解决方案:

const asyncDouble = (value,successCallback)=>{
  return new Promise((resolve)=>{
    setTimeout(()=>{
      resolve(value * 2)
    },1000)
  })
}

asyncDouble(10)
  .then(res => { console.log(res); return asyncDouble(res) }) // 20
  .then(res => { console.log(res); return asyncDouble(res) }) // 40
  .then(res => { console.log(res); return asyncDouble(res) }) // 80

总结

总结一下本文的内容,在开头我们通过快递员的例子讲解了同步和异步的概念:

  • 同步:挨家挨户的送货,在未签收前无法前往下一家
  • 异步 7E92 快递存放驿站,用户可以互不影响,多任务的签收

然后,我们举例说明了在没有 Promise 之前,异步函数多层嵌套带来的可阅读性、可维护性差问题(回调地狱)

正式介绍 Promise 对象时,我们介绍了其基本语法和三个状态,并可以通过执行器函数切换状态,还介绍了 Promise.prototype 原型对象带给实例的三个方法 then()catch()finally()

参考资料

参透JavaScript系列

本文已收录至《参透JavaScript系列》,全文地址:我的 GitHub 博客 | 掘金专栏

交流讨论

对文章内容有任何疑问、建议,或发现有错误,欢迎交流和指正

@bryqiu bryqiu added the 参透JavaScript系列 参透 JavaScript 核心技术 label Mar 8, 2025
@bryqiu bryqiu changed the title 【JavaScript内功系列】循序渐进理解 Promise 异步编程(一) 【JavaScript内功系列】理解同步异步与 Promise 异步编程 Mar 11, 2025
@bryqiu bryqiu changed the title 【JavaScript内功系列】理解同步异步与 Promise 异步编程 参透 JavaScript —— 异步编程与Promise Apr 11, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
参透JavaScript系列 参透 JavaScript 核心技术
Projects
None yet
Development

No branches or pull requests

1 participant
0