JS异步处理的进化史深入讲解
前言
javascript是一门单线程的语言,也就是说一次只能完成一件任务,如果有多个任务,就需要排队进行处理。如果一个任务耗时很长,后面的任务也必须排队等待,这样大大的影响了整个程序的执行。为了解决这个问题,javascript语言将任务分为两种模式:
- 同步:当我们打开网站,网页的页面骨架渲染和页面元素渲染,就是一大推同步任务。
- 异步:我们在浏览新闻时,加载图片或音乐之类占用资源大且耗时久的任务就是异步任务。
本文主要针对近两年javascript的发展,主要介绍异步处理的进化史。目前,在javascript异步处理中,有以下几种方式:
callback
回调函数是最早解决异步编程的方法。无论是常见的settimeout还是ajax请求,都是采用回调的形式把事情在某一固定的时刻进行执行。
//常见的:settimeout settimeout(function callback(){ console.log('aa'); }, 1000); //ajax请求 ajax(url,function callback(){ console.log("ajax success",res); })
回调函数的处理一般将函数callback作为参数传进函数,在合适的时候被调用执行。回调函数的优点就是简单、容易理解和实现,但有个致命的缺点,容易出现回调地狱(callback hell),即多个回调函数嵌套使用。造成代码可读性差、可维护性差且只能在回调中处理异常。
ajax(url, () => { //todo ajax(url1, () => { //todo ajax(url2, () => { //todo }) }) })
事件监听
事件监听采用的是事件驱动的模式。事件的执行不取决于代码的顺序,而是某个事件的发生。
假设有两个函数,为f1绑定一个事件(jquery的写法),当f1函数发生success事件时,执行函数f2:
f1.on('success',f2);
对f1进行改写:
function f1(){ ajax(url,() => { //todo f1.trigger('success');//触发success事件,从而执行f2函数 }) }
事件监听的方式较容易理解,可以绑定多个事件,每个事件可以指定多个回调函数,而且可以"去耦合",有利于实现模块化。缺点是整个程序都要变成事件驱动型,运行流程会变得很不清晰。阅读代码的时候,很难看出主流程。
发布订阅
我们假定,存在一个"信号中心",某个任务执行完成,就向信号中心"发布"(publish)一个信号,其他任务可以向信号中心"订阅"(subscribe)这个信号,从而知道什么时候自己可以开始执行。这就叫做 发布/订阅模式(publish-subscribe pattern),又称**观察者模式"(observer pattern) **。
//利用jquery的插件实现 //首先,f2向消息中心订阅success事件 jquery.subscribe('success',f2); //对f1进行改写: function f1(){ ajax(url,() => { //todo jquery.publish('success');//当f1执行完毕后,向消息中心jquery发布success事件,从而执行f2函数 }) } //f2执行完毕后,可以取消订阅 jquery.unsubscribe('success',f2)
该方法和事件监听的性质类似,但我们可以通过消息中心来查阅一共有多少个信号,每个信号有多少个订阅者。
promise
**promise**是commonjs工作组提出的一种规范,可以获取异步操作的消息,也是异步处理中常用的一种解决方案。promise的出现主要是用来解决回调地狱、支持多个并发的请求,获取并发请求的数据并且解决异步的问题。
let p = new promise((resolve, reject) => { //做一些异步操作 settimeout(()=>{ let num = parseint(math.random()*100); if(num > 50){ resolve("num > 50"); // 如果数字大于50就调用成功的函数,并且将状态变成resolved }else{ reject("num <50");// 否则就调用失败的函数,将状态变成rejected } },10000) }); p.then((res) => { console.log(res); }).catch((err) =>{ console.log(err); })
promise有三种状态:等待pending、成功fulfied、失败rejected;状态一旦改变,就不会再变化,在promise对象创建后,会马上执行。等待状态可以变为fulfied状态并传递一个值给相应的状态处理方法,也可能变为失败状态rejected并传递失败信息。任一一种情况出现时,promise对象的 then 方法就会被调用(then方法包含两个参数:onfulfilled 和 onrejected,均为 function。当promise状态为fulfilled时,调用 then 的 onfulfilled 方法,当promise状态为rejected时,调用 then 的 onrejected 方法)。
需要注意的是: promise.prototype.then 和 promise.prototype.catch 方法返回promise 对象, 所以可以被链式调用,如下图:
promise的方法:
- promise.all(iterable):谁执行得慢,以谁为准执行回调。返回一个promise对象,只有当iterable里面的所有promise对象成功后才会执行。一旦iterable里面有promise对象执行失败就触发该对象的失败。对象在触发成功后,会把一个包iterable里所有promise返回值的数组作为成功回调的返回值,顺序跟iterable的顺序保持一致;如果这个新的promise对象触发了失败状态,它会把iterable里第一个触发失败的promise对象的错误信息作为它的失败错误信息。promise.all方法常被用于处理多个promise对象的状态集合。
- promise.race(iterable): 谁执行得快,以谁为准执行回调。iterable参数里的任意一个子promise被成功或失败后,父promise马上也会用子promise的成功返回值或失败详情作为参数调用父promise绑定的相应句柄,并返回该promise对象。
- promise.reject(err)与promise.resolve(res)
generators/yield
generators是es6提供的异步解决方案,其最大的特点就是可以控制函数的执行。可以理解成一个内部封装了很多状态的状态机,也是一个遍历器对象生成函数。generator 函数的特征:
- function关键字与函数名之间有一个星号;
- 函数体内部使用yield表达式,定义不同的内部状态;
- 通过yield暂停函数,next启动函数,每次返回的是yield表达式结果。next可以接受参数,从而实现在函数运行的不同阶段,可以从外部向内部注入不同的值。next返回一个包含value和done的对象,其中value表示迭代的值,后者表示迭代是否完成。
举个例子:
function* createiterator(x) { let y = yield (x+1) let z = 2*(yield(y/3)) return (x+y+z) } // generators可以像正常函数一样被调用,不同的是会返回一个 iterator let iterator = createiterator(4); console.log(iterator.next()); // {value:5,done:false} console.log(iterator.next()); // {value:nan,done:false} console.log(iterator.next()); // {value:nan,done:true} let iterator1 = createiterator(4);//返回一个iterator //next传参数 console.log(iterator1.next()); // {value:5,done:false} console.log(iterator1.next(12)); // {value:4,done:false} console.log(iterator1.next(15)); // {value:46,done:true}
代码分析:
- 当不参数时,next的value返回nan;
- 当传参数时,作为上一个yeild的值,在第一次使用next时,传参数无效,只有第二次开始,才有效。
- 第一次执行next时,函数会被暂停在yeild(x+1),所以返回的是4+1=5;
- 第二次执行next时,传入的12为上一次yeild表达式的值,所以y=12,返回的是12/3=4;
- 第三次执行next时,传入的15为上一次yeild表达式的值,所以z=30,y=12;x=4,返回30+12+4=46
async/await
初入async/await
async/await在es7提出,是目前在javascript异步处理的终极解决方案。
async 其本质是 generator 函数的语法糖。相较于generator放入改进如下:
- 内置执行器:generator 函数的执行必须靠执行器,而async函数自带执行器。其调用方式与普通函数一模一样,不需要调next方法;
- 更好的语义:async表示定义异步函数,而await表示后面的表达式需要等待,相较于*和yeild更语义化;
- 更广的适用性:co模块约定,yield命令后面只能是thunk函数或 promise对象。而 async 函数的await命令后面则可以是promise 或者 原始类型的值;
- 返回promise:async 函数返回值是promise对象,比 generator函数返回的 iterator对象方便,可以直接使用 then() 方法进行链式调用;
语法分析
async语法
用来定义异步函数,自动将函数转换为promise对象,可以使用then来添加回调,其内部return的值作为then回调的参数。
async function f(){ return "hello async"; } f().then((res) => { //通过then来添加回调且内部返回的res作为回调的参数 console.log(res); // hello async })
在异步函数的内部可以使用await,其返回的promise对象必须等到内部所以await命令后的promise对象执行完,才会发生状态变化即执行then回调。
const delay = function(timeout){ return new promise(function(resolve){ return settimeout(resolve, timeout); }); } async function f(){ await delay(1000); await delay(2000); return '完成'; } f().then(res => console.log(res));//需要等待3秒之后才会打印:完成
await即表示异步等待,用来暂停异步函数的执行,只能在异步函数和promise使用,且当使用在promise前面,表示等待promise完成并返回结果。
async function f() { return await 1 //await后面不是promise的话,也会被转换为一个立即为resolve的promise }; f().then( res => console.log("处理成功",res))//打印出:处理成功 1 .catch(err => console.log("处理是被",err))////打印出:promise{<resolved>:undefined}
错误处理
如果await后面的异步出现错误,等同于async返回的promise对象为reject,其错误会被catch的回调函数接收到。需要注意的是,当 async 函数中只要一个 await 出现 reject 状态,则后面的 await 都不会被执行。
let a; async function f(){ await promise.reject("error") a = await 1 //该await并没有执行 } err().then(res => console.log(a))
怎么处理呢,可以把第一个await放在try/catch,遇到函数的时候,可以将错误抛出并往下执行。
async function f() { try{ await promise.reject('error'); }catch(error){ console.log(error); } return await 1 } f().then(res => console.log('成功', res))//成功打印出1
如果有多个await处理,可以统一放在try/catch模块中,而且async可以使得try/catch同时处理同步和异步错误。
总结
通过以上六种javascript异步处理的常用方法,可以看出async/await可以说是异步终极解决方案了,最后看一下async/await用得最多的场景:
如果一个业务需要很多个异步操作组成,并且每个步骤都依赖于上一步的执行结果,这里采用不同的延时来体现:
//首先定义一个延时函数 function delay(time) { return new promise(resolve => { settimeout(() => resolve(time), time); }); } //采用promise链式调用实现 delay(500).then(result => { return delay(result + 1000) }).then(result => { return delay(result + 2000) }).then(result => { console.log(result) //3500ms后打印出3500 }).catch(error => { console.log(error) }) //采用async实现 async function f(){ const r1 = await delay(500) const r2 = await delay(r1+1000) const r3 = await delay(r2+2000) return r3 } f().then(res =>{ console.log(res) }).catch(err=>{ console.log(err) })
可以看出,采用promise实现采用了很多then进行不停的链式调用,使得代码变得冗长和复杂且没有语义化。而 async/await首先使用同步的方法来写异步,代码非常清晰直观,而且使代码语义化,一眼就能看出代码执行的顺序,最后 async 函数自带执行器,执行的时候无需手动加载。
总结
以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对的支持。