0、异步编程
JavaScript
是单线程事件循环模型,一步行为是为了优化因计算量大而时间长的操作,如果在等待其他操作完成的同时,即使运行其他指令,系统也能保持稳定.- 重要的是,并不是一定要量大或等待时间长才可以使用.只要你不想为等待异步操作而阻塞线程执行,什么时候都可以使用.
- 换成大白话来理解就是假设下班后需要乘车、买菜、做饭、吃饭,那么在下班的时候可以委派外卖员帮忙买菜,委派饭店老板煮熟,回家吃就行,而并非需要按序执行。
1、同步与异步
同步行为对应内存中顺序执行的处理器指令,每条指令都会严格按照出现的顺序来执行,而每条指令执行后也能立即获得储存在系统本地的信息.这样的执行流程容易分析程序在执行到代码任意位置时的状态. 如下例子:
在程序执行的每一步都可以推断程序的状态,因为后面的指令需要前面的完成后才执行.等到最后一条指令执行完毕,存储在X的值就可以立即使用. 这两行代码首先操作系统会在栈内存上分配一个储存浮点数值的空间,然后针对这个值做一次数学计算,再把计算结果写回之前分配的内存中.这些指令都是单线程中按顺序执行的.
异步行为:这段代码如果按照单线程来输出应该是
1,2,3
,但这里加上延时器做异步演示,输出的是2,3,1
,这是因为这次执行线程1的值取决于回调何时从消息列队出列并执行.
2、以往的异步编程模式
在早期的js中只支持定义回调函数来表明异步操作的完成.串联多个异步操作是一个常见的问题,通常需要深度嵌套的回调函数(回调地狱)来解决,
异步返回值
假设异步操作中会返回一个有用的值,有什么好办法把这个值传给需要它的地方?广泛接受的一个策略是给异步操作提供一个回调
callback
,这个回调中包含要使用异步返回值的代码(作为回调的参数)
function double(value, callback) {
setTimeout(() => {
callback(value * 2)
}, 2000)
}
double(3, (x) => { console.log(`回来的数值为:${x}`); })
//回来的数值为:6
//会在2000毫秒后打印
这个的setTimeout调用告诉js运行时在2000毫秒后把一个函数推到消息队列.这个函数会由运行时负责异步调用执行,而位于函数闭包中的回调及其参数在异步执行时仍然时可用的.
失败处理
这种模式较少使用,因为必须在初始化异步操作时定义回调,异步函数的返回值旨在短时间内存在,只有预备好将在这个短时间内存在的值作为参数的回调才能接收到它.
// 第一参数为值
// 第二参数为正确的回调
// 第三参数为失败的回调
function double(value, success, failure) {
setTimeout(() => {
try {
if (typeof value !== 'number') {
throw '必须提供数字作为第一个参数'
}
success(value * 2)
} catch (e) {
failure(e)
}
}, 2000)
}
const successCallback = (x) => console.log(`success:${x}`);
const failureCallback = (e) => console.log(`failure:${e}`);
double(3, successCallback, failureCallback)//success:6
double('b', successCallback, failureCallback)//failure:必须提供数字作为第一个参数
3、嵌套异步回调(回调地狱)
如果异步返回值又依赖另一个异步返回值,那么回调的情况还会进一步变复杂,随着代码越来越复杂,回调策略是不具有扩展性,维护起来很上头.
// 第一参数为值
// 第二参数为正确的回调
// 第三参数为失败的回调
function double(value, success, failure) {
setTimeout(() => {
try {
if (typeof value !== 'number') {
throw '必须提供数字作为第一个参数'
}
success(value * 2)
} catch (e) {
failure(e)
}
}, 2000)
}
const successCallback = (x) => double(x, (y) => { console.log(`success:${y}`); })
const failureCallback = (e) => console.log(`failure:${e}`);
double(3, successCallback, failureCallback)//success:12