前端笔记 异步编程

异步编程

一、进程与线程

进程(工厂):可以理解为程序运行的环境,程序需要在内存中开辟一部分空间,存储代码、数据等内容

线程(工人):是实际进行运算的部分,

二、同步问题

通常情况下代码都是同步执行的,即自上而下逐行执行

如以下代码:

1
2
3
console.log("哈哈")
console.log("嘿嘿")
console.log("嘻嘻")

输出应该为:

1
2
3
哈哈
嘿嘿
嘻嘻

同步情况下,如果前一条代码没有执行完,后面的代码也不会执行

如果该条代码非常耗时(设计网络请求、IO等内容的操作),会导致后续代码被阻塞

“一行代码执行缓慢”会影响整个程序的执行

三、其他语言的解决办法(Java、Python)—— 多线程

同步问题在 Java、Python 中是通过多线程的方式来解决的;

将高耗时的任务放在单独的线程中执行,其他任务不受影响;

但多线程会带来更高的性能开销,对服务器性能要求更高;

同时,对编写程序的要求也更高,程序需要针对多线程进行优化。

四、Node.js 的解决方法 —— 异步

异步是指,一段代码的执行不会影响其他代码的执行

这样就能让高耗时的任务不阻塞后续代码

如 setTimeout() 函数就是异步代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function sum(a, b){
setTimeout(()=>{
return a + b;
}, 10000)
}

console.log("111")
const result = sum(111, 222)
console.log(result)
console.log("222")

/**
* 输出:
*
* 111
* undefined
* 222
*
*/

可以看到 “222” 的输出并未被阻塞,但 result 也变为了 undefined,这是因为在输出 result 时,函数还未能返回值

这也就引出了异步的一个问题,异步函数无法通过 return 的方式来直接返回结果

五、通过回调函数返回结果

为了解决这一问题,可以采用回调函数的方法,在使用需要异步代码的结果时,在获取到结果后调用回调函数来获取:

如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function sum(a, b, callback){
setTimeout(()=>{
return callback(a + b);
}, 10000)
}

console.log("111")
const result = sum(111, 222, (result)=>{
console.log(result)
})
console.log("222")

/**
* 输出:
*
* 111
* 222
*
* 十秒后
*
* 333
*
*/

回调函数的方法看似解决了异步问题,但如果这一函数被反复使用,会形成如下情况:

如在执行 111 + 222 后,还需将结果再加上 333

1
2
3
4
5
const result = sum(111, 222, (result)=>{
sum(result, 333, (result)=>{
console.log(result)
}
})

就会形成如上图所示的回调函数嵌套的情况,如果需要继续使用,就会不断嵌套:

1
2
3
4
5
6
7
8
9
10
11
const result = sum(111, 222, (result)=>{
sum(result, 333, (result)=>{
sum(result, 444, (result)=>{
sum(result, 555, (result)=>{
sum(result, 666, (result)=>{
......
}
}
}
}
})

由于每次执行新的异步代码,都需要在之前的异步代码的回调函数中书写

导致代码书写非常繁杂代码可读性非常差代码难以调试,被称之为“回调地狱”

六、Promise 解决异步问题

Promise 是一个用于存储数据的容器,存取数据的方式比较特殊,使得 Promise 可以存储异步调用的数据

创建 Promise:

可以使用构造函数来创建,且构造函数中需要一个函数作为参数

这个函数会在创建 Promise 时调用,调用时会传入两个参数,分别为 resolve 和 reject

1
2
3
4
5
6
7
const promise = new Promise((resolve, reject)=>{
// resolve 和 reject 也是函数,通过这两个函数向 Promise 中存储数据
// resolve 在执行正常时存储数据,reject 在执行错误时存储数据
setTimeout(()=>{
resolve("成功")
},2000)
})

在读取数据时,可以通过 Promise 的实例方法 then() 来读取

也需要两个回调函数作为参数,来获取 Promise 中的数据

第一个函数获取 resolve 返回的数据(正常数据)第二个获取 reject 返回的数据(异常数据)

1
2
3
4
5
promise.then((result)=>{
console.log(result)
}, (error)=>{
console.log(result)
})

七、Promise 原理

异步的真正难点不在于获取异步数据,而在于获取异步数据的时机,因为我们难以预测异步代码执行需要多久

为了解决这一问题,Promise中维护了两个隐藏属性:

PromiseResult - 用于存储数据

PromiseState - 用于记录 Promise 状态,分为三种:

- **fulfilled** 完成 - 通过 resolve 存储数据时,切换为这个状态
- **rejected** 拒绝\出错 - 通过 reject 存储数据 ,切换为这个状态
- **pending** 进行中 - 初始状态

PromiseState 只能修改一次且修改后永远不会改变

这两个属性设置为私有属性,也是为了保证数据不被篡改

Promise 的详细流程如下:

  • Promise 创建时,PromiseState 初始值为 pending
    • 当通过 resolve 存储时,PromiseState 切换为 fulfilled,PromiseResult 变为存储的数据
    • 当通过 reject 存储或出错时,PromiseState 切换为 rejected,PromiseResult 变为存储的数据或异常信息
  • 当通过 then 读取数据时,相当于为 Promise 设置了回调函数
    • 当 PromiseState 为 fulfilled 时,调用 then 的第一个回调函数来返回
    • 当 PromiseState 为 rejected 时,调用 then 的第二个回调函数来返回

Promise还设置了 catch() 方法,用于异常处理

catch() 与 then 类似,但只需要一个回调函数,这个回调函数只会在 rejected 时才会调用

catch() 相当于 then(null, reason =>{ })

Promise 还设置了 finally() 方法,无论是正常获取还是异常,都会执行

但 finally 的回调函数是无参的,不会接收到数据,用于执行无论成功与否都要执行的代码


前端笔记 异步编程
https://username.github.io/2023/02/15/前端笔记-异步编程/
作者
ZhuoRan-Takuzen
发布于
2023年2月15日
许可协议