🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
- 异步的诞生 - ajax年代 - Promise年代 - promise和生成器 - 什么是生成器 - 怎么和promise配合 - Co - Async/Await [TOC] ## 异步的诞生 javascript由于设计之初被设计成了单线程,So,这会导致一个问题,如果有一个任务的量太重很耗费时间,那这个任务后面的代码就会因为它被阻塞很久才能执行。 有些man觉得这段等待的时间蛮浪费的,于是冒出了一个想法,有木有办法不让我们这么干等着,它操作它的,我们剩下的程序继续执行自己的,当它最终拿到结果了再通知我们。 嗯……异步解决的就是这么个东东。。。 但这样推理异步的诞生私以为是错误的。 事实上,javascript被设计出来的主要作用就是用来处理DOM的,它天生就是异步的,如果说有什么是为了适应这种设计而生,那么单线程这种设计才是故意而为之的。 为什么这么说呢? 想象一下,我们要一个元素在0.5秒的时间向左移动100px,接着再让它在0.5秒的时间往右移动100px,如果是多线程,这两个任务几乎会同时下单,也就意味着这个元素几乎不会动,这显然和我们预期的结果不同。 而如果是单线程如果是异步操作,那么这个元素会先向右运动,然后在0.5秒的时间完成运动后,将向左的运动作为`宏任务`加入到`callbacks queque`中,作为一轮单独在执行栈中再执行,纵然它又会被当做异步任务分发出去,但却确保了向左的操作是在向右操作完成以后的某个时机才开始执行的。 虽然说多线程不是不能做到(类似于锁这样的操作),但实现起来肯定不如一个线程简单(一个人干事不存在多个人干事需要协调的问题),而javascript最初只用了10天的时间就被创造了出来! ## ajax年代 在这个年代,我们的网站不再是一滩死水,我们开始能通过异步的HTTP请求来更新我们网页的部分信息, 我们的代码中开始出现这样的书写结构 ``` $.ajax({ type: "GET", url: "地址!!", data: {param1:xxx, param2:xxx}, dataType: "json", success: function(data){ } }); ``` 上一段落我们说过,异步任务帮我们解决了阻塞问题,js的回调机制(事件环)帮我们解决了异步任务的执行顺序问题,但成也萧何败萧何,有些场景我们的异步任务是需要嵌套的,一层套一层,那么我们的代码就会长成这样 ``` $.ajax({ type: "GET", url: "地址!!", data: {param1:xxx, param2:xxx}, dataType: "json", success: function(data){ $.ajax({ type: "GET", url: "地址!!", data: {param1:xxx, param2:xxx}, dataType: "json", success: function(data){ $.ajax({ type: "GET", url: "地址!!", data: {param1:xxx, param2:xxx}, dataType: "json", success: function(data){ } }); } }); } }); ``` 这就是所谓的回调地狱了 嗯....这维护起来同志们肯定、铁定、一定呀!觉得相当不方便! 于是就开始折腾。。。想去改变这种传统异步方法的**书写形式**,想办法让代码更易读易维护 ## Promise年代 > Promise 的原理与用法详见我的这篇白菜大文 [Promise深度学习—我のPromise/A+实现](https://fancierpj0.github.io/iPromise/) 这个年代,我们在书写异步代码的形式上取得了一定程度的进步,我们写起代码来是像这个样子滴 ``` $('div').find().css()... ``` 嗯,开了个玩笑别介意。。。其实大体想法就是这样的,像jQ一样**链式书写**异步代码 ``` read(url,encode){ return new Promise((resolve,reject)=>{ readFile(url,encode,(err,data)=>{ if(err)?reject(err):resolve(data); }) }) } read('1.txt','utf8').then(value=>{ return readFile(value,'utf8'); //根据1.txt的内容来查找读取2.txt }).then(value=>{ return readFile(value,'utf8'); //根据2.txt的内容来查找读取3.txt }).then((value)=>{ console.log(value); //输出3.txt的内容 }).catch((err)=>{ //deal with error }) //下一次then接收的参数为上一次return的结果,如果这个return的结果为promise则为promise的结果 ``` 嗯。。。好想好上不少? emmm....好上不少才有鬼咧! 虽然通过promise的then方法让我们实现了链式调用,但我们还需要手动将原本的异步API进行一次封装,并且还要每次在then中将这个封装的函数return执行,这。。。。 >[imortant] promise就像是一个异步API的包装器,它能将传统的异步API的`本体`和`回调`部分进行分离,让我们更好的专注于异步回调的处理。 ## promise和生成器 个人觉得单单是promise的话,其实相当的。。。鸡肋!真正使promise发扬光大的是在人们认识到不论怎样异步终究是异步终究是一种反人类的操作,我们理应竖起大义的旗帜开始反击的时候。 什么不反人类?当然是同步代码啊!书写简单又易于阅读~ 那怎么做到呢?其实借由生成器这么个东东我们就能够实现啦。 ### 什么是生成器 那么,我们需要先了解一下生成器是什么 生成生成,就是要生点什么,那么`生成器`生了点什么呢?生成器实际上生成了`迭代器`。 emmm...那迭代器又是个什么鬼呢?迭代器其实就是有next方法的对象,每次调用next方法都会返回一个data和一个标识符(用来标识是否已经迭代完毕)。 嗯,可能这么解释还是不怎么清楚。其实生成器它本身是一个函数,或则说是一个集成的函数,它用`*`来标识它自己,像这样`function *gen(){}`,然后我们每次调用迭代器的next方法的时候,生成器方法就会被执行一部分,只有我们通过不断调用next,这个生成器方法才会被彻底执行完成,并在最后一次next调用时返回done:false的标识。 我们来看一个示例 ``` function *r(){ let content1 = yield read('./1.txt','utf8'); let content2 = yield read(content1,'utf8'); return content2; } let it = r(); ``` 其中`*r`就是一个生成器函数,而`it`就是这个生成器函数生成的迭代器。每一次`it.next()`,生成函数都会执行一部分 ![](https://box.kancloud.cn/933117a2ea8d7fc9c9e7baf7d9116cf7_397x137.png) 其中青色的线框住的部分就是第一次调用`it.next`时执行的代码,橘色的是第二次,红色的是第三次。 也就是说每次调用时以`yield`为分界的,yield代表产出,它会以yield后面的部分作为`next`调用时返回的value值。 另外还有点需要注意的是生成器里的yield左边的`=`**并不**代表赋值运算,而代表调用`next`时会接受一个参数传入作为输入,**而content1、content2实际上是作为参数传入的形参。** >[warning] **注意:** 第一次迭代是无法传入参数的,但生成器生成迭代器时可以接收参数作为输入。 最后生成器方法的return的值就是最后一次`next`调用时返回的value值,并且此时的done为true。另外不是说从此之后不能再调next了,只是得到的对象永远都会是`{value:undefined,done:true}` ## 怎么和promise配合 我们的目的是为了使异步代码书写起来看起来像是同步代码一样 我们知道生成器函数是分段执行的,且每次迭代都会接受一个参数作为输入,然后每次都会`yield`产出。So我们能利用它这种机制 ``` function *r(p1){ console.log(p1) let content1 = yield read('./1.txt','utf8'); let content2 = yield read(content1,'utf8'); return content2; } let it = r('生成迭代器时传入的参数'); //第一次迭代 it.next().value.then(function(data){ // 2.txt //第二次迭代 it.next(data).value.then(function(data){ //第三次迭代,迭代完毕 console.log(it.next(data).value); }); }); ``` 上面的示例中,如果我们只看`*r`里面的内容,那么这样书写的形式几乎是和同步木有区别的。 那么,有没有一种方法能够让`*r`下面那一团子代码在我们在生成其中写完代码后就自己产生呢? ### Co 嗯,`Co`的出现就是为了解决这个问题的,Co是TJ大姥姥写的一个库,能帮我们自动生成迭代代码 ``` function *read() { console.log('开始'); let a = yield readFile('1.txt'); console.log(a); let b = yield readFile('2.txt'); //执行这里时必然有一个le a的输入,就像是上一句代码立即得到了返回值一样 console.log(b); let c = yield readFile('3.txt'); console.log(c); return c; } //我们只需在生成器里写完代码后再加上这么一句 co(read).then(function(data){ console.log(data); //data为成器函数中c的值 }) //--- function readFile(filename) { return new Promise(function (resolve, reject) { fs.readFile(filename, 'utf8', function (err, data) { err ? reject(err) : resolve(data); }); }) } ``` 那么这是怎么实现的呢?从代码量上来说其实很简单,就几行代码, ``` function co(gen){ //传入一个生成器 let it = gen(); //生成一个迭代器 return new Promise((resolve,reject)=>{ !function next(lastVal){ //这里的next的lastVal参数即为上一次迭代出的promise的结果,也是a的值,然后依次类推... let{value,done} = it.next(lastVal); if(done) { resolve(value); //如果生成器函数执行完成就让co的promise成功 }else{ //如果还没有迭代完,在此次返回的promise中绑定回调,当状态改变时调用下一次迭代 value.then(next,reject); } }() }) } // 效果等同于前文所说的 //第一次迭代 it.next().value.then(function(data){ // 2.txt //第二次迭代 it.next(data).value.then(function(data){ //第三次迭代,迭代完毕 console.log(it.next(data).value); }); }); ``` 思路分析: `yield readFile('1.txt')`执行完毕,会等待下一次迭代和`let a`的输入,而等到什么时候呢?会等到readFile这个异步函数得到结果后才会继续走。这时`let a`对于`yield readFile('2.txt')`是有效的,就像同步代码中立即得到了返回值一样。 --- 经过上面一遭我们终于能够像写同步代码一样写异步了,但美中不足的是每次在我们在生成器中写完异步代码,都需要在最后用Co来生成对应的迭代代码,那有没有更简单的方法呢?嗯。。。有的! ## Async/Await Async/Await 实际上是 promise+迭代器实现的语法糖,常和`bluebird` promise实现库 结合起来使用,号称异步的终极解决方案。 ``` let Promise = require('bluebird'); let readFile = Promise.promisify(require('fs').readFile); async function read() { //await后面必须跟一个promise, let a = await readFile('./1.txt','utf8'); console.log(a); let b = await readFile('./2.txt','utf8'); console.log(b); let c = await readFile('./3.txt','utf8'); console.log(c); return 'ok'; } read().then(data => { console.log(data); }); ``` 抛去语法糖的糖衣,其实就是对Co进行了一层封装 ``` //co实现 function read(){ return co(function *(){ let a = yield readFile('./1.txt'); console.log(a); let b = yield readFile('./2.txt'); console.log(b); let c = yield readFile('./3.txt'); console.log(c); return 'ok'; }); } ``` 到此为止,我们终于走完了异步编程10年发展的慢慢长路,鼓掌!!! --- 参考资料: - [Co](https://github.com/tj/co)