前端笔记 异步编程
异步编程
一、进程与线程
进程(工厂):可以理解为程序运行的环境,程序需要在内存中开辟一部分空间,存储代码、数据等内容
线程(工人):是实际进行运算的部分,
二、同步问题
通常情况下代码都是同步执行的,即自上而下逐行执行。
如以下代码:
1 |
|
输出应该为:
1 |
|
同步情况下,如果前一条代码没有执行完,后面的代码也不会执行
如果该条代码非常耗时(设计网络请求、IO等内容的操作),会导致后续代码被阻塞
即“一行代码执行缓慢”会影响整个程序的执行
三、其他语言的解决办法(Java、Python)—— 多线程
同步问题在 Java、Python 中是通过多线程的方式来解决的;
将高耗时的任务放在单独的线程中执行,其他任务不受影响;
但多线程会带来更高的性能开销,对服务器性能要求更高;
同时,对编写程序的要求也更高,程序需要针对多线程进行优化。
四、Node.js 的解决方法 —— 异步
异步是指,一段代码的执行不会影响其他代码的执行
这样就能让高耗时的任务不阻塞后续代码
如 setTimeout() 函数就是异步代码:
1 |
|
可以看到 “222” 的输出并未被阻塞,但 result 也变为了 undefined,这是因为在输出 result 时,函数还未能返回值
这也就引出了异步的一个问题,异步函数无法通过 return 的方式来直接返回结果
五、通过回调函数返回结果
为了解决这一问题,可以采用回调函数的方法,在使用需要异步代码的结果时,在获取到结果后调用回调函数来获取:
如:
1 |
|
回调函数的方法看似解决了异步问题,但如果这一函数被反复使用,会形成如下情况:
如在执行 111 + 222 后,还需将结果再加上 333
1 |
|
就会形成如上图所示的回调函数嵌套的情况,如果需要继续使用,就会不断嵌套:
1 |
|
由于每次执行新的异步代码,都需要在之前的异步代码的回调函数中书写
导致代码书写非常繁杂,代码可读性非常差,代码难以调试,被称之为“回调地狱”
六、Promise 解决异步问题
Promise 是一个用于存储数据的容器,存取数据的方式比较特殊,使得 Promise 可以存储异步调用的数据
创建 Promise:
可以使用构造函数来创建,且构造函数中需要一个函数作为参数
这个函数会在创建 Promise 时调用,调用时会传入两个参数,分别为 resolve 和 reject:
1 |
|
在读取数据时,可以通过 Promise 的实例方法 then() 来读取
也需要两个回调函数作为参数,来获取 Promise 中的数据
第一个函数获取 resolve 返回的数据(正常数据),第二个获取 reject 返回的数据(异常数据)
1 |
|
七、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 的回调函数是无参的,不会接收到数据,用于执行无论成功与否都要执行的代码