最近碰到了异步编程的问题,决定从原理开始重新撸一遍,彻底弄懂异步编程。
1.异步编程思想
异步编程是为了解决同步模式的一些痛点,同步模式中任务是依次执行,后一个任务必须要等待前一个任务结束后才能开始执行,当某个函数耗时过长时就可能造成页面的假死和卡顿,而异步编程中,后一个任务不会去等待前一个任务结束后才开始,当前一个任务开启过后就立即往后执行下一个任务。耗时函数的后续逻辑会通过回调函数的方式定义。在内部,耗时任务完成过后就会自动执行传入的回调函数。
2.同步与异步
同步行为对应内存中顺序执行的处理器指令,每条指令都会严格按照出现的顺序来执行,而每条指令执行后也能立即获得储存在系统本地的信息.这样的执行流程容易分析程序在执行到代码任意位置时的状态.
如下例子:
///同步模式 console.log('global begin') function bar () { console.log('bar task') } function foo () { console.log('foo task') bar() } foo() console.log('global end') // 程序打印输出: // global begin // foo task // bar task // global end
在程序执行的每一步都可以推断程序的状态,因为后面的指令需要前面的完成后才执行.等到最后一条指令执行完毕,存储在X的值就可以立即使用.这两行代码首先操作系统会在栈内存上分配一个储存浮点数值的空间,然后针对这个值做一次数学计算,再把计算结果写回之前分配的内存中.这些指令都是单线程中按顺序执行的.
异步行为就是下达这个任务开启的指令之后代码就会继续执行,代码不会等待任务的结束,如下例子:
// 异步模式 console.log('global begin') // 延时器 setTimeout(function timer1 () { console.log('timer1 invoke') }, 1800) // 延时器中又嵌套了一个延时器 setTimeout(function timer2 () { console.log('timer2 invoke') setTimeout(function inner () { console.log('inner invoke') }, 500) }, 1000) console.log('global end') // global begin // global end // timer2 invoke // inner invoke // timer1 invoke
3.以往的异步编程模式
在早期的js中只支持定义回调函数来表明异步操作的完成.串联多个异步操作是一个常见的问题,通常需要深度嵌套的回调函数来解决,这样会造成回调地狱。
回调函数的理解:某个方法a自己没调用,但是在另一个方法b被调用的时候,顺便把方法a给调用了,那么方法a就是一个回调函数
function double(value, callback) { setTimeout(() => { callback(value * 2) }, 2000) } double(3, (x) => { console.log(`回来的数值为:${x}`); }) //回来的数值为:6 //会在2000毫秒后打印
这个的setTimeout调用告诉js运行时在2000毫秒后把一个函数推到消息队列.这个函数会由运行时负责异步调用执行,而位于函数闭包中的回调及其参数在异步执行时仍然时可用的.
4.嵌套异步回调(回调地狱)
如果异步返回值又依赖另一个异步返回值,那么回调的情况还会进一步变复杂,随着代码越来越复杂,回调策略是不具有扩展性,维护起来很麻烦.
// 第一参数为值 // 第二参数为正确的回调 // 第三参数为失败的回调 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
5.ES6的解决方案
ES6中推出了一个处理异步的对象:Promise
Promise解决方式:
Promise 对象代表了未来将要发生的事件,用来传递异步操作的消息。
Promise 对象代表一个异步操作,有三种状态:
- pending: 初始状态,不是成功或失败状态。
- fulfilled: 意味着操作成功完成。
- rejected: 意味着操作失败。
Promise是一个对象,它的参数是一个回调函数,这个回调函数里面有两个参数:resolve和reject,分别代表成功状态fulfilled执行的代码和失败状态rejected执行的代码
需要注意的是,Promise里面的代码本来是当作同步代码执行的,但是一旦遇到异步代码,就会挂起为pending,然后才有后面的异步操作
Promise解决问题:
问题:从后台获取商品数量,并计算商品总价格。
// 这里我用setTimeout模拟异步请求,使用random函数随机获取商品的数量(假设每件商品的单价为50),最后需要输出总价 new Promise((resolve,reject) => { console.log("开始请求数据。。。"); setTimeout(() => { // 这里,我们考虑服务器传来错误的数据,比如负的数量,然后进入reject里面 let count = parseInt(Math.random() * 10) - 5; // [-5,5) if (count >= 0){ resolve(count); }else{ reject(count); } }, 1000); }).then(data => { let price = 50; console.log(`商品的数量为:${data},总价为:${data * price}`) }).catch(err => { console.log(`错误的商品数量:${err}`) })
注意:需要注意的是,then只能写在promise对象上,所以如果想写多个then,则需要在每个then里面重新编写一个promise对象,然后return出去,就可以继续往下写then了
多个promise
// 多个promise的编写 new Promise((reslove, reject) => { console.log(`开始向后台获取商品数量。。。`) setTimeout(() => { let count = parseInt(Math.random() * 10) - 5; if (count >= 0) { reslove(count); } else { reject(count); } }, 2000); }).then(res => { console.log(`商品数量正确,为:${res}`) // 开启一个新的promise,用于计算商品的总价是否大于99(假设商品单价为50) let totalPrice = res * 50; let p = new Promise((reslove, reject) => { if (totalPrice > 99) { reslove(totalPrice); } else { reject(totalPrice); } }) // 如果想继续往下编写then的话,这里必须要返回一个promise对象,因为then必须要学在promise对象后面 return p; }).then(res => { console.log(`商品的总价超过了99,打9折,打折后的价格为:${res * 0.9}`); }).catch(err => { if (err >= 0) { // 如果参数是大于等于0的,说明不是第一个promise的reject console.log(`商品的总价没超过99,不打折,价格为:${err}`) } else { console.log(`商品的数量不能为负数,${err}`) } })
但是,如果then多了还是不够优雅,看着不习惯,因为一般我们的阅读代码的习惯是一行一行的从向往下看,如果可以将异步代码写成这种形式就好了,ES7就实现了这个功能,使用async/await
6.async/await解决方式
async其实可以理解为promise的语法糖,它将promise的编写形式改写为人们更容易理解的串行代码的形式,使得异步代码像同步代码
async 是一个修饰符,被它修饰的函数叫异步函数,会默认的返回一个 Promise 的 resolve的值。
await也是一个修饰符,它后面修饰的函数必须返回一个promise对象,它将异步代码转换为同步结果,会阻塞线程,执行完成后才能执行后面的代码,但是必须用在被async修饰的函数里面
// 使用async/await修改之前的promise // 获取商品数量的方法 let getCount = () => { return new Promise((resolve, reject) => { console.log("开始请求数据。。。"); setTimeout(() => { // 这里,我们考虑服务器传来错误的数据,比如负的数量,然后进入reject里面 let count = parseInt(Math.random() * 10) - 5; // [-5,5) if (count >= 0) { resolve(count); } else { reject(count); } }, 1000); }) } // 计算出总价的方法 let totalPrice = (count) => { console.log("开始计算商品总价。。。"); let p = new Promise((resolve,reject) => { setTimeout(() => { let price = count * 50; if (price >= 99){ resolve(price); }else{ reject(price); } }, 1000); }) return p; } let getPrice = async () => { try{ // 使用await执行 let count = await getCount(); console.log(`商品的数量为:${count}`); let price = await totalPrice(count); console.log(`商品的总价是:${price}`); }catch(error){ console.log(`进入reject,error:${error}`) } } getPrice();
这里除去上面的两部异步请求的方法,可以看到最后面那个async修饰的函数,里面使用await,非常的易于理解,就像是同步代码一样,下一行代码必须等上一次代码执行完成才能继续执行。说的底层一点就是await会阻塞线程,延迟执行await语句后面的语句。