首页 > 代码库 > ES6-Async & 异步

ES6-Async & 异步

依赖文件地址 :https://github.com/chanceLe/ES6-Basic-Syntax/tree/master/js

  1 <!DOCTYPE html>
  2 <html>
  3     <head>
  4         <meta charset="UTF-8">
  5         <title>[es6]-16-异步操作和Async函数</title>
  6         <script src="./js/browser.js"></script>
  7         <script src="./js/babel-pollyfill.js"></script>
  8         <script type="text/babel">
  9             /*
 10              * 异步编程对js语言太重要。js语言的执行环境是单线程的,如果没有异步编程,根本没法用。
 11              * 
 12              * ES6以前,异步编程的方法,大概有四种:
 13              *   回调函数  事件监听   发布/订阅  Promise对象
 14              * 
 15              * ES6将js异步编程带入了一个新阶段,ES7的Async函数更是提出了异步编程的终极解决方案。
 16              * 
 17              * 所谓异步,就是一个任务分成两段,先执行一段,然后转而执行其他任务,等做好了准备,再回头执行第二段。
 18              * 这种不连续的执行,就叫异步。
 19              * 相应的,连续的执行叫同步,由于是连续执行,所以要等待IO,这时候程序暂停,cpu浪费。
 20              * 
 21              * 回调函数:
 22              * js语言对异步编程的实现,就是回调函数。所谓回调函数,就是把任务的第二段单独写在一个函数里面。
 23              * 等到重新执行这个任务的时候,就直接调用这个函数。
 24              * 回调函数不多说,这里只说Node.js约定,回调函数的第一个参数是err,原因是执行分成两段,这两段之间抛出的
 25              * 错误,程序无法捕捉,只能当做参数,传入第二段。
 26              * 
 27              * Promise:
 28              * 回调函数本身并没有问题,问题出在多个回调函数嵌套。如果多重嵌套,代码就横向发展,很快就乱成一团,这种
 29              * 情况称为 回调函数噩梦。
 30              * Promise就是为了解决回调函数噩梦的问题提出的。它不是新的语法功能,而是一种新的写法,允许将回调函数的嵌套,
 31              * 改成链式调用。
 32              * 
 33              * Promise 的写法只是回调函数的改进,使用then方法后,异步任务的两段执行看的更清楚了,除此之外,并无新意。
 34              * Promise的最大问题是代码冗余,原来的任务被Promise包装了以下,不管什么操作,一眼看去都是then,原来的语义
 35              * 变得很不清楚。
 36              * 
 37              * 更好的写法...
 38              * Generator函数
 39              * 协程:传统的编程语言,早有异步编程解决方案。其中有一种叫做协程,意思就是多个线程互相协作,完成异步任务。
 40              *    协程有点像函数,又有点像线程。运行流程大致如下:
 41              *     第一步,协程A开始执行。
 42              *            第二步,协程A执行到一半,进入暂停,执行权转到协程B。
 43              *     第三步,(一段时间后)协程B交还执行权。
 44              *     第四步,协程A恢复执行。
 45              * 举例来说,读取文件的协程写法如下:
 46              *   function* asyncJob(){
 47              *       // 其他代码
 48              *    var f = yield readFile(fileA);
 49              *    //其他代码
 50              * }
 51              * 上面代码的奥妙在于其中的yield命令。它表示执行到此处,执行权交给其他协程。也就是说yield是异步两个阶段的分界线。
 52              * 协程遇到yield命令会暂停,等到执行权返回,再从暂停的地方继续往后执行。它最大的优点是代码的写法
 53              * 非常像同步操作,除了yield命令,几乎一模一样。
 54              * 
 55              * Generator函数的概念
 56              *  Generator函数是协程在ES6的实现,最大的特点就是可以交出函数的执行权(即暂停执行)。
 57              *  整个Generator函数就是一个封装的异步任务,或者说是异步任务的容器。异步操作需要暂停的地方,都用yield语句注明。
 58              *  
 59              * 
 60              * 异步任务的封装:
 61              * var fetch = require("node-fetch");
 62              * function *gen(){
 63              *     var url = ‘https://api.github.com/users/github‘;
 64              *  var result = yield fetch(url);
 65              * console.log(result.bio)
 66              * }
 67              * 
 68              * 上面代码中,Generator函数封装了一个异步操作,该操作先读取一个远程接口,然后从json格式的数据解析信息。
 69              * 执行这段代码的方法如下:
 70              * var g = gen();
 71              * var result = g.next();
 72              * result.value.then(function(data){
 73              *     return data.json();
 74              * }).then(function(data){
 75              *     g.next(data);
 76              * })
 77              * 
 78              * 虽然Generator函数将异步操作表示的很简洁,但是流程管理却不方便,(什么时候执行第一段,什么时候执行第二段)
 79              */
 80             
 81             /*
 82              * Thunk函数
 83              * 参数的求值策略:
 84              * Thunk函数在上世纪60年代就诞生了。那时,编程语言刚起步,计算机科学家还在研究,编译器怎么写比较好。
 85              * 争论的一个焦点是  求值策略,即函数的参数到底应该何时求值。
 86              * 
 87              * 一种意见是传值调用,即在进入函数体之前,就计算参数表达式的值。
 88              * 另一种是传名调用,直接将函数的参数表达式传入函数体,只在用到它的时候求值,Haskell语言采用这种策略。
 89              * 
 90              * 传值调用比较简单,时但是催参数求值的时候,实际上还没用到这个参数,有可能造成性能损失。
 91              * 
 92              * Thunk函数的含义:
 93              * 编译器的传名调用的实现,往往是将参数放到一个临时函数之中,再将这个临时函数传入函数体,这个临时函数就叫做Thunk函数。
 94              */
 95             {
 96                 var x = 2;
 97                 function f(m){
 98                     return m*2;
 99                 }    
100                 console.log(f(x+5));  //14  
101                 //等同于
102                 var thunk = function(){
103                     return x+5;
104                 }
105                 function f1(thunk){
106                     return thunk()*2;
107                 }
108                 
109                 //上面代码中,函数f的参数x+5被一个函数替换了。凡是用到原参数的地方,对thunk函数求值即可。
110             }
111             //这就是thunk函数的定义:它是传名调用的一种实现策略,用来替换某个表达式。
112             
113             /*
114              * js中的Thunk函数
115              * js是传值调用,它的Thunk函数含义有所不同。在js语言中,Thunk函数替换的不是表达式,而是多参数函数,将其替换成单参数的版本
116              * ,且只接受回调函数作为参数。
117              */
118             /*
119             {
120                 //正常版本的readFile(多参数版本)
121                 fs.readFile(filename,callback);
122                 //Thunk版本的readFile(单参数版本)
123                 var readFileThunk = Thunk(fileName);
124                 readFileThunk(callback);
125                 
126                 var Thunk = function(fileName){
127                     return function(callback){
128                         return fs.readFile(fileName,callback);
129                     }
130                 }
131                 
132                 //上面代码中,fs模块的readFile方法是一个多参数函数,两个参数分别是文件名和回调函数。
133                 //经过转换器处理,它变成了一个单参数函数,只接受回调函数作为参数。
134                 //这个单参数版本,就叫做Thunk函数。
135             }
136             //任何函数,只要参数有回调函数,就能协程Thunk函数的形式。下面是一个简单的Thunk函数转换器
137         
138             {
139                 //es5版本
140                 var Thunk = function(fn){
141                     return function(){
142                         var args = Array.prototype.slice.call(arguments);
143                         return function(callback){
144                             args.push(callback);
145                             return fn.apply(this,args);
146                         }
147                         
148                     }
149                 }
150             }
151             {
152                 //es6 版本
153                 var Thunk = function(fn){
154                     return function(...args){
155                         return function(callback){
156                             return fn.call(this,...args,callback);
157                         }
158                     }
159                 }
160             }
161             */
162             /*
163              * Thunk函数在以前确实没什么用,但有了ES6的Generator函数之后,Thunk函数可以用于Generator函数的
164              * 自动流程管理。
165              */
166             //Generator函数可以自动执行
167             
168              {
169                  function* gen(){
170                      //some code
171                      yield 1;
172                      yield 2;
173                      yield 3;
174                      yield 4;
175                  }
176                  var g = gen();
177                  var res = g.next();
178                  
179                  while(!res.done){
180                      console.log(res.value);
181                      res = g.next();
182                  }
183              }
184              /*
185              上面代码中,Generator函数gen会自动执行完所有步骤。但是,这并不适合异步操作。
186              如果必须保证前一步执行完,后一步才执行,那上面的自动执行就不可行。这时Thunk函数就能派上用场。
187              在Generator函数中,yield命令用于将程序的执行权移出Generator函数,那么需要一种方法将
188              执行权还给Generator函数。这种方法就是Thunk函数,因为它可以在回调函数里,将执行权交还给
189              Generator函数。
190              */
191             
192             //Thunk函数真正的威力,在于可以自动执行Generator函数。
193             //下面是一个基于Thunk函数的Generator执行器。
194             
195             function run(fn){
196                 var ge = fn();
197                 function next(err,data){
198                     var result = ge.next(data);
199                     if(result.done) return;
200 //                    result.value(next);
201                     console.log(result.value);
202                     result.value = next();
203                     
204                 }
205                 next();
206             }
207             function* g4(){
208                 yield 1;
209                 yield 2;
210                 yield 3;
211                 yield 4;
212             }
213             run(g4);
214             /*
215              * 上面的代码的run函数,就是一个Generator函数的自动执行器。内部的next函数就是Thunk的
216              * 回调函数。next先将指针移到Generator函数的下一步。然后判断Generator函数是否结束,如果没结束,就将
217              * next函数,再传入Thunk函数(result.value属性),否则直接退出。
218              * 有了这个执行器,执行Generator函数方便多了。不管内部有多少个异步操作,直接把Generator函数传入
219              * run函数即可。当然前提是,每个异步操作都要是Thunk函数,也就是说跟在yield命令后面的必须是Thunk函数。
220              * 
221              * var g = function*(){
222              *     var f1 = yield readFile("fileA")
223              * var f2 = yield readFile("fileB")
224              * var f3 = yield readFile("fileC")
225              * ...
226              * var f4 = yield readFile("fileD")
227              * }
228              * run(g);
229              * 
230              * 上面代码中,函数g封装了n个异步的读取文件操作,只要执行run函数,这些操作就会自动完成。
231              * 这样一来,异步操作不仅可以写的像同步操作,而且一行代码就可以执行。
232              * 
233              * Thunk函数并不是唯一自动执行Generator函数的方案。因为自动执行的关键是,必须有一种机制,
234              * 自动控制Generator函数的流程,接收和交还程序的执行权。回调函数可以做到这一点,Promise对象也可以做到这一点。
235              */
236             
237             /*
238              * co模块
239              * 是著名程序员TJ Holowaychuk于2013年6月发布的小工具,用于Generator函数的自动执行。
240              * 比如有一个Generator函数,co模块可以让你不用编写Generator函数的执行器。
241              * var co = require(‘co‘);
242                 co(gen);
243                 上面代码中,Generator函数只要传入co函数,就会自动执行。
244                 co函数返回一个Promise对象,因此可以用then方法添加回调函数。
245                 
246                 co(gen).then(function(){
247                     console.log("Generator函数执行完成。")
248                 })
249                 
250               co模块的原理?
251               Generator就是一个异步操作的容器。它的自动执行需要一种机制,当异步操作有了结果,能够自动交回执行权。
252               两种方法可以做到这点:
253               1.回调函数。 将异步操作包装成Thunk函数,在回调函数里面交回执行权。
254               2.Promise对象。将异步对象包装成Promise对象,用then方法交回执行权。
255               
256               co模块其实就是将两种自动执行器(Thunk函数和Promise对象),包装成一个模块。
257               使用co的前提条件是,Generator函数的yield后面,只能在是Thunk函数或Promise对象。
258              */
259             
260             /*
261              *基于Promise对象的自动执行。
262              * 跟Thunk函数的差别就是用then方法,层层添加回调函数具体实现如下
263              */
264             /*
265              function run(gen){
266                  var g = gen();
267                  function next(data){
268                      var result = g.next(data);
269                      if(result.done){
270                          return result.value;
271                      }
272                      result.value.then(function(data){
273                          next(data);
274                      })
275                  }
276                  next();
277              }
278              
279              run(gen);
280              //上面代码中,只要Generator函数还没执行到最后一步,next函数就调用自身,以此实现自动执行。
281              * */
282             
283             
284             /*
285              * co模块是Generator自动执行器的扩展,源码只有几十行非常简单。
286              * 首先,co函数接受Generator函数作为参数,返回一个Promise对象。
287             
288             
289             function co(gen){
290                 var ctx = this;
291                 return new Promise(function(resolve,reject){
292                 })
293             }
294              */
295             /*
296              * 在返回的Promise对象里面,co先检查函数gen是否为Generator函数。如果是,就执行该函数,得到一个内部指针对象
297              * 如果不是就返回,并将Promise的状态改为resolved。
298              
299             function co(gen){
300                 var ctx = this;
301                 return new Promise(function(resolve,reject){
302                     if(typeof gen === ‘function‘){
303                         gen = gen.call(ctx);
304                     }
305                     if(!gen || typeof gen.next !== ‘function‘){
306                         return resolve(gen);
307                     }
308                 })
309             }
310             */
311             
312             /*
313              * 接着,co将Generator函数的内部指针对象的next方法,包装成onFullfilled函数。
314              * 这主要是为了能够捕捉抛出的错误。
315              */
316             
317             function co(gen){
318                 var ctx = this;
319                 return new Promise(function(resolve,reject){
320                     if(typeof gen === function){
321                         gen = gen.call(ctx);
322                     }
323                     if(!gen || typeof gen.next !== function){
324                         return resolve(gen);
325                     }
326                     
327                     onFullfilled();
328                     function onFullfilled(res){
329                         var ret;
330                         try {
331                             ret = gen.next(res);
332                         }catch(e){
333                             return reject(e);
334                         }
335                         next(ret);
336                     }
337                 })
338             }
339             //最后,就是关键的next函数,它会反复调用自身。
340             function next(ret){
341                 if(ret.done){
342                     return resolve(ret.value);
343                 }
344                 var value = toPromise.call(ctx,ret.value);
345                 if(value && isPromise(value)){
346                     return value.then(onFullfilled,onRejected);
347                 }
348                 return onRejected(new TypeError("You may only yield a function,promise,generator,"
349                 +"array or object,but the following object was passed:‘"+String(ret.value)+""));
350             }
351             
352             /*
353              * 上面代码中,next函数的内部代码,
354              * 第一行,检查当前行为是否为Generator函数的最后一步,如果是就返回。
355              * 第二行确保每一步的返回值,是Promise对象。
356              * 第三行,使用then方法,为返回值加上回调函数,然后通过onFullfilled函数再次调用next函数。
357              */
358             
359             /*
360              * 处理并发的异步操作
361              * co支持并发的异步操作,即允许某些操作同时进行,等到他们全部完成,才进行下一步。
362              * 这时,要把并发的操作都放在数组或对象里面,跟在yield语句后面。
363              *
364              */
365             //数组的写法
366             co(function*(){
367                 var res = yield [
368                 Promise.resolve(1),
369                 Promise.resolve(2)
370                 ];
371                 console.log(res);
372             }).catch(onerror);
373             //对象的写法
374             co(function*(){
375                 var res = yield {
376                     1:Promise.resolve(1),
377                     2:Promise.resolve(2)
378                 };
379                 console.log(res);
380             }).catch(onerror);
381             
382             //下面还有一个例子
383             co(function*(){
384                 var values = [n1,n2,n3];
385                 yield values.map(somethingAsync);
386             });
387             function* somethingAsync(x){
388                 //do something async
389                 return y;
390             }
391             //上面的代码允许并发三个somethingAsync异步操作,等到他们全部完成,才会进行下一步。
392         </script>
393     </head>
394     <body>
395     </body>
396 </html>

 

ES6-Async & 异步